From 55e321133312a1627cf6a50c434603d4779a77e6 Mon Sep 17 00:00:00 2001 From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:52:55 -0400 Subject: [PATCH 01/14] refactor: webpack/build scripts (#4670) * test: fix core tests prompting build task Problem: When we run the core tests they prompt for a build task before running. We should not have to do this. Solution: Create a specific build task and explicitly use that. I think due to our recent changes the "defaultBuildTask" cannot be appropriately resolved which is why this change was needed. Signed-off-by: Nikolas Komonen * refactor: remove index files in core Problem: We had index files in the core package which we used to expose certain methods once we packaged core in to a node module for use by the toolkit and amazonq package. We don't need it. Solution: Stop using the index files and go back to using the extension*.ts files Signed-off-by: Nikolas Komonen * refactor: dynamically build webpack configs Now we can dynamically build webpack configs by exporting a function instead of the config. This will allow us to check for the 'development' mode at creation and from there we can modify the webpack config that we return. So now if we have `webpack --mode development`, we can recognize we are in development mode and incrementally change our config to our liking. Signed-off-by: Nikolas Komonen * refactor: webpack/build scripts This commit: - Adds some comments to certain parts of the build process - Fixes our build tasks so that `core` will build all of the required artifacts for `toolkit` will run properly. Before there were cases where running the `Extension (toolkit)` would fail due to a missing `dist/vue` folder - Fixes webviews not reflecting updated code when we refresh the webview during debugging. This was due to the `webview serve` not being utilized correctly. Now if you change `.vue` code and reload the webview the changes should be seen. Signed-off-by: Nikolas Komonen * refactor: remove vue hot reload This feature doesn't seem to work, or just isn't worth the effort to get working. This removes use of it, but we can always look to add it in if we have a need for it. Signed-off-by: Nikolas Komonen * fix: post debug task not found We had multiple debug tasks with the same name, so when we tried to run them it didn't know which one to use. Solution: Rename one of them so the names are unique Signed-off-by: Nikolas Komonen * refactor: webpack web configs + scripts This commit: - Updates the webpack web config to be dynamic, exporting a function which is used to create the config. Previously we exported the final object. - As a result the users of the config had to update to work with this change. - Now we can tweak the config depending on input arguments - Update the tasks in the launch.json to improve the debug mode in VS Code. - Remove the `serve` configs from the main webpack. - We previously used these for hot reloading but since we do not have a use for them anymore we are getting rid of them and simplifying things. Signed-off-by: Nikolas Komonen * refactor: browser test output unique file Problem: We need to both compile all source code + webpack a web extension when running the Web unit tests so that we do not have type errors + have an executable file (webpacked file) The problem is that the name extensionWeb.js is shared by both the compiled output AND webpacked output. So one gets overwritten. Solution: Change the name of the webpacked output so that it does not get overwritten. Now in unit tests we target that specifically. Signed-off-by: Nikolas Komonen * refactor: clean up VS Code Debug menu Problem: The VS Code Debug launch menu is cluttered with lots of different launch configs. It is confusing and there are many rarely used configs. Solution: Clean it up and reorder the configs so they are more relevant. Signed-off-by: Nikolas Komonen * refactor: web mode webpack Before we had a custom flag to not build certain Web mode bundles. But it is instead easier to always build it, but output the bundle with a different name. We did this due to overlapping output files with the same name, but with this new change it does not happen anymore. Signed-off-by: Nikolas Komonen * upgrade @vscode/test-web module Signed-off-by: Nikolas Komonen * PR comment fixes: - Update CONTRIBUTING regarding webview dev server - Change the script name for web development compilation Signed-off-by: Nikolas Komonen --------- Signed-off-by: Nikolas Komonen --- CONTRIBUTING.md | 8 +- aws-toolkit-vscode.code-workspace | 4 +- package-lock.json | 72 -------- packages/amazonq/webpack.config.js | 44 ++--- packages/core/.vscode/launch.json | 59 +++---- packages/core/.vscode/tasks.json | 50 ++---- packages/core/package.json | 31 ++-- packages/core/scripts/test/testWeb.ts | 2 +- packages/core/src/auth/index.ts | 5 + packages/core/src/extension.ts | 4 - packages/core/src/extensionWeb.ts | 3 - packages/core/src/index.ts | 6 - packages/core/src/indexWeb.ts | 6 - packages/core/src/webviews/main.ts | 3 +- packages/core/webpack.config.js | 61 ++++--- packages/toolkit/.vscode/launch.json | 67 ++++---- packages/toolkit/.vscode/tasks.json | 156 +++++++++--------- packages/toolkit/package.json | 8 +- packages/toolkit/scripts/build/copyFiles.ts | 12 +- packages/toolkit/src/main.ts | 6 +- packages/toolkit/src/mainWeb.ts | 6 +- packages/toolkit/webpack.config.js | 64 +++---- packages/webpack.base.config.js | 174 ++++++++++---------- packages/webpack.vue.config.js | 114 +++++++------ packages/webpack.web.config.js | 131 +++++++-------- 25 files changed, 485 insertions(+), 611 deletions(-) delete mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/indexWeb.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cf0bca24c4..358eb4860fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -517,7 +517,13 @@ requests just from the model/types. ### Webview dev-server -Webviews can be hot-reloaded (updated without restarting the extension) by running a developer server provided by webpack. This server is started automatically when running the `Extension` launch configuration. You can also start it by running `npm serve`. Note that only frontend components will be updated; if you change backend code you will still need to restart the development extension. +Webviews can be refreshed to show changes to `.vue` code when running in Debug mode. You do not have to +reload the Debug VS Code window. + +- Use `Command Palette` -> `Reload Webviews` +- Only the frontend `.vue` changes will be reflected. If changing any backend code you must restart Debug mode. + +This works by continuously building the final Vue webview files (`webpack watch`) and then serving them through a local server (`webpack serve`). Whenever a webview is loaded it will grab the latest build from the server. ### Font generation diff --git a/aws-toolkit-vscode.code-workspace b/aws-toolkit-vscode.code-workspace index e45fe8b71a0..f03cfefd48d 100644 --- a/aws-toolkit-vscode.code-workspace +++ b/aws-toolkit-vscode.code-workspace @@ -4,10 +4,10 @@ "path": "." }, { - "path": "packages/core" + "path": "packages/toolkit" }, { - "path": "packages/toolkit" + "path": "packages/core" }, { "path": "packages/amazonq" diff --git a/package-lock.json b/package-lock.json index 8f378927468..4393bbf51a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9296,40 +9296,6 @@ "node": ">=18" } }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -12832,40 +12798,6 @@ "node": ">= 14" } }, - "node_modules/jsdom/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -19778,10 +19710,6 @@ "vscode": "^1.68.0" } }, - "packages/core/node_modules/@amzn/codewhisperer-streaming": { - "resolved": "src.gen/@amzn/codewhisperer-streaming", - "link": true - }, "packages/core/node_modules/@types/node": { "version": "16.18.95", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.95.tgz", diff --git a/packages/amazonq/webpack.config.js b/packages/amazonq/webpack.config.js index 3f3248c8676..bda003e4d42 100644 --- a/packages/amazonq/webpack.config.js +++ b/packages/amazonq/webpack.config.js @@ -7,33 +7,33 @@ * This is the final webpack config that collects all webpack configs. */ -const baseConfig = require('../webpack.base.config') -const baseVueConfig = require('../webpack.vue.config') -const baseWebConfig = require('../webpack.web.config') +const baseConfigFactory = require('../webpack.base.config') +const baseVueConfigFactory = require('../webpack.vue.config') +const baseWebConfigFactory = require('../webpack.web.config') -const config = { - ...baseConfig, - entry: { - 'src/extension': './src/extension.ts', - }, -} +module.exports = (env, argv) => { + const config = { + ...baseConfigFactory(env, argv), + entry: { + 'src/extension': './src/extension.ts', + }, + } -const vueConfigs = baseVueConfig.configs.map(c => { - // Inject entry point into all configs. - return { - ...c, + const vue = baseVueConfigFactory(env, argv) + const vueConfig = { + ...vue.config, entry: { - ...baseVueConfig.utils.createVueEntries(), + ...vue.createVueEntries(), //'src/amazonq/webview/ui/amazonq-ui': './src/amazonq/webview/ui/main.ts', }, } -}) -const webConfig = { - ...baseWebConfig, - entry: { - 'src/extensionWeb': './src/extensionWeb.ts', - }, -} + const webConfig = { + ...baseWebConfigFactory(env, argv), + entry: { + 'src/extensionWeb': './src/extensionWeb.ts', + }, + } -module.exports = [config, ...vueConfigs, webConfig] + return [config, vueConfig, webConfig] +} diff --git a/packages/core/.vscode/launch.json b/packages/core/.vscode/launch.json index d81242ae7c5..89470127224 100644 --- a/packages/core/.vscode/launch.json +++ b/packages/core/.vscode/launch.json @@ -21,7 +21,7 @@ "AWS_TOOLKIT_AUTOMATION": "local" }, "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "build" }, { "name": "Extension Tests (current file)", @@ -40,7 +40,7 @@ "AWS_TOOLKIT_AUTOMATION": "local" }, "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "build" }, { "name": "Extension Tests (web)", @@ -51,12 +51,11 @@ "--disable-extension=amazonwebservices.aws-toolkit-vscode", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionDevelopmentKind=web", - "--extensionTestsPath=${workspaceFolder}/dist/src/testWeb/testRunner", + "--extensionTestsPath=${workspaceFolder}/dist/src/testWeb/testRunnerWebCore", "${workspaceRoot}/dist/src/testFixtures/workspaceFolder" ], "outFiles": ["${workspaceFolder}/dist/src/**/*.js"], - "preLaunchTask": "webWatch", - "postDebugTask": "webRunTerminate" + "preLaunchTask": "testsBuildWatch" }, { "name": "Integration Tests", @@ -74,7 +73,7 @@ "AWS_TOOLKIT_AUTOMATION": "local" }, "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "build" }, { "name": "Integration Tests (current file)", @@ -93,7 +92,7 @@ "AWS_TOOLKIT_AUTOMATION": "local" }, "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "build" }, { "name": "E2E Test (current file)", @@ -112,7 +111,7 @@ "AWS_TOOLKIT_AUTOMATION": "local" }, "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" + "preLaunchTask": "build" }, { "name": "Test Lint", @@ -120,29 +119,31 @@ "request": "launch", "program": "${workspaceFolder}/scripts/lint/testLint.ts", "outFiles": ["${workspaceFolder}/dist/**/*.js"], - "preLaunchTask": "${defaultBuildTask}" - }, - { - "name": "Attach to ASL Server", - "type": "node", - "request": "attach", - "port": 6009, - "restart": true, - "outFiles": ["${workspaceRoot}/dist/src/stepFunctions/asl/**.js"] - }, - { - "name": "Attach to SSM Document Language Server", - "type": "node", - "request": "attach", - "port": 6010, - "restart": true, - "outFiles": ["${workspaceRoot}/dist/src/ssmDocument/ssm/ssmServer.js"] + "preLaunchTask": "build" } + // ---- We do not need the following currently, and we want to reduce clutter. Re-enable if necessary ---- + // { + // "name": "Attach to ASL Server", + // "type": "node", + // "request": "attach", + // "port": 6009, + // "restart": true, + // "outFiles": ["${workspaceRoot}/dist/src/stepFunctions/asl/**.js"] + // }, + // { + // "name": "Attach to SSM Document Language Server", + // "type": "node", + // "request": "attach", + // "port": 6010, + // "restart": true, + // "outFiles": ["${workspaceRoot}/dist/src/ssmDocument/ssm/ssmServer.js"] + // } ], "compounds": [ - { - "name": "Extension + Attach to SSM Document Language Server", - "configurations": ["Extension", "Attach to SSM Document Language Server"] - } + // ---- We do not need the following currently, and we want to reduce clutter. Re-enable if necessary ---- + // { + // "name": "Extension + Attach to SSM Document Language Server", + // "configurations": ["Extension", "Attach to SSM Document Language Server"] + // } ] } diff --git a/packages/core/.vscode/tasks.json b/packages/core/.vscode/tasks.json index ebaed8c9590..850565e29a1 100644 --- a/packages/core/.vscode/tasks.json +++ b/packages/core/.vscode/tasks.json @@ -3,47 +3,31 @@ { "version": "2.0.0", "tasks": [ + { + // Single build task to enable parallel tasks + "label": "build", + "dependsOn": ["watch", "webpackWatch"] + }, { "label": "watch", "type": "npm", "script": "watch", "problemMatcher": "$tsc-watch", - "isBackground": true, - "group": { - "kind": "build", - "isDefault": true - }, - "dependsOn": ["serve"] + "isBackground": true }, { - "label": "serve", - "type": "npm", - "script": "serve", - "group": "build", + "label": "webpackWatch", + "command": "npm run webpackDev -- --watch", + "type": "shell", "isBackground": true, - "problemMatcher": { - "owner": "custom", - "pattern": { - "regexp": ".", - "file": 1, - "location": 2, - "message": 3 - }, - "background": { - "activeOnStart": true, - "beginsPattern": "Project is running at", - "endsPattern": "compiled successfully" - } - } + "problemMatcher": "$ts-webpack-watch" }, { - "label": "webWatch", - "type": "npm", - "script": "webWatch", - "detail": "Webpacks our toolkit code (with --watch) in preparation to be run in the browser", + "label": "testsBuildWatch", + "command": "npm run compileDev -- --watch", + "detail": "Build that is sufficient for Web mode tests", + "type": "shell", "isBackground": true, - // Since `webpack --watch` never terminates (but finishes packaging at some point), - // VS Code uses this to parse the CLI output to pattern match something that indicates it is done "problemMatcher": "$ts-webpack-watch" }, /** @@ -53,8 +37,8 @@ From: https://stackoverflow.com/a/60330174 **/ { - "label": "webRunTerminate", - "command": "echo ${input:webRunTerminate}", + "label": "webRunTerminateCore", + "command": "echo ${input:webRunTerminateCore}", "type": "shell" }, { @@ -94,7 +78,7 @@ "args": "terminateAll" }, { - "id": "webRunTerminate", + "id": "webRunTerminateCore", "type": "command", "command": "workbench.action.tasks.terminate", "args": "webRun" diff --git a/packages/core/package.json b/packages/core/package.json index b71b83dad85..2f92536b577 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,10 +27,10 @@ "onCommand:aws.codeWhisperer.accept" ], "main": "./dist/src/extension.js", - "browser": "./dist/src/extensionWeb", + "browser": "./dist/src/extensionWebCore.js", "exports": { - ".": "./dist/src/index.js", - "./web": "./dist/src/indexWeb.js", + ".": "./dist/src/extension.js", + "./web": "./dist/src/extensionWeb.js", "./auth": "./dist/src/auth/index.js" }, "contributes": { @@ -4218,14 +4218,15 @@ "clean": "ts-node ../../scripts/clean.ts dist/", "copyFiles": "ts-node ./scripts/build/copyFiles.ts", "buildScripts": "npm run generateClients && npm run generatePackage && npm run copyFiles", - "compile": "npm run clean && npm run buildScripts && tsc -p ./ && webpack --mode development", - "compileLite": "tsc -p ./", - "watch": "npm run clean && npm run buildScripts && tsc -watch -p ./", - "testCompile": "npm run clean && npm run buildScripts && tsc -p ./", + "compile": "npm run testCompile && webpack", + "compileOnly": "tsc -p ./", + "compileDev": "npm run compile -- --mode development", + "webpackDev": "webpack --mode development", + "serveVue": "webpack serve --config-name vue --mode development", + "watch": "npm run testCompile -- -- --watch", + "testCompile": "npm run clean && npm run buildScripts && npm run compileOnly", "test": "npm run testCompile && c8 ts-node ./scripts/test/test.ts", - "webCompile": "npm run clean && npm run buildScripts && webpack --config-name web", - "webWatch": "npm run webCompile -- --watch", - "testWeb": "npm run webCompile && c8 ts-node ./scripts/test/testWeb.ts", + "testWeb": "npm run compileDev && c8 ts-node ./scripts/test/testWeb.ts", "testE2E": "npm run testCompile && c8 ts-node ./scripts/test/testE2E.ts", "testInteg": "npm run testCompile && c8 ts-node ./scripts/test/testInteg.ts", "format": "prettier --ignore-path ../../.prettierignore --check src scripts", @@ -4233,8 +4234,14 @@ "lint": "ts-node ./scripts/lint/testLint.ts && npm run format", "generateClients": "ts-node ./scripts/build/generateServiceClient.ts ", "generatePackage": "ts-node ./scripts/build/generateIcons.ts", - "generateTelemetry": "node ../../node_modules/@aws-toolkits/telemetry/lib/generateTelemetry.js --extraInput=src/shared/telemetry/vscodeTelemetry.json --output=src/shared/telemetry/telemetry.gen.ts", - "serve": "webpack serve --config-name vue-hmr --mode development" + "generateTelemetry": "node ../../node_modules/@aws-toolkits/telemetry/lib/generateTelemetry.js --extraInput=src/shared/telemetry/vscodeTelemetry.json --output=src/shared/telemetry/telemetry.gen.ts" + }, + "scriptsComments": { + "compile": "Builds EVERYTHING we would need for a final production-ready package", + "compileOnly": "Only compiles the `core` source code.", + "compileDev": "Same as compile, but in `development` mode", + "webpackDev": "Webpacks using all configs, but in development mode", + "serveVue": "Local server for Vue.js code for development purposes. Provides faster iteration when updating Vue files" }, "devDependencies": { "@aws-sdk/types": "^3.13.1", diff --git a/packages/core/scripts/test/testWeb.ts b/packages/core/scripts/test/testWeb.ts index e3ef73f7e67..c6eca1469b2 100644 --- a/packages/core/scripts/test/testWeb.ts +++ b/packages/core/scripts/test/testWeb.ts @@ -5,5 +5,5 @@ import { runToolkitTests } from './launchTestUtilities' void (async () => { - await runToolkitTests('web', 'dist/src/testWeb/testRunner.js') + await runToolkitTests('web', 'dist/src/testWeb/testRunnerWebCore.js') })() diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 5753c0b30dd..31f413826cc 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -3,5 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +/** + * The auth specific code we are exporting to consumers of `core`. + * + * This module is exposed through `exports` in the `package.json` file + */ export { Connection } from './connection' export { Auth } from './auth' diff --git a/packages/core/src/extension.ts b/packages/core/src/extension.ts index 693b098e71b..e3d7e4559c6 100644 --- a/packages/core/src/extension.ts +++ b/packages/core/src/extension.ts @@ -230,7 +230,3 @@ function recordToolkitInitialization(activationStartedOn: number, settingsValid: logger?.error(err as Error) } } - -// Unique extension entrypoint names, so that they can be obtained from the webpack bundle -export const awsToolkitActivate = activate -export const awsToolkitDeactivate = deactivate diff --git a/packages/core/src/extensionWeb.ts b/packages/core/src/extensionWeb.ts index 2473fcdd6da..b609b18af29 100644 --- a/packages/core/src/extensionWeb.ts +++ b/packages/core/src/extensionWeb.ts @@ -39,6 +39,3 @@ function patchOsVersion() { export async function deactivate() { await deactivateShared() } - -export const awsToolkitWebActivate = activate -export const awsToolkitWebDeactivate = deactivate diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index 71d77c284fb..00000000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export { awsToolkitActivate, awsToolkitDeactivate } from './extension' diff --git a/packages/core/src/indexWeb.ts b/packages/core/src/indexWeb.ts deleted file mode 100644 index 9c31c1a155b..00000000000 --- a/packages/core/src/indexWeb.ts +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -export { awsToolkitWebActivate, awsToolkitWebDeactivate } from './extensionWeb' diff --git a/packages/core/src/webviews/main.ts b/packages/core/src/webviews/main.ts index 2634abb56b9..9dd457d2c41 100644 --- a/packages/core/src/webviews/main.ts +++ b/packages/core/src/webviews/main.ts @@ -174,7 +174,8 @@ export abstract class VueWebview { this.protocol = commands - // Will be sent to the dist/vue folder by webpack + // All vue files defined by `source` are collected in to `dist/vue` + // so we must update the relative paths to point here const sourcePath = vscode.Uri.joinPath(vscode.Uri.parse('vue/'), source).path this.source = sourcePath[0] === '/' ? sourcePath.slice(1) : sourcePath // VSCode URIs like to create root paths... } diff --git a/packages/core/webpack.config.js b/packages/core/webpack.config.js index 766da8f1a98..b9ae7010acd 100644 --- a/packages/core/webpack.config.js +++ b/packages/core/webpack.config.js @@ -4,37 +4,50 @@ */ /** - * This is the final webpack config that collects all webpack configs. + * This webpack config is used for everything else that the TypeScript transpilation does not do. + * + * Some things include: + * - Building the Web toolkit (Web extensions must be a single file, hence webpack) + * - Building Vue.js files for webviews */ -const baseConfig = require('../webpack.base.config') -const baseVueConfig = require('../webpack.vue.config') -const baseWebConfig = require('../webpack.web.config') +const baseConfigFactory = require('../webpack.base.config') +const baseVueConfigFactory = require('../webpack.vue.config') +const baseWebConfigsFactory = require('../webpack.web.config') -const config = { - ...baseConfig, - entry: { - 'src/stepFunctions/asl/aslServer': './src/stepFunctions/asl/aslServer.ts', - }, -} +module.exports = (env, argv) => { + const baseVueConfig = baseVueConfigFactory(env, argv) + const baseConfig = baseConfigFactory(env, argv) + + const config = { + ...baseConfig, + entry: { + 'src/stepFunctions/asl/aslServer': './src/stepFunctions/asl/aslServer.ts', + }, + } -const vueConfigs = [baseVueConfig.configs.vue, baseVueConfig.configs.vueHotReload].map(c => { - // Inject entry point into all configs. - return { - ...c, + const vueConfig = { + ...baseVueConfig.config, + // Inject entry point into all configs. entry: { - ...baseVueConfig.utils.createVueEntries(), + ...baseVueConfig.createVueEntries(), + // The above `createVueEntries` path pattern match does not catch this: 'src/amazonq/webview/ui/amazonq-ui': './src/amazonq/webview/ui/main.ts', }, } -}) -const WebConfig = { - ...baseWebConfig, - entry: { - 'src/extensionWeb': './src/extensionWeb.ts', - 'src/testWeb/testRunner': './src/testWeb/testRunner.ts', - }, -} + const webConfig = { + ...baseWebConfigsFactory(env, argv), + entry: { + // We webpack AND compile at the same time in certain build scripts. + // Both webpack and compile can output the same named file, overwriting one another. + // Due to this we must ensure the webpack `entry` files have a different + // name from the actual source files so we do not overwrite the output + // from the compilation. + 'src/extensionWebCore': './src/extensionWeb.ts', + 'src/testWeb/testRunnerWebCore': './src/testWeb/testRunner.ts', + }, + } -module.exports = [config, ...vueConfigs, WebConfig] + return [config, vueConfig, webConfig] +} diff --git a/packages/toolkit/.vscode/launch.json b/packages/toolkit/.vscode/launch.json index 04e5fe6e261..8fc78965fb5 100644 --- a/packages/toolkit/.vscode/launch.json +++ b/packages/toolkit/.vscode/launch.json @@ -42,24 +42,10 @@ a file in `Developer Tools` > `Sources`. I was inspired by this: https://github.com/microsoft/vscode-test-web/blob/897bca4907a87a6bc564efc242ce6794e5da3232/.vscode/launch.json#L28 **/ - "resolveSourceMapLocations": [ - // "!**/node_modules/**", // TODO: Removing this makes it slow again, but debugging in VSCode won't work without it :( - "!**/vs/**", - "!**/extensions/**" - ], + "resolveSourceMapLocations": ["!**/node_modules/**", "!**/vs/**", "!**/extensions/**"], "preLaunchTask": "webRun", "postDebugTask": "webRunTerminate" }, - { - "name": "Extension (webpack)", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceFolder}"], - "outFiles": ["${workspaceFolder}/dist/**/*.js", "${workspaceFolder}/../core/dist/**/*.js"], - "preLaunchTask": "npm: compile", - "postDebugTask": "terminate" - }, { "name": "Test Lint", "type": "node", @@ -67,28 +53,39 @@ "program": "${workspaceFolder}/scripts/lint/testLint.ts", "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "${defaultBuildTask}" - }, - { - "name": "Attach to ASL Server", - "type": "node", - "request": "attach", - "port": 6009, - "restart": true, - "outFiles": ["${workspaceRoot}/dist/src/stepFunctions/asl/**.js"] - }, - { - "name": "Attach to SSM Document Language Server", - "type": "node", - "request": "attach", - "port": 6010, - "restart": true, - "outFiles": ["${workspaceRoot}/dist/src/ssmDocument/ssm/ssmServer.js"] } + // ---- We do not need the following currently, and we want to reduce clutter. Re-enable if necessary ---- + // { + // "name": "Extension (webpack)", + // "type": "extensionHost", + // "request": "launch", + // "runtimeExecutable": "${execPath}", + // "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + // "outFiles": ["${workspaceFolder}/dist/**/*.js", "${workspaceFolder}/../core/dist/**/*.js"], + // "preLaunchTask": "npm: compile", + // "postDebugTask": "terminate" + // }, + // { + // "name": "Attach to ASL Server", + // "type": "node", + // "request": "attach", + // "port": 6009, + // "restart": true, + // "outFiles": ["${workspaceRoot}/dist/src/stepFunctions/asl/**.js"] + // }, + // { + // "name": "Attach to SSM Document Language Server", + // "type": "node", + // "request": "attach", + // "port": 6010, + // "restart": true, + // "outFiles": ["${workspaceRoot}/dist/src/ssmDocument/ssm/ssmServer.js"], + // } ], "compounds": [ - { - "name": "Extension + Attach to SSM Document Language Server", - "configurations": ["Extension", "Attach to SSM Document Language Server"] - } + // { + // "name": "Extension + Attach to SSM Document Language Server", + // "configurations": ["Extension", "Attach to SSM Document Language Server"] + // } ] } diff --git a/packages/toolkit/.vscode/tasks.json b/packages/toolkit/.vscode/tasks.json index 69b7604d8df..e8354b53606 100644 --- a/packages/toolkit/.vscode/tasks.json +++ b/packages/toolkit/.vscode/tasks.json @@ -22,102 +22,35 @@ "kind": "build", "isDefault": true }, - "dependsOn": ["watchCore", "serve"] + "dependsOn": ["watchCore", "webpackCore", "serveVueCore"] }, { - "label": "serve", - "type": "npm", - "script": "serve", - "group": "build", + "label": "watchCore", + "command": "npm run compileOnly -- --watch", + "type": "shell", "isBackground": true, - "problemMatcher": { - "owner": "custom", - "pattern": { - "regexp": ".", - "file": 1, - "location": 2, - "message": 3 - }, - "background": { - "activeOnStart": true, - "beginsPattern": "Project is running at", - "endsPattern": "compiled" - } - }, + "problemMatcher": "$tsc-watch", "options": { - "env": { - // This is the lowest GB I found to avoid heap allocation errors if you reload the debugging instance. - // May not work for everyone, and may not be enough under certain conditions. We will have - // to re-adjust as needed. I think the reason we need this is because of the large number of sourcemaps - // generated by the core-lib, but more research is needed. - "NODE_OPTIONS": "--max_old_space_size=5120" - } + "cwd": "../core" } }, { - "label": "webWatch", - "type": "npm", - "script": "webWatch", - "detail": "Webpacks our toolkit code (with --watch) in preparation to be run in the browser", + "label": "webpackCore", + "command": "npm run webpackDev -- --watch", + "type": "shell", "isBackground": true, - // Since `webpack --watch` never terminates (but finishes packaging at some point), - // VS Code uses this to parse the CLI output to pattern match something that indicates it is done "problemMatcher": "$ts-webpack-watch", - "dependsOn": ["copyPackageJson"] - }, - { - "label": "webRun", - "type": "npm", - "script": "webRun", - "detail": "Runs VS Code in the Chrome browser with our toolkit installed", - "isBackground": true, - "dependsOn": ["webWatch"], - /** - We only need this problem matcher to signal when this task is completed. - Since this task starts a web server it does not terminate and we need to use - problemMatcher.background.endsPattern to read the CLI and know when this task - can signal it is done. - - The rest of the data in problemMatcher is required by VS Code to be "valid", - but not important for what we need. - - Doc: https://code.visualstudio.com/Docs/editor/tasks#_defining-a-problem-matcher - **/ - "problemMatcher": { - "pattern": [ - { - "regexp": "this section irrelevant but it must exist to work", - "file": 1, - "location": 2, - "message": 3 - } - ], - "background": { - "activeOnStart": true, - "beginsPattern": "^@vscode/test-web", - "endsPattern": "^Listening on" - } + "options": { + "cwd": "../core" } }, { - "label": "watchCore", - "command": "npm run compileLite -- --watch", + "label": "serveVueCore", + "command": "npm run serveVue", "type": "shell", + "detail": "Webpack + local server for Vue webview files from `core`.", "isBackground": true, - "problemMatcher": { - "owner": "custom", - "pattern": { - "regexp": ".", - "file": 1, - "location": 2, - "message": 3 - }, - "background": { - "activeOnStart": true, - "beginsPattern": "Starting compilation", - "endsPattern": "Watching for file changes." - } - }, + "problemMatcher": "$ts-webpack-watch", "options": { "cwd": "../core" } @@ -159,6 +92,65 @@ }, "dependsOn": ["restorePackageJson"] }, + { + "label": "webRun", + "type": "npm", + "script": "webRun", + "detail": "Runs VS Code in the Chrome browser with our toolkit installed", + "isBackground": true, + "dependsOn": ["webWatch"], + /** + We only need this problem matcher to signal when this task is completed. + Since this task starts a web server it does not terminate and we need to use + problemMatcher.background.endsPattern to read the CLI and know when this task + can signal it is done. + + The rest of the data in problemMatcher is required by VS Code to be "valid", + but not important for what we need. + + Doc: https://code.visualstudio.com/Docs/editor/tasks#_defining-a-problem-matcher + **/ + "problemMatcher": { + "pattern": [ + { + "regexp": "this section irrelevant but it must exist to work", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^@vscode/test-web", + "endsPattern": "^Listening on" + } + } + }, + { + "label": "webWatch", + "type": "npm", + "script": "webWatch", + "detail": "Webpacks our toolkit code (with --watch) in preparation to be run in the browser", + "isBackground": true, + // Since `webpack --watch` never terminates (but finishes packaging at some point), + // VS Code uses this to parse the CLI output to pattern match something that indicates it is done + "problemMatcher": "$ts-webpack-watch", + "dependsOn": ["webCoreModuleWatch"] + }, + { + "label": "webCoreModuleWatch", + "type": "shell", + "command": "npm run compileDev -- --watch", + "detail": "Webpacks our toolkit code as a module in preparation to be webpacked by the toolkit package", + "isBackground": true, + // Since `webpack --watch` never terminates (but finishes packaging at some point), + // VS Code uses this to parse the CLI output to pattern match something that indicates it is done + "problemMatcher": "$ts-webpack-watch", + "dependsOn": ["copyPackageJson"], + "options": { + "cwd": "../core" + } + }, /** After we stop debugging our browser, we also want to stop the web server. When this task is ran it will stop the web server. diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index f56588b3599..a17996dc58f 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -60,9 +60,9 @@ "generateNonCodeFiles": "ts-node ./scripts/build/generateNonCodeFiles.ts", "copyFiles": "ts-node ./scripts/build/copyFiles.ts", "clean": "ts-node ../../scripts/clean.ts dist/ LICENSE NOTICE quickStart* CHANGELOG.md", - "compile": "npm run clean && npm run buildScripts && webpack --mode development", - "webWatch": "npm run clean && npm run copyFiles --vueHr && webpack --config-name web --watch", - "webCompile": "npm run clean && npm run buildScripts -- --vueHr && webpack --config-name web", + "compile": "npm run clean && npm run buildScripts && webpack", + "webWatch": "npm run clean && npm run buildScripts && webpack --mode development --watch", + "webCompile": "npm run clean && npm run buildScripts && webpack --config-name web", "webRun": "npx @vscode/test-web --open-devtools --browserOption=--disable-web-security --waitForDebugger=9222 --extensionDevelopmentPath=. .", "package": "npm run copyPackageJson && ts-node ../../scripts/package.ts && npm run restorePackageJson", "install-plugin": "vsce package --ignoreFile '../.vscodeignore.packages' -o aws-toolkit-vscode-test.vsix && code --install-extension aws-toolkit-vscode-test.vsix", @@ -70,7 +70,7 @@ "formatfix": "prettier --ignore-path ../../.prettierignore --write src scripts", "lint": "echo 'Nothing to lint yet!'", "createRelease": "ts-node ./scripts/build/createRelease.ts", - "watch": "npm run clean && npm run buildScripts -- --vueHr && tsc -watch -p ./", + "watch": "npm run clean && npm run buildScripts && tsc -watch -p ./", "serve": "webpack serve --config-name mainServe --mode development", "copyPackageJson": "ts-node ./scripts/build/handlePackageJson", "restorePackageJson": "ts-node ./scripts/build/handlePackageJson --restore" diff --git a/packages/toolkit/scripts/build/copyFiles.ts b/packages/toolkit/scripts/build/copyFiles.ts index 0ad572316aa..87f31617be6 100644 --- a/packages/toolkit/scripts/build/copyFiles.ts +++ b/packages/toolkit/scripts/build/copyFiles.ts @@ -7,13 +7,9 @@ import * as fs from 'fs-extra' import * as path from 'path' // Copies various dependencies into "dist/". -// -// Options: -// `--vueHr`: controls whether files are copied from "node_modules/aws-core-vscode/dist/vuehr/" (Vue Hot Reload) or "…/vue/" const projectRoot = process.cwd() const outRoot = path.join(projectRoot, 'dist') -let vueHr = false // The target file or directory must exist, otherwise we should fail the whole build. interface CopyTask { @@ -75,7 +71,7 @@ const tasks: CopyTask[] = [ destination: path.join('libs', 'vue.min.js'), }, { - target: path.join('../../node_modules/aws-core-vscode/dist', vueHr ? 'vuehr' : 'vue'), + target: path.join('../../node_modules/aws-core-vscode/dist', 'vue'), destination: 'vue/', }, @@ -112,12 +108,6 @@ async function copy(task: CopyTask): Promise { } void (async () => { - const args = process.argv.slice(2) - if (args.includes('--vueHr')) { - vueHr = true - console.log('Using Vue Hot Reload webpacks from core/') - } - try { await Promise.all(tasks.map(copy)) } catch (error) { diff --git a/packages/toolkit/src/main.ts b/packages/toolkit/src/main.ts index c42294ba95c..88c9691cd29 100644 --- a/packages/toolkit/src/main.ts +++ b/packages/toolkit/src/main.ts @@ -4,14 +4,14 @@ */ import type { ExtensionContext } from 'vscode' -import { awsToolkitActivate, awsToolkitDeactivate } from 'aws-core-vscode' +import { activate as activateCore, deactivate as deactivateCore } from 'aws-core-vscode' import { awsToolkitApi } from './api' export async function activate(context: ExtensionContext) { - await awsToolkitActivate(context) + await activateCore(context) return awsToolkitApi } export async function deactivate() { - await awsToolkitDeactivate() + await deactivateCore() } diff --git a/packages/toolkit/src/mainWeb.ts b/packages/toolkit/src/mainWeb.ts index b48bb64d653..cb792466824 100644 --- a/packages/toolkit/src/mainWeb.ts +++ b/packages/toolkit/src/mainWeb.ts @@ -4,12 +4,12 @@ */ import type { ExtensionContext } from 'vscode' -import { awsToolkitWebActivate, awsToolkitWebDeactivate } from 'aws-core-vscode/web' +import { activate as activateWeb, deactivate as deactivateWeb } from 'aws-core-vscode/web' export async function activate(context: ExtensionContext) { - return awsToolkitWebActivate(context) + return activateWeb(context) } export async function deactivate() { - await awsToolkitWebDeactivate() + await deactivateWeb() } diff --git a/packages/toolkit/webpack.config.js b/packages/toolkit/webpack.config.js index 9268352cbfe..1bc58ac246b 100644 --- a/packages/toolkit/webpack.config.js +++ b/packages/toolkit/webpack.config.js @@ -5,47 +5,25 @@ const path = require('path') const currentDir = process.cwd() - -const baseConfig = require('../webpack.base.config') -const baseWebConfig = require('../webpack.web.config') - -const devServer = { - static: { - directory: path.resolve(currentDir, 'dist'), - }, - headers: { - 'Access-Control-Allow-Origin': '*', - }, - allowedHosts: 'all', -} - -const config = { - ...baseConfig, - entry: { - 'src/main': './src/main.ts', - }, -} - -const serveConfig = { - ...config, - name: 'mainServe', - devServer, - externals: { - vscode: 'commonjs vscode', - }, +const { merge } = require('webpack-merge') + +const baseConfigFactory = require('../webpack.base.config') +const baseWebConfigsFactory = require('../webpack.web.config') + +module.exports = (env, argv) => { + const config = { + ...baseConfigFactory(env, argv), + entry: { + 'src/main': './src/main.ts', + }, + } + + const webConfig = { + ...baseWebConfigsFactory(env, argv), + entry: { + 'src/mainWeb': './src/mainWeb.ts', + }, + } + + return [config, webConfig] } - -const webConfig = { - ...baseWebConfig, - entry: { - 'src/mainWeb': './src/mainWeb.ts', - }, -} - -const webServeConfig = { - ...webConfig, - name: 'webServe', - devServer, -} - -module.exports = process.env.npm_lifecycle_event === 'serve' ? [serveConfig, webServeConfig] : [config, webConfig] diff --git a/packages/webpack.base.config.js b/packages/webpack.base.config.js index 137faee6a22..f258a96a8dc 100644 --- a/packages/webpack.base.config.js +++ b/packages/webpack.base.config.js @@ -23,94 +23,96 @@ const packageId = `${packageJson.publisher}.${packageJson.name}` //@ts-check /** @typedef {import('webpack').Configuration} WebpackConfig **/ -/** @type WebpackConfig */ -const baseConfig = { - name: 'main', - target: 'node', - output: { - path: path.resolve(currentDir, 'dist'), - filename: '[name].js', - libraryTarget: 'commonjs2', - }, - devtool: 'source-map', - externals: { - vscode: 'commonjs vscode', - vue: 'root Vue', - }, - resolve: { - extensions: ['.ts', '.js'], - alias: { - // Forces webpack to resolve the `main` (cjs) entrypoint - // - // This is only necessary because of issues with transitive dependencies - // `umd-compat-loader` cannot handle ES2018 syntax which is used in later versions of `vscode-json-languageservice` - // But, for whatever reason, the ESM output is used if we don't explicitly set `mainFields` under webpack's `resolve` - '@aws/fully-qualified-names$': '@aws/fully-qualified-names/node/aws_fully_qualified_names.js', +module.exports = (env = {}, argv = {}) => { + const isDevelopment = argv.mode === 'development' + /** @type WebpackConfig */ + const baseConfig = { + name: 'main', + target: 'node', + output: { + path: path.resolve(currentDir, 'dist'), + filename: '[name].js', + libraryTarget: 'commonjs2', }, - }, - node: { - __dirname: false, //preserve the default node.js behavior for __dirname - }, - module: { - rules: [ - { - test: /\.ts$/, - exclude: /node_modules|testFixtures/, - use: [ - // This creates nls-metadata.json in dist/, but we don't need this functionality currently. - // If we want to have language locale support later we should look to re-enable this. - // This was disabled since it caused issues with the browser version of the toolkit. - // THe localize() func does not work in the browser when used this loader, we got - // the error from - // https://github.com/microsoft/vscode-nls/blob/6f88c23c3ab630e9d4c60f65ed33839bd4a0cec2/src/browser/main.ts#L15 - // Now, with this disabled we do not create externalized strings. - // { - // loader: require.resolve('vscode-nls-dev/lib/webpack-loader'), - // options: { - // base: path.join(__dirname, 'src'), - // }, - // }, - { - // This transpiles our typescript to javascript. - loader: 'esbuild-loader', - options: { - loader: 'ts', - target: 'es2021', - }, - }, - ], - }, - { - test: /node_modules[\\|/](amazon-states-language-service|vscode-json-languageservice)/, - use: { loader: 'umd-compat-loader' }, + devtool: isDevelopment ? 'source-map' : undefined, + externals: { + vscode: 'commonjs vscode', + vue: 'root Vue', + }, + resolve: { + extensions: ['.ts', '.js'], + alias: { + // Forces webpack to resolve the `main` (cjs) entrypoint + // + // This is only necessary because of issues with transitive dependencies + // `umd-compat-loader` cannot handle ES2018 syntax which is used in later versions of `vscode-json-languageservice` + // But, for whatever reason, the ESM output is used if we don't explicitly set `mainFields` under webpack's `resolve` + '@aws/fully-qualified-names$': '@aws/fully-qualified-names/node/aws_fully_qualified_names.js', }, - ], - }, - plugins: [ - new NLSBundlePlugin(packageId), - new webpack.DefinePlugin({ - EXTENSION_VERSION: JSON.stringify(packageJson.version), - }), - new CircularDependencyPlugin({ - exclude: /node_modules|testFixtures/, - failOnError: true, - }), - ], - optimization: { - minimize: true, - minimizer: [ - new ESBuildMinifyPlugin({ - target: 'es2021', - // Are these enabled by default? - // minify: true, - // treeShaking: true, - - // De-duplicate license headers and list them at end of file. - legalComments: 'eof', - // sourcemap: 'external', + }, + node: { + __dirname: false, //preserve the default node.js behavior for __dirname + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules|testFixtures/, + use: [ + // This creates nls-metadata.json in dist/, but we don't need this functionality currently. + // If we want to have language locale support later we should look to re-enable this. + // This was disabled since it caused issues with the browser version of the toolkit. + // THe localize() func does not work in the browser when used this loader, we got + // the error from + // https://github.com/microsoft/vscode-nls/blob/6f88c23c3ab630e9d4c60f65ed33839bd4a0cec2/src/browser/main.ts#L15 + // Now, with this disabled we do not create externalized strings. + // { + // loader: require.resolve('vscode-nls-dev/lib/webpack-loader'), + // options: { + // base: path.join(__dirname, 'src'), + // }, + // }, + { + // This transpiles our typescript to javascript. + loader: 'esbuild-loader', + options: { + loader: 'ts', + target: 'es2021', + }, + }, + ], + }, + { + test: /node_modules[\\|/](amazon-states-language-service|vscode-json-languageservice)/, + use: { loader: 'umd-compat-loader' }, + }, + ], + }, + plugins: [ + new NLSBundlePlugin(packageId), + new webpack.DefinePlugin({ + EXTENSION_VERSION: JSON.stringify(packageJson.version), + }), + new CircularDependencyPlugin({ + exclude: /node_modules|testFixtures/, + failOnError: true, }), ], - }, -} + optimization: { + minimize: !isDevelopment, + minimizer: [ + new ESBuildMinifyPlugin({ + target: 'es2021', + // Are these enabled by default? + // minify: true, + // treeShaking: true, -module.exports = baseConfig + // De-duplicate license headers and list them at end of file. + legalComments: 'eof', + // sourcemap: 'external', + }), + ], + }, + } + return baseConfig +} diff --git a/packages/webpack.vue.config.js b/packages/webpack.vue.config.js index 5958cf9159d..18befe1e156 100644 --- a/packages/webpack.vue.config.js +++ b/packages/webpack.vue.config.js @@ -10,7 +10,7 @@ const path = require('path') const glob = require('glob') const { VueLoaderPlugin } = require('vue-loader') -const baseConfig = require('./webpack.base.config') +const baseConfigFactory = require('./webpack.base.config') const { merge } = require('webpack-merge') const currentDir = process.cwd() @@ -37,66 +37,64 @@ const createVueEntries = (targetPattern = 'index.ts') => { .reduce((a, b) => ((a[b.name] = b.path), a), {}) } -/** @type WebpackConfig */ -const vueConfig = merge(baseConfig, { - name: 'vue', - target: 'web', - output: { - path: path.resolve(currentDir, 'dist', 'vue'), - libraryTarget: 'this', - }, - module: { - rules: [ - { - test: /\.vue$/, - loader: 'vue-loader', - }, - { - test: /\.css$/, - use: ['vue-style-loader', 'css-loader'], - }, - { - test: /\.(png|jpg|gif|svg)$/, - use: 'file-loader', - }, - // sass loaders for Mynah - { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, - ], - }, - plugins: [new VueLoaderPlugin()], -}) +module.exports = (env, argv) => { + const isDevelopment = argv.mode === 'development' + const baseConfig = baseConfigFactory(env, argv) -/** @type WebpackConfig */ -const vueHotReloadConfig = { - ...vueConfig, - name: 'vue-hmr', - output: { - path: path.resolve(currentDir, 'dist', 'vuehr'), - libraryTarget: 'this', - }, - devServer: { - static: { - directory: path.resolve(currentDir, 'dist'), + /** @type WebpackConfig */ + let config = merge(baseConfig, { + name: 'vue', + target: 'web', + output: { + // IMPORTANT: This path influences `VueWebview.constructor` as it updates `VueWebview.source` paths relative to this + path: path.resolve(currentDir, 'dist', 'vue'), + libraryTarget: 'this', }, - headers: { - 'Access-Control-Allow-Origin': '*', + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + }, + { + test: /\.css$/, + use: ['vue-style-loader', 'css-loader'], + }, + { + test: /\.(png|jpg|gif|svg)$/, + use: 'file-loader', + }, + // sass loaders for Mynah + { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, + ], }, - // This is not ideal, but since we're only running the server locally it's not too bad - // The webview debugger tries to establish a websocket with a GUID as its origin, so not much of a workaround - allowedHosts: 'all', - }, - // Normally we want to exclude Vue from the bundle, but for hot reloads we need it - externals: { - vscode: 'commonjs vscode', - }, -} + plugins: [new VueLoaderPlugin()], + }) + + if (isDevelopment) { + // add development specific vue config settings + config = { + ...config, + devServer: { + static: { + directory: path.resolve(currentDir, 'dist'), + }, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + // This is not ideal, but since we're only running the server locally it's not too bad + // The webview debugger tries to establish a websocket with a GUID as its origin, so not much of a workaround + allowedHosts: 'all', + }, + // Normally we want to exclude Vue from the bundle, but for hot reloads we need it + externals: { + vscode: 'commonjs vscode', + }, + } + } -module.exports = { - configs: { - vue: vueConfig, - vueHotReload: vueHotReloadConfig, - }, - utils: { + return { + config, createVueEntries, - }, + } } diff --git a/packages/webpack.web.config.js b/packages/webpack.web.config.js index 6ee2b3432e6..3171c354088 100644 --- a/packages/webpack.web.config.js +++ b/packages/webpack.web.config.js @@ -11,79 +11,70 @@ */ const webpack = require('webpack') const { merge } = require('webpack-merge') +const baseConfigFactory = require('./webpack.base.config') -const baseConfig = require('./webpack.base.config') +module.exports = (env, argv) => { + const baseConfig = baseConfigFactory(env, argv) + const isDevelopment = argv.mode === 'development' -/** @type WebpackConfig */ -const webConfig = merge(baseConfig, { - name: 'web', - target: 'webworker', - /** - * The keys in the following 'entry' object are the relative paths of the final output files in 'dist'. - * They are suffixed with '.js' implicitly. - */ - plugins: [ - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1, // disable chunks by default since web extensions must be a single bundle - }), - // some modules are provided globally and dont require an explicit import, - // so we need to polyfill them using this method - new webpack.ProvidePlugin({ - process: require.resolve('process/browser'), - Buffer: ['buffer', 'Buffer'], - }), - new webpack.EnvironmentPlugin({ - NODE_DEBUG: 'development', - READABLE_STREAM: 'disable', - }), - /** - * HACK: the HttpResourceFetcher breaks Web mode if imported, BUT we still dynamically import this module for non web mode - * environments. The following allows compilation to pass in Web mode by never bundling the module in the final output. - */ - new webpack.IgnorePlugin({ - resourceRegExp: /httpResourceFetcher/, // matches the path in the require() statement - }), - /** - * The following solves issues w/ breakpoints being offset when debugging in Chrome. IDK WHY!!!! - * - * To sanity check, comment out the following, set a breakpoint in the toolkit activation function, then see how the breakpoints - * are not working as expected. - */ - new webpack.SourceMapDevToolPlugin({ - exclude: /\*\*\/node_modules\/\*\*/, - }), - ], - resolve: { - extensions: ['.ts', '.js'], - fallback: { - stream: require.resolve('stream-browserify'), - os: require.resolve('os-browserify/browser'), - path: require.resolve('path-browserify'), - assert: require.resolve('assert'), - fs: false, - crypto: require.resolve('crypto-browserify'), - 'fs-extra': false, - perf_hooks: false, // should be using globalThis.performance instead + /** @type WebpackConfig */ + const config = merge(baseConfig, { + name: 'webBase', + target: 'webworker', + devtool: isDevelopment ? 'inline-source-map' : undefined, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, // disable chunks by default since web extensions must be a single bundle + }), + // some modules are provided globally and dont require an explicit import, + // so we need to polyfill them using this method + new webpack.ProvidePlugin({ + process: require.resolve('process/browser'), + Buffer: ['buffer', 'Buffer'], + }), + new webpack.EnvironmentPlugin({ + NODE_DEBUG: 'development', + READABLE_STREAM: 'disable', + }), + /** + * HACK: the HttpResourceFetcher breaks Web mode if imported, BUT we still dynamically import this module for non web mode + * environments. The following allows compilation to pass in Web mode by never bundling the module in the final output. + */ + new webpack.IgnorePlugin({ + resourceRegExp: /httpResourceFetcher/, // matches the path in the require() statement + }), + ], + resolve: { + extensions: ['.ts', '.js'], + fallback: { + stream: require.resolve('stream-browserify'), + os: require.resolve('os-browserify/browser'), + path: require.resolve('path-browserify'), + assert: require.resolve('assert'), + fs: false, + crypto: require.resolve('crypto-browserify'), + 'fs-extra': false, + perf_hooks: false, // should be using globalThis.performance instead - // *** If one of these modules actually gets used an error will be raised *** - // You may see something like: "TypeError: path_ignored_0.join is not a function" + // *** If one of these modules actually gets used an error will be raised *** + // You may see something like: "TypeError: path_ignored_0.join is not a function" - // We don't need these yet, but as we start enabling functionality in the web - // we may need to polyfill. - http: false, // http: require.resolve('stream-http'), - https: false, // https: require.resolve('https-browserify'), - zlib: false, // zlib: require.resolve('browserify-zlib'), - constants: false, //constants: require.resolve('constants-browserify'), - // These do not have a straight forward replacement - child_process: false, // Reason for error: 'TypeError: The "original" argument must be of type Function' - async_hooks: false, + // We don't need these yet, but as we start enabling functionality in the web + // we may need to polyfill. + http: false, // http: require.resolve('stream-http'), + https: false, // https: require.resolve('https-browserify'), + zlib: false, // zlib: require.resolve('browserify-zlib'), + constants: false, //constants: require.resolve('constants-browserify'), + // These do not have a straight forward replacement + child_process: false, // Reason for error: 'TypeError: The "original" argument must be of type Function' + async_hooks: false, + }, }, - }, - mode: 'production', // lets see if we can change this to 'development' later - optimization: { - // If `true` then we will get confusing variable names in the debugging menu - minimize: false, - }, -}) + optimization: { + // If `true` then we will get confusing variable names in the debugging menu + minimize: !isDevelopment, + }, + }) -module.exports = webConfig + return config +} From 130cb1b99b963213b3fd3af54fa6e275a249636b Mon Sep 17 00:00:00 2001 From: Joshua Pinkney Date: Fri, 12 Apr 2024 14:37:39 -0400 Subject: [PATCH 02/14] deps: Update Mynah UI to 4.5.5 Problem: - we are seeing multiple "Generating your answer..." cards in featureDev Solution: - update mynah ui to 4.5.5 --- package-lock.json | 8 ++++---- packages/core/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4393bbf51a0..64aecc75a4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3607,9 +3607,9 @@ } }, "node_modules/@aws/mynah-ui": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.5.4.tgz", - "integrity": "sha512-ZFJLLxybwzfBKhO5NKDzt++ntHbtf7bUqyAX//9pcvYVLHBHegGgqJcDECA1inYMUb2rybeVRIv7jpmjX7ks2g==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/@aws/mynah-ui/-/mynah-ui-4.5.5.tgz", + "integrity": "sha512-xvqr46XFfCWxcHsQURnDuHHs0GLJaCBanJpV22t9TBf/BNjzJN8aBM7AGYwXvZbykRKIGNpHnvQnKlMfVvr/aQ==", "hasInstallScript": true, "dependencies": { "just-clone": "^6.2.0", @@ -19601,7 +19601,7 @@ "@aws-sdk/property-provider": "3.46.0", "@aws-sdk/smithy-client": "^3.46.0", "@aws-sdk/util-arn-parser": "^3.46.0", - "@aws/mynah-ui": "4.5.4", + "@aws/mynah-ui": "^4.5.5", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/shared-ini-file-loader": "^2.2.8", diff --git a/packages/core/package.json b/packages/core/package.json index 2f92536b577..e3719715cd1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -4315,7 +4315,7 @@ "@aws-sdk/property-provider": "3.46.0", "@aws-sdk/smithy-client": "^3.46.0", "@aws-sdk/util-arn-parser": "^3.46.0", - "@aws/mynah-ui": "4.5.4", + "@aws/mynah-ui": "^4.5.5", "@gerhobbelt/gitignore-parser": "^0.2.0-9", "@iarna/toml": "^2.2.5", "@smithy/shared-ini-file-loader": "^2.2.8", From fc2482d06a48f8bfdb58554c3276a3af50100434 Mon Sep 17 00:00:00 2001 From: Nikolas Komonen <118216176+nkomonen-amazon@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:32:57 -0400 Subject: [PATCH 03/14] web: make shared crypto.randomUUID() function (#4693) * refactor: globalThis.crypto.randomUUID() Before, `crypto.randomUUID()` broke web mode since it is not available in the polyfill `crypto-browserify`. Solution: Use `globalThis.crypto.randomUUID()` as it works in both node + browser since each environment provides an implementation at runtime. In the long term we will want a better solution, but this commit quickly gets web mode working. Signed-off-by: Nikolas Komonen * use randomUUID in existing places Signed-off-by: Nikolas Komonen * add tests Signed-off-by: Nikolas Komonen * add documentation Signed-off-by: Nikolas Komonen --------- Signed-off-by: Nikolas Komonen --- .../amazonqFeatureDev/session/sessionState.ts | 2 +- .../telemetry/codeTransformTelemetryState.ts | 3 +- packages/core/src/auth/auth.ts | 2 +- .../controllers/chat/controller.ts | 2 +- packages/core/src/common/crypto.ts | 33 +++++++++++++++++++ packages/core/src/shared/telemetry/util.ts | 2 +- .../session/sessionState.test.ts | 2 +- packages/core/src/test/common/crypto.test.ts | 28 ++++++++++++++++ .../core/src/testWeb/common/crypto.test.ts | 28 ++++++++++++++++ 9 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/common/crypto.ts create mode 100644 packages/core/src/test/common/crypto.test.ts create mode 100644 packages/core/src/testWeb/common/crypto.test.ts diff --git a/packages/core/src/amazonqFeatureDev/session/sessionState.ts b/packages/core/src/amazonqFeatureDev/session/sessionState.ts index b712305e6aa..012f7f10200 100644 --- a/packages/core/src/amazonqFeatureDev/session/sessionState.ts +++ b/packages/core/src/amazonqFeatureDev/session/sessionState.ts @@ -4,7 +4,6 @@ */ import { MynahIcons } from '@aws/mynah-ui' -import { randomUUID } from 'crypto' import * as path from 'path' import * as vscode from 'vscode' import { ToolkitError } from '../../shared/errors' @@ -34,6 +33,7 @@ import { CodeReference } from '../../amazonq/webview/ui/connector' import { isPresent } from '../../shared/utilities/collectionUtils' import { encodeHTML } from '../../shared/utilities/textUtilities' import { AuthUtil } from '../../codewhisperer/util/authUtil' +import { randomUUID } from '../../common/crypto' export class ConversationNotStartedState implements Omit { public tokenSource: vscode.CancellationTokenSource diff --git a/packages/core/src/amazonqGumby/telemetry/codeTransformTelemetryState.ts b/packages/core/src/amazonqGumby/telemetry/codeTransformTelemetryState.ts index b81b3bee18f..4f94f7a4c6e 100644 --- a/packages/core/src/amazonqGumby/telemetry/codeTransformTelemetryState.ts +++ b/packages/core/src/amazonqGumby/telemetry/codeTransformTelemetryState.ts @@ -2,7 +2,8 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -import { randomUUID } from 'crypto' + +import { randomUUID } from '../../common/crypto' interface ICodeTransformerTelemetryState { sessionId: string diff --git a/packages/core/src/auth/auth.ts b/packages/core/src/auth/auth.ts index b3a38ec2b76..6c8f1a99236 100644 --- a/packages/core/src/auth/auth.ts +++ b/packages/core/src/auth/auth.ts @@ -10,7 +10,6 @@ const localize = nls.loadMessageBundle() import * as vscode from 'vscode' import * as localizedText from '../shared/localizedText' -import { randomUUID } from 'crypto' import { Credentials } from '@aws-sdk/types' import { SsoAccessTokenProvider } from './sso/ssoAccessTokenProvider' import { Timeout } from '../shared/utilities/timeoutUtils' @@ -61,6 +60,7 @@ import { } from './connection' import { isSageMaker, isCloud9 } from '../shared/extensionUtilities' import { telemetry } from '../shared/telemetry/telemetry' +import { randomUUID } from '../common/crypto' interface AuthService { /** diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 126ef4e133d..6e43e735d8e 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -33,7 +33,6 @@ import { EditorContentController } from '../../../amazonq/commons/controllers/co import { EditorContextCommand } from '../../commands/registerCommands' import { PromptsGenerator } from './prompts/promptsGenerator' import { TriggerEventsStorage } from '../../storages/triggerEvents' -import { randomUUID } from 'crypto' import { CodeWhispererStreamingServiceException, GenerateAssistantResponseCommandOutput, @@ -46,6 +45,7 @@ import { triggerPayloadToChatRequest } from './chatRequest/converter' import { OnboardingPageInteraction } from '../../../amazonq/onboardingPage/model' import { getChatAuthState } from '../../../codewhisperer/util/authUtil' import { openUrl } from '../../../shared/utilities/vsCodeUtils' +import { randomUUID } from '../../../common/crypto' export interface ChatControllerMessagePublishers { readonly processPromptChatMessage: MessagePublisher diff --git a/packages/core/src/common/crypto.ts b/packages/core/src/common/crypto.ts new file mode 100644 index 00000000000..fc89890ef00 --- /dev/null +++ b/packages/core/src/common/crypto.ts @@ -0,0 +1,33 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Why do we need this? + * + * Depending on environment (node vs web), `crypto` is different. + * Node requires `crypto` to be imported, while in web it is available globally + * through just calling `crypto` or `globalThis.crypto`. + * + * The crypto signatures and functions are not 1:1 between the environments. + * So this module provides environment agnostic functions for `crypto`. + * + * --- + * + * Node `crypto` has `crypto.webcrypto` except the interface is more cumbersome to use + * compared to node `crypto`. So we will want to eventually exclusively use functions + * in this class instead of the `crypto` functions. + * + * Once we do not need `crypto` anymore, we can get rid of the polyfill. + */ + +import { isWeb } from './webUtils' + +export function randomUUID(): `${string}-${string}-${string}-${string}-${string}` { + if (isWeb()) { + return globalThis.crypto.randomUUID() + } + + return require('crypto').randomUUID() +} diff --git a/packages/core/src/shared/telemetry/util.ts b/packages/core/src/shared/telemetry/util.ts index f4bcd5303ca..78341574efb 100644 --- a/packages/core/src/shared/telemetry/util.ts +++ b/packages/core/src/shared/telemetry/util.ts @@ -7,7 +7,6 @@ import { env, Memento, version } from 'vscode' import { getLogger } from '../logger' import { fromExtensionManifest } from '../settings' import { shared } from '../utilities/functionUtils' -import { randomUUID } from 'crypto' import { isInDevEnv, extensionVersion, isAutomation } from '../vscode/env' import { addTypeName } from '../utilities/typeConstructors' import globals from '../extensionGlobals' @@ -16,6 +15,7 @@ import { Result } from './telemetry.gen' import { MetricDatum } from './clienttelemetry' import { isValidationExemptMetric } from './exemptMetrics' import { isCloud9, isSageMaker } from '../../shared/extensionUtilities' +import { randomUUID } from '../../common/crypto' const legacySettingsTelemetryValueDisable = 'Disable' const legacySettingsTelemetryValueEnable = 'Enable' diff --git a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts index 30551a84e0e..d006388c4ab 100644 --- a/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts @@ -21,7 +21,7 @@ import { MessagePublisher } from '../../../amazonq/messages/messagePublisher' import { FeatureDevClient } from '../../../amazonqFeatureDev/client/featureDev' import { ToolkitError } from '../../../shared/errors' import { PrepareRepoFailedError } from '../../../amazonqFeatureDev/errors' -import crypto from 'crypto' +import * as crypto from '../../../common/crypto' import { TelemetryHelper } from '../../../amazonqFeatureDev/util/telemetryHelper' import { assertTelemetry, createTestWorkspaceFolder } from '../../testUtil' import { getFetchStubWithResponse } from '../../common/request.test' diff --git a/packages/core/src/test/common/crypto.test.ts b/packages/core/src/test/common/crypto.test.ts new file mode 100644 index 00000000000..f6963a8d2b0 --- /dev/null +++ b/packages/core/src/test/common/crypto.test.ts @@ -0,0 +1,28 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { randomUUID } from '../../common/crypto' + +describe('crypto', function () { + describe('randomUUID()', function () { + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + + it('functions as normal', async function () { + const result1 = randomUUID() + const result2 = randomUUID() + const result3 = randomUUID() + assert(uuidPattern.test(result1)) + assert(uuidPattern.test(result2)) + assert(uuidPattern.test(result3)) + assert(result1 !== result2) + assert(result2 !== result3) + }) + + it('test pattern fails on non-uuid', function () { + assert(uuidPattern.test('not-a-uuid') === false) + }) + }) +}) diff --git a/packages/core/src/testWeb/common/crypto.test.ts b/packages/core/src/testWeb/common/crypto.test.ts new file mode 100644 index 00000000000..f6963a8d2b0 --- /dev/null +++ b/packages/core/src/testWeb/common/crypto.test.ts @@ -0,0 +1,28 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import { randomUUID } from '../../common/crypto' + +describe('crypto', function () { + describe('randomUUID()', function () { + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + + it('functions as normal', async function () { + const result1 = randomUUID() + const result2 = randomUUID() + const result3 = randomUUID() + assert(uuidPattern.test(result1)) + assert(uuidPattern.test(result2)) + assert(uuidPattern.test(result3)) + assert(result1 !== result2) + assert(result2 !== result3) + }) + + it('test pattern fails on non-uuid', function () { + assert(uuidPattern.test('not-a-uuid') === false) + }) + }) +}) From 4dda74f9ddc17e5ffc31cc8054417638ab3dad0e Mon Sep 17 00:00:00 2001 From: Dogus Atasoy <116281103+dogusata@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:12:25 +0200 Subject: [PATCH 04/14] fix(amazonq): Quick Action commands in new tabs are coming disabled (#4707) * fixed quick actions in new tabs are coming disabled --- .../Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json | 4 ++++ .../core/src/amazonq/webview/ui/quickActions/generator.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json diff --git a/.changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json b/.changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json new file mode 100644 index 00000000000..915346e14f8 --- /dev/null +++ b/.changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q: Fixed newly opening tabs have all quick actions disabled." +} diff --git a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts index 3fad0b0b4be..ce16d81558b 100644 --- a/packages/core/src/amazonq/webview/ui/quickActions/generator.ts +++ b/packages/core/src/amazonq/webview/ui/quickActions/generator.ts @@ -83,8 +83,8 @@ export class QuickActionGenerator { unavailableItems: ['/dev', '/transform'], }, unknown: { - description: "This command isn't available", - unavailableItems: ['/dev', '/transform', '/help', '/clear'], + description: '', + unavailableItems: [], }, } From 5f9710725a215bc7f7384258b36587592392de61 Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Mon, 15 Apr 2024 17:40:28 +0000 Subject: [PATCH 05/14] Release 2.19.0 --- .changes/2.19.0.json | 30 +++++++++++++++++++ ...-29f91f61-c02b-4010-81e4-91f29541a628.json | 4 --- ...-a1567f38-b9c9-460b-94ea-66ff50ea9f73.json | 4 --- ...-ba11248f-2ea2-412d-a06e-7aaded483d8f.json | 4 --- ...-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json | 4 --- ...-a5f37aec-e586-4057-9834-7d58c55c5695.json | 4 --- ...-b9e8098d-e1a6-46e8-b13e-137b3ac6f95e.json | 4 --- CHANGELOG.md | 9 ++++++ package-lock.json | 2 +- packages/toolkit/package.json | 2 +- 10 files changed, 41 insertions(+), 26 deletions(-) create mode 100644 .changes/2.19.0.json delete mode 100644 .changes/next-release/Bug Fix-29f91f61-c02b-4010-81e4-91f29541a628.json delete mode 100644 .changes/next-release/Bug Fix-a1567f38-b9c9-460b-94ea-66ff50ea9f73.json delete mode 100644 .changes/next-release/Bug Fix-ba11248f-2ea2-412d-a06e-7aaded483d8f.json delete mode 100644 .changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json delete mode 100644 .changes/next-release/Feature-a5f37aec-e586-4057-9834-7d58c55c5695.json delete mode 100644 .changes/next-release/Feature-b9e8098d-e1a6-46e8-b13e-137b3ac6f95e.json diff --git a/.changes/2.19.0.json b/.changes/2.19.0.json new file mode 100644 index 00000000000..9858bc0543e --- /dev/null +++ b/.changes/2.19.0.json @@ -0,0 +1,30 @@ +{ + "date": "2024-04-15", + "version": "2.19.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Amazon Q: Fixed quick action command list inconsistency between Q tabs" + }, + { + "type": "Bug Fix", + "description": "Amazon Q Code Transform: Handle whitespaces in manually entered JAVA_HOME" + }, + { + "type": "Bug Fix", + "description": "Amazon Q: Fixed cursor not focuses to prompt input when code is sent to Q Chat" + }, + { + "type": "Bug Fix", + "description": "Amazon Q: Fixed newly opening tabs have all quick actions disabled." + }, + { + "type": "Feature", + "description": "Support extended CodeWhisperer session durations when using CodeCatalyst on the same SSO connection" + }, + { + "type": "Feature", + "description": "Amazon Q Feature Dev: Use project context from workspace root directory instead of the /src folder" + } + ] +} \ No newline at end of file diff --git a/.changes/next-release/Bug Fix-29f91f61-c02b-4010-81e4-91f29541a628.json b/.changes/next-release/Bug Fix-29f91f61-c02b-4010-81e4-91f29541a628.json deleted file mode 100644 index faf723b864e..00000000000 --- a/.changes/next-release/Bug Fix-29f91f61-c02b-4010-81e4-91f29541a628.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q: Fixed quick action command list inconsistency between Q tabs" -} diff --git a/.changes/next-release/Bug Fix-a1567f38-b9c9-460b-94ea-66ff50ea9f73.json b/.changes/next-release/Bug Fix-a1567f38-b9c9-460b-94ea-66ff50ea9f73.json deleted file mode 100644 index 04d2fbd8a4c..00000000000 --- a/.changes/next-release/Bug Fix-a1567f38-b9c9-460b-94ea-66ff50ea9f73.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q Code Transform: Handle whitespaces in manually entered JAVA_HOME" -} diff --git a/.changes/next-release/Bug Fix-ba11248f-2ea2-412d-a06e-7aaded483d8f.json b/.changes/next-release/Bug Fix-ba11248f-2ea2-412d-a06e-7aaded483d8f.json deleted file mode 100644 index a0c1c05b9ae..00000000000 --- a/.changes/next-release/Bug Fix-ba11248f-2ea2-412d-a06e-7aaded483d8f.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q: Fixed cursor not focuses to prompt input when code is sent to Q Chat" -} diff --git a/.changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json b/.changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json deleted file mode 100644 index 915346e14f8..00000000000 --- a/.changes/next-release/Bug Fix-f5a0ad13-60ce-4ee4-9da9-0687d0961466.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Amazon Q: Fixed newly opening tabs have all quick actions disabled." -} diff --git a/.changes/next-release/Feature-a5f37aec-e586-4057-9834-7d58c55c5695.json b/.changes/next-release/Feature-a5f37aec-e586-4057-9834-7d58c55c5695.json deleted file mode 100644 index 87336a7b7e1..00000000000 --- a/.changes/next-release/Feature-a5f37aec-e586-4057-9834-7d58c55c5695.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Support extended CodeWhisperer session durations when using CodeCatalyst on the same SSO connection" -} diff --git a/.changes/next-release/Feature-b9e8098d-e1a6-46e8-b13e-137b3ac6f95e.json b/.changes/next-release/Feature-b9e8098d-e1a6-46e8-b13e-137b3ac6f95e.json deleted file mode 100644 index 1037d5d0f87..00000000000 --- a/.changes/next-release/Feature-b9e8098d-e1a6-46e8-b13e-137b3ac6f95e.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Feature", - "description": "Amazon Q Feature Dev: Use project context from workspace root directory instead of the /src folder" -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 014d514c8d8..cca2861a14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.19.0 2024-04-15 + +- **Bug Fix** Amazon Q: Fixed quick action command list inconsistency between Q tabs +- **Bug Fix** Amazon Q Code Transform: Handle whitespaces in manually entered JAVA_HOME +- **Bug Fix** Amazon Q: Fixed cursor not focuses to prompt input when code is sent to Q Chat +- **Bug Fix** Amazon Q: Fixed newly opening tabs have all quick actions disabled. +- **Feature** Support extended CodeWhisperer session durations when using CodeCatalyst on the same SSO connection +- **Feature** Amazon Q Feature Dev: Use project context from workspace root directory instead of the /src folder + ## 2.18.0 2024-04-04 - **Bug Fix** Amazon Q Feature Dev: Fix followups after hitting iteration limit diff --git a/package-lock.json b/package-lock.json index 64aecc75a4b..e0098641078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19732,7 +19732,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "2.19.0-SNAPSHOT", + "version": "2.19.0", "engines": "This field will be autopopulated from the core module during debugging and packaging.", "license": "Apache-2.0", "dependencies": { diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index a17996dc58f..730a841488c 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit - Amazon Q, CodeWhisperer, and more", "description": "Including Amazon Q, CodeWhisperer, CodeCatalyst, Application Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services", - "version": "2.19.0-SNAPSHOT", + "version": "2.19.0", "extensionKind": [ "workspace" ], From 63c6005974eb549b917c065d065ba060f7e759ee Mon Sep 17 00:00:00 2001 From: aws-toolkit-automation <> Date: Mon, 15 Apr 2024 18:44:55 +0000 Subject: [PATCH 06/14] Update version to snapshot version: 2.20.0-SNAPSHOT --- package-lock.json | 2 +- packages/toolkit/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0098641078..ded55fda7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19732,7 +19732,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "2.19.0", + "version": "2.20.0-SNAPSHOT", "engines": "This field will be autopopulated from the core module during debugging and packaging.", "license": "Apache-2.0", "dependencies": { diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 730a841488c..ef17806a550 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit - Amazon Q, CodeWhisperer, and more", "description": "Including Amazon Q, CodeWhisperer, CodeCatalyst, Application Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services", - "version": "2.19.0", + "version": "2.20.0-SNAPSHOT", "extensionKind": [ "workspace" ], From 975f8d3652590a1cbe3049cdb758c3de40c3d8db Mon Sep 17 00:00:00 2001 From: Santiago Martin <143631912+sannicm@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:36:38 +0200 Subject: [PATCH 07/14] fix(amazonFeatureDev): accepted files telemetry #4689 Problem File acceptance telemetry was not counting deleted files, i.e. the files that code generation marked to be deleted, even if they were rejected or not. Solution Count deleted files for the metric and add tests to assure this. --- .../controllers/chat/controller.ts | 8 +- .../controllers/chat/controller.test.ts | 132 +++++++++++++++--- 2 files changed, 121 insertions(+), 19 deletions(-) diff --git a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts index 06810145242..526a4fb498b 100644 --- a/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts +++ b/packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts @@ -449,9 +449,15 @@ export class FeatureDevController { let session try { session = await this.sessionStorage.getSession(message.tabID) + + const acceptedFiles = (paths?: { rejected: boolean }[]) => (paths || []).filter(i => !i.rejected).length + + const amazonqNumberOfFilesAccepted = + acceptedFiles(session.state.filePaths) + acceptedFiles(session.state.deletedFiles) + telemetry.amazonq_isAcceptedCodeChanges.emit({ amazonqConversationId: session.conversationId, - amazonqNumberOfFilesAccepted: session.state.filePaths?.filter(i => !i.rejected).length ?? -1, + amazonqNumberOfFilesAccepted, enabled: true, result: 'Succeeded', }) diff --git a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts index 8c929ec6798..f2e5c22d4ab 100644 --- a/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts +++ b/packages/core/src/test/amazonqFeatureDev/controllers/chat/controller.test.ts @@ -8,13 +8,23 @@ import * as assert from 'assert' import * as path from 'path' import sinon from 'sinon' import { waitUntil } from '../../../../shared/utilities/timeoutUtils' -import { ControllerSetup, createController, createSession } from '../../utils' -import { CurrentWsFolders, FollowUpTypes, createUri } from '../../../../amazonqFeatureDev/types' +import { ControllerSetup, createController, createSession, generateVirtualMemoryUri } from '../../utils' +import { + CurrentWsFolders, + FollowUpTypes, + createUri, + NewFileInfo, + DeletedFileInfo, +} from '../../../../amazonqFeatureDev/types' import { Session } from '../../../../amazonqFeatureDev/session/session' import { Prompter } from '../../../../shared/ui/prompter' import { assertTelemetry, toFile } from '../../../testUtil' import { SelectedFolderNotInWorkspaceFolderError } from '../../../../amazonqFeatureDev/errors' -import { CodeGenState, PrepareRefinementState } from '../../../../amazonqFeatureDev/session/sessionState' +import { + CodeGenState, + PrepareCodeGenState, + PrepareRefinementState, +} from '../../../../amazonqFeatureDev/session/sessionState' import { FeatureDevClient } from '../../../../amazonqFeatureDev/client/featureDev' let mockGetCodeGeneration: sinon.SinonStub @@ -26,6 +36,40 @@ describe('Controller', () => { let session: Session let controllerSetup: ControllerSetup + const getFilePaths = (controllerSetup: ControllerSetup): NewFileInfo[] => [ + { + zipFilePath: 'myfile1.js', + relativePath: 'myfile1.js', + fileContent: '', + rejected: false, + virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'myfile1.js'), + workspaceFolder: controllerSetup.workspaceFolder, + }, + { + zipFilePath: 'myfile2.js', + relativePath: 'myfile2.js', + fileContent: '', + rejected: true, + virtualMemoryUri: generateVirtualMemoryUri(uploadID, 'myfile2.js'), + workspaceFolder: controllerSetup.workspaceFolder, + }, + ] + + const getDeletedFiles = (): DeletedFileInfo[] => [ + { + zipFilePath: 'myfile3.js', + relativePath: 'myfile3.js', + rejected: false, + workspaceFolder: controllerSetup.workspaceFolder, + }, + { + zipFilePath: 'myfile4.js', + relativePath: 'myfile4.js', + rejected: true, + workspaceFolder: controllerSetup.workspaceFolder, + }, + ] + before(() => { sinon.stub(performance, 'now').returns(0) }) @@ -245,7 +289,6 @@ describe('Controller', () => { }) describe('fileClicked', () => { - const filePath = 'myfile.js' async function createCodeGenState() { mockGetCodeGeneration = sinon.stub().resolves({ codeGenerationStatus: { status: 'Complete' } }) @@ -269,16 +312,7 @@ describe('Controller', () => { const codeGenState = new CodeGenState( testConfig, testApproach, - [ - { - zipFilePath: 'myfile.js', - relativePath: 'myfile.js', - fileContent: '', - rejected: false, - virtualMemoryUri: '' as unknown as vscode.Uri, - workspaceFolder: controllerSetup.workspaceFolder, - }, - ], + getFilePaths(controllerSetup), [], [], tabID, @@ -293,7 +327,12 @@ describe('Controller', () => { }) return newSession } - async function fileClicked(getSessionStub: sinon.SinonStub<[tabID: string], Promise>, action: string) { + + async function fileClicked( + getSessionStub: sinon.SinonStub<[tabID: string], Promise>, + action: string, + filePath: string + ) { controllerSetup.emitters.fileClicked.fire({ tabID, conversationID, @@ -307,20 +346,77 @@ describe('Controller', () => { }, {}) return getSessionStub.getCall(0).returnValue } + it('clicking the "Reject File" button updates the file state to "rejected: true"', async () => { + const filePath = getFilePaths(controllerSetup)[0].zipFilePath const session = await createCodeGenState() const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - const rejectFile = await fileClicked(getSessionStub, 'reject-change') + const rejectFile = await fileClicked(getSessionStub, 'reject-change', filePath) assert.strictEqual(rejectFile.state.filePaths?.find(i => i.relativePath === filePath)?.rejected, true) }) + it('clicking the "Reject File" button and then "Revert Reject File", updates the file state to "rejected: false"', async () => { + const filePath = getFilePaths(controllerSetup)[0].zipFilePath const session = await createCodeGenState() const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(session) - await fileClicked(getSessionStub, 'reject-change') - const revertRejection = await fileClicked(getSessionStub, 'revert-rejection') + await fileClicked(getSessionStub, 'reject-change', filePath) + const revertRejection = await fileClicked(getSessionStub, 'revert-rejection', filePath) assert.strictEqual(revertRejection.state.filePaths?.find(i => i.relativePath === filePath)?.rejected, false) }) }) + + describe('insertCode', () => { + it('sets the number of files accepted counting also deleted files', async () => { + async function insertCode() { + const initialState = new PrepareCodeGenState( + { + conversationId: conversationID, + proxyClient: new FeatureDevClient(), + workspaceRoots: [''], + workspaceFolders: [controllerSetup.workspaceFolder], + uploadId: uploadID, + }, + '', + getFilePaths(controllerSetup), + getDeletedFiles(), + [], + tabID, + 0 + ) + + const newSession = await createSession({ + messenger: controllerSetup.messenger, + sessionState: initialState, + conversationID, + tabID, + uploadID, + }) + const getSessionStub = sinon.stub(controllerSetup.sessionStorage, 'getSession').resolves(newSession) + + controllerSetup.emitters.followUpClicked.fire({ + tabID, + conversationID, + followUp: { + type: FollowUpTypes.InsertCode, + }, + }) + + // Wait until the controller has time to process the event + await waitUntil(() => { + return Promise.resolve(getSessionStub.callCount > 0) + }, {}) + } + + await insertCode() + + assertTelemetry('amazonq_isAcceptedCodeChanges', { + amazonqConversationId: conversationID, + amazonqNumberOfFilesAccepted: 2, + enabled: true, + result: 'Succeeded', + }) + }) + }) }) From 5ceeaba103717db87d4d5ff88ac8c7c8bc62e55a Mon Sep 17 00:00:00 2001 From: Leonardo Araneda Freccero Date: Wed, 17 Apr 2024 17:44:29 +0200 Subject: [PATCH 08/14] fix(amazonq transform): failure if whitespace in JAVA_HOME #4725 Problem: Users can provide a JAVA_HOME with trailing or leading whitespaces, this causes issues when resolving the JAVA_HOME. Solution: Using `.trim()` to do preliminary cleanup of the string. --- packages/core/src/amazonqGumby/chat/controller/controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index fd0fa42e1f2..ae82b3ac77f 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -346,6 +346,7 @@ export class GumbyController { * Examples: * ``` * extractPath("./some/path/here") => "C:/some/root/some/path/here" + * extractPath(" ./some/path/here\n") => "C:/some/root/some/path/here" * extractPath("C:/some/nonexistent/path/here") => undefined * extractPath("C:/some/filepath/.txt") => undefined * ``` @@ -354,6 +355,6 @@ export class GumbyController { * @returns the absolute path if path points to existing folder, otherwise undefined */ function extractPath(text: string): string | undefined { - const resolvedPath = path.resolve(text) + const resolvedPath = path.resolve(text.trim()) return fs.existsSync(resolvedPath) && fs.lstatSync(resolvedPath).isDirectory() ? resolvedPath : undefined } From b7dc20344d64326cd9098d138a83a9756cf348aa Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Wed, 17 Apr 2024 08:46:28 -0700 Subject: [PATCH 09/14] changelog --- .../Bug Fix-1be65539-66a5-4443-ae5e-2a93a67fe436.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changes/next-release/Bug Fix-1be65539-66a5-4443-ae5e-2a93a67fe436.json diff --git a/.changes/next-release/Bug Fix-1be65539-66a5-4443-ae5e-2a93a67fe436.json b/.changes/next-release/Bug Fix-1be65539-66a5-4443-ae5e-2a93a67fe436.json new file mode 100644 index 00000000000..24bb3fe80fa --- /dev/null +++ b/.changes/next-release/Bug Fix-1be65539-66a5-4443-ae5e-2a93a67fe436.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q CodeTransform may fail if JAVA_HOME has leading or trailing whitespace" +} From 4069db1d4657d54be7778405ee8fef6a23f09e89 Mon Sep 17 00:00:00 2001 From: David Hasani <60020664+dhasani23@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:23:46 -0700 Subject: [PATCH 10/14] Remove certain maven metadata from uploaded dependencies. (#4704) * Remove certain maven metadata from uploaded dependencies. * Add test ensuring correct dependencies added to Q Code Transform upload zip. * Address comments and fix ci. * Add changelog. --------- Co-authored-by: Leonardo Araneda Freccero --- ...-ffcf9c8e-bd5e-4b34-a418-469717d913f6.json | 4 ++ .../transformByQ/transformApiHandler.ts | 24 +++++++ .../commands/transformByQ.test.ts | 63 ++++++++++++++++++- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 .changes/next-release/Bug Fix-ffcf9c8e-bd5e-4b34-a418-469717d913f6.json diff --git a/.changes/next-release/Bug Fix-ffcf9c8e-bd5e-4b34-a418-469717d913f6.json b/.changes/next-release/Bug Fix-ffcf9c8e-bd5e-4b34-a418-469717d913f6.json new file mode 100644 index 00000000000..f5b19414cdd --- /dev/null +++ b/.changes/next-release/Bug Fix-ffcf9c8e-bd5e-4b34-a418-469717d913f6.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q Code Transformation - Omit Maven metadata files when uploading dependencies to fix certain build failures in backend." +} diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index ae2fc3a2922..ec2855f2b6d 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -195,6 +195,27 @@ export async function uploadPayload(payloadFileName: string) { return response.uploadId } +/** + * Array of file extensions used by Maven as metadata in the local repository. + * Files with these extensions influence Maven's behavior during compile time, + * particularly in checking the availability of source repositories and potentially + * re-downloading dependencies if the source is not accessible. Removing these + * files can prevent Maven from attempting to download dependencies again. + */ +const MavenExcludedExtensions = ['.repositories', '.sha1'] + +/** + * Determines if the specified file path corresponds to a Maven metadata file + * by checking against known metadata file extensions. This is used to identify + * files that might trigger Maven to recheck or redownload dependencies from source repositories. + * + * @param path The file path to evaluate for exclusion based on its extension. + * @returns {boolean} Returns true if the path ends with an extension associated with Maven metadata files; otherwise, false. + */ +function isExcludedDependencyFile(path: string): boolean { + return MavenExcludedExtensions.some(extension => path.endsWith(extension)) +} + /** * Gets all files in dir. We use this method to get the source code, then we run a mvn command to * copy over dependencies into their own folder, then we use this method again to get those @@ -252,6 +273,9 @@ export async function zipCode(dependenciesFolder: FolderInfo) { if (dependencyFiles.length > 0) { for (const file of dependencyFiles) { + if (isExcludedDependencyFile(file)) { + continue + } const relativePath = path.relative(dependenciesFolder.path, file) const paddedPath = path.join(`dependencies/${dependenciesFolder.name}`, relativePath) zip.addLocalFile(file, path.dirname(paddedPath)) diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index 87bb1483c7b..60af299025f 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -5,13 +5,16 @@ import assert from 'assert' import * as vscode from 'vscode' +import * as fs from 'fs-extra' import * as sinon from 'sinon' +import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities' import * as model from '../../../codewhisperer/models/model' import * as startTransformByQ from '../../../codewhisperer/commands/startTransformByQ' import { HttpResponse } from 'aws-sdk' import * as codeWhisperer from '../../../codewhisperer/client/codewhisperer' import * as CodeWhispererConstants from '../../../codewhisperer/models/constants' import { getTestWindow } from '../../shared/vscode/window' +import AdmZip from 'adm-zip' import { stopTransformByQMessage } from '../../../codewhisperer/models/constants' import { convertToTimeString, convertDateToTimestamp } from '../../../shared/utilities/textUtilities' import path from 'path' @@ -26,6 +29,7 @@ import { pollTransformationJob, getHeadersObj, throwIfCancelled, + zipCode, } from '../../../codewhisperer/service/transformByQ/transformApiHandler' import { validateOpenProjects, @@ -34,8 +38,15 @@ import { import { TransformationCandidateProject } from '../../../codewhisperer/models/model' describe('transformByQ', function () { - afterEach(function () { + let tempDir: string + + beforeEach(async function () { + tempDir = await makeTemporaryToolkitFolder() + }) + + afterEach(async function () { sinon.restore() + await fs.remove(tempDir) }) it('WHEN converting short duration in milliseconds THEN converts correctly', async function () { @@ -199,4 +210,54 @@ describe('transformByQ', function () { } assert.deepStrictEqual(actual, expected) }) + + it(`WHEN zip created THEN dependencies contains no .sha1 or .repositories files`, async function () { + const m2Folders = [ + 'com/groupid1/artifactid1/version1', + 'com/groupid1/artifactid1/version2', + 'com/groupid1/artifactid2/version1', + 'com/groupid2/artifactid1/version1', + 'com/groupid2/artifactid1/version2', + ] + // List of files that exist in m2 artifact directory + const filesToAdd = [ + '_remote.repositories', + 'test-0.0.1-20240315.145420-18.pom', + 'test-0.0.1-20240315.145420-18.pom.sha1', + 'test-0.0.1-SNAPSHOT.pom', + 'maven-metadata-test-repo.xml', + 'maven-metadata-test-repo.xml.sha1', + 'resolver-status.properties', + ] + const expectedFilesAfterClean = [ + 'test-0.0.1-20240315.145420-18.pom', + 'test-0.0.1-SNAPSHOT.pom', + 'maven-metadata-test-repo.xml', + 'resolver-status.properties', + ] + + m2Folders.forEach(folder => { + const folderPath = path.join(tempDir, folder) + fs.mkdirSync(folderPath, { recursive: true }) + filesToAdd.forEach(file => { + fs.writeFileSync(path.join(folderPath, file), 'sample content for the test file') + }) + }) + + const tempFileName = `testfile-${Date.now()}.zip` + model.transformByQState.setProjectPath(tempDir) + return zipCode({ + path: tempDir, + name: tempFileName, + }).then(zipFile => { + const zip = new AdmZip(zipFile) + const dependenciesToUpload = zip.getEntries().filter(entry => entry.entryName.startsWith('dependencies')) + // Each dependency version folder contains each expected file, thus we multiply + const expectedNumberOfDependencyFiles = m2Folders.length * expectedFilesAfterClean.length + assert.strictEqual(expectedNumberOfDependencyFiles, dependenciesToUpload.length) + dependenciesToUpload.forEach(dependency => { + assert(expectedFilesAfterClean.includes(dependency.name)) + }) + }) + }) }) From 21b96fa9d6c0e01fae5cdc546092f2d80f86af71 Mon Sep 17 00:00:00 2001 From: aemada-aws <157400220+aemada-aws@users.noreply.github.com> Date: Wed, 17 Apr 2024 22:52:53 +0200 Subject: [PATCH 11/14] feat(amazonq): enable builder id auth for amazon q (#4608) * feat: enable builder id auth for amazon q * address comment to update if condition * Fix verification of amazonQ connections and adding correct scopes --------- Co-authored-by: David Hasani <60020664+dhasani23@users.noreply.github.com> Co-authored-by: Joao Salles --- ...-51070f02-21b0-447b-9743-e7ce616337fe.json | 4 ++ .../src/amazonq/explorer/amazonQTreeNode.ts | 12 ++++- .../src/codewhisperer/models/constants.ts | 3 -- .../core/src/codewhisperer/util/authUtil.ts | 44 ++++++------------- .../test/codewhisperer/util/authUtil.test.ts | 18 ++++---- .../src/testE2E/util/codewhispererUtil.ts | 4 +- 6 files changed, 39 insertions(+), 46 deletions(-) create mode 100644 .changes/next-release/Feature-51070f02-21b0-447b-9743-e7ce616337fe.json diff --git a/.changes/next-release/Feature-51070f02-21b0-447b-9743-e7ce616337fe.json b/.changes/next-release/Feature-51070f02-21b0-447b-9743-e7ce616337fe.json new file mode 100644 index 00000000000..3bab41b73c7 --- /dev/null +++ b/.changes/next-release/Feature-51070f02-21b0-447b-9743-e7ce616337fe.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "Enable Amazon Q feature development and Amazon Q transform capabilities (/dev and /transform) for AWS Builder ID users." +} diff --git a/packages/core/src/amazonq/explorer/amazonQTreeNode.ts b/packages/core/src/amazonq/explorer/amazonQTreeNode.ts index f7d6d8e6dfc..efc008bb063 100644 --- a/packages/core/src/amazonq/explorer/amazonQTreeNode.ts +++ b/packages/core/src/amazonq/explorer/amazonQTreeNode.ts @@ -9,7 +9,7 @@ import { ResourceTreeDataProvider, TreeNode } from '../../shared/treeview/resour import { AuthUtil, amazonQScopes, codeWhispererChatScopes } from '../../codewhisperer/util/authUtil' import { createLearnMoreNode, enableAmazonQNode, switchToAmazonQNode } from './amazonQChildrenNodes' import { Command, Commands } from '../../shared/vscode/commands2' -import { hasScopes, isSsoConnection } from '../../auth/connection' +import { hasScopes, isBuilderIdConnection, isSsoConnection } from '../../auth/connection' import { listCodeWhispererCommands } from '../../codewhisperer/ui/statusBarMenu' import { getIcon } from '../../shared/icons' import { vsCodeState } from '../../codewhisperer/models/model' @@ -77,6 +77,16 @@ export class AmazonQNode implements TreeNode { } } + if (isBuilderIdConnection(AuthUtil.instance.conn)) { + const missingScopes = + (AuthUtil.instance.isBuilderIdInUse() && !hasScopes(AuthUtil.instance.conn, amazonQScopes)) || + !hasScopes(AuthUtil.instance.conn, codeWhispererChatScopes) + + if (missingScopes) { + return [enableAmazonQNode(), createLearnMoreNode()] + } + } + return [ vsCodeState.isFreeTierLimitReached ? createFreeTierLimitMet('tree') : switchToAmazonQNode('tree'), createNewMenuButton(), diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index 0db6819d655..abe65ca6e6b 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -310,9 +310,6 @@ export const transformByQPartiallyCompletedMessage = export const noPomXmlFoundMessage = 'None of your open Java projects are supported by Amazon Q Code Transformation. Currently, Amazon Q can only upgrade Java projects built on Maven. A pom.xml must be present in the root of your project to upgrade it. For more information, see the [Amazon Q documentation](LINK_HERE).' -export const noActiveIdCMessage = - 'Amazon Q Code Transformation requires an active IAM Identity Center connection. For more information, see the [Code Transformation documentation](LINK_HERE).' - export const noOngoingJobMessage = 'No job is in-progress at the moment' export const jobInProgressMessage = 'Job is already in-progress' diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index 4f349ae3a4b..c533d8a269f 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -57,20 +57,15 @@ export const isValidCodeWhispererCoreConnection = (conn?: Connection): conn is C (isSsoConnection(conn) && hasScopes(conn, codeWhispererCoreScopes)) ) } -/** For Builder ID only, if using IdC then use {@link isValidAmazonQConnection} */ -export const isValidCodeWhispererChatConnection = (conn?: Connection): conn is Connection => { +/** Superset that includes all of CodeWhisperer + Amazon Q */ +export const isValidAmazonQConnection = (conn?: Connection): conn is Connection => { return ( - isBuilderIdConnection(conn) && + (isSsoConnection(conn) || isBuilderIdConnection(conn)) && isValidCodeWhispererCoreConnection(conn) && - hasScopes(conn, codeWhispererChatScopes) + hasScopes(conn, amazonQScopes) ) } -/** Superset that includes all of CodeWhisperer + Amazon Q */ -export const isValidAmazonQConnection = (conn?: Connection): conn is Connection => { - return isSsoConnection(conn) && isValidCodeWhispererCoreConnection(conn) && hasScopes(conn, amazonQScopes) -} - interface HasAlreadySeenQWelcome { local?: boolean devEnv?: boolean @@ -217,9 +212,9 @@ export class AuthUtil { let conn = (await this.auth.listConnections()).find(isBuilderIdConnection) if (!conn) { - conn = await this.auth.createConnection(createBuilderIdProfile(codeWhispererChatScopes)) - } else if (!isValidCodeWhispererChatConnection(conn)) { - conn = await this.secondaryAuth.addScopes(conn, codeWhispererChatScopes) + conn = await this.auth.createConnection(createBuilderIdProfile(amazonQScopes)) + } else if (!isValidAmazonQConnection(conn)) { + conn = await this.secondaryAuth.addScopes(conn, amazonQScopes) } if (this.auth.getConnectionState(conn) === 'invalid') { @@ -333,11 +328,10 @@ export class AuthUtil { // Edge Case: With the addition of Amazon Q/Chat scopes we may need to add // the new scopes to existing pre-chat connections. if (addMissingScopes) { - if (isBuilderIdConnection(this.conn) && !isValidCodeWhispererChatConnection(this.conn)) { - const conn = await this.secondaryAuth.addScopes(this.conn, codeWhispererChatScopes) - await this.secondaryAuth.useNewConnection(conn) - return - } else if (isIdcSsoConnection(this.conn) && !isValidAmazonQConnection(this.conn)) { + if ( + (isBuilderIdConnection(this.conn) || isIdcSsoConnection(this.conn)) && + !isValidAmazonQConnection(this.conn) + ) { const conn = await this.secondaryAuth.addScopes(this.conn, amazonQScopes) await this.secondaryAuth.useNewConnection(conn) return @@ -386,7 +380,7 @@ export class AuthUtil { } public isValidCodeTransformationAuthUser(): boolean { - return this.isEnterpriseSsoInUse() && this.isConnectionValid() + return (this.isEnterpriseSsoInUse() || this.isBuilderIdInUse()) && this.isConnectionValid() } } @@ -411,23 +405,11 @@ export async function getChatAuthState(cwAuth = AuthUtil.instance): Promise Date: Wed, 17 Apr 2024 17:04:44 -0400 Subject: [PATCH 12/14] update to require problem matcher extension (#4729) When we build locally this problem matcher helps the IDE detect that a webpack watch execution is finished so that the build scripts can progress. Signed-off-by: Nikolas Komonen --- .vscode/extensions.json | 3 +++ CONTRIBUTING.md | 3 ++- packages/amazonq/.vscode/extensions.json | 5 ----- packages/core/.vscode/extensions.json | 5 ----- packages/toolkit/.vscode/extensions.json | 5 ----- 5 files changed, 5 insertions(+), 16 deletions(-) create mode 100644 .vscode/extensions.json delete mode 100644 packages/amazonq/.vscode/extensions.json delete mode 100644 packages/core/.vscode/extensions.json delete mode 100644 packages/toolkit/.vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000000..c5faaa009d6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["amodio.tsl-problem-matcher", "dbaeumer.vscode-eslint"] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 358eb4860fa..51eba829681 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,8 @@ To develop this project, install these dependencies: - [Git](https://git-scm.com/downloads) - (optional) Set `git blame` to ignore noise-commits: `git config blame.ignoreRevsFile .git-blame-ignore-revs` - [AWS `git secrets`](https://github.com/awslabs/git-secrets) -- (required for Web mode) [TypeScript + Webpack Problem Matcher](https://marketplace.visualstudio.com/items?itemName=amodio.tsl-problem-matcher) +- [TypeScript + Webpack Problem Matcher](https://marketplace.visualstudio.com/items?itemName=amodio.tsl-problem-matcher) + - Not installing will result in the following error during building: `Error: Invalid problemMatcher reference: $ts-webpack-watch` - (optional) [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) - (optional) [Docker](https://docs.docker.com/get-docker/) diff --git a/packages/amazonq/.vscode/extensions.json b/packages/amazonq/.vscode/extensions.json deleted file mode 100644 index b1a2d99f09e..00000000000 --- a/packages/amazonq/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": ["dbaeumer.vscode-eslint"] -} diff --git a/packages/core/.vscode/extensions.json b/packages/core/.vscode/extensions.json deleted file mode 100644 index b1a2d99f09e..00000000000 --- a/packages/core/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": ["dbaeumer.vscode-eslint"] -} diff --git a/packages/toolkit/.vscode/extensions.json b/packages/toolkit/.vscode/extensions.json deleted file mode 100644 index b1a2d99f09e..00000000000 --- a/packages/toolkit/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": ["dbaeumer.vscode-eslint"] -} From 26cd09ed8a8602f6013c42f423e065098ad4517d Mon Sep 17 00:00:00 2001 From: David Hasani <60020664+dhasani23@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:02:40 -0700 Subject: [PATCH 13/14] fix(amazonq): show error messages in chat and match with IntelliJ (#4715) * fix(amazonq): show error messages in chat and match with IntelliJ * revert accidental deletion: * remove unused button * small string changes * fix lint issues * Update transformByQ.test.ts --------- Co-authored-by: David Hasani --- ...-5d6c3406-8213-4aec-ba80-a488e9728e3a.json | 4 + packages/core/src/amazonqGumby/activation.ts | 3 - .../chat/controller/controller.ts | 7 +- .../chat/controller/messenger/messenger.ts | 56 ++-- .../controller/messenger/messengerUtils.ts | 19 +- .../controller/messenger/stringConstants.ts | 23 -- .../commands/startTransformByQ.ts | 74 +++-- .../src/codewhisperer/models/constants.ts | 253 +++++++++++------- .../core/src/codewhisperer/models/model.ts | 24 +- .../transformByQ/transformApiHandler.ts | 21 +- .../transformByQ/transformMavenHandler.ts | 8 +- .../transformProjectValidationHandler.ts | 13 +- .../transformationResultsViewProvider.ts | 34 ++- .../commands/transformByQ.test.ts | 10 +- 14 files changed, 301 insertions(+), 248 deletions(-) create mode 100644 .changes/next-release/Bug Fix-5d6c3406-8213-4aec-ba80-a488e9728e3a.json delete mode 100644 packages/core/src/amazonqGumby/chat/controller/messenger/stringConstants.ts diff --git a/.changes/next-release/Bug Fix-5d6c3406-8213-4aec-ba80-a488e9728e3a.json b/.changes/next-release/Bug Fix-5d6c3406-8213-4aec-ba80-a488e9728e3a.json new file mode 100644 index 00000000000..4aadbd1636a --- /dev/null +++ b/.changes/next-release/Bug Fix-5d6c3406-8213-4aec-ba80-a488e9728e3a.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Amazon Q Code Transformation: show error messages in chat" +} diff --git a/packages/core/src/amazonqGumby/activation.ts b/packages/core/src/amazonqGumby/activation.ts index ac88becd152..b52e520160d 100644 --- a/packages/core/src/amazonqGumby/activation.ts +++ b/packages/core/src/amazonqGumby/activation.ts @@ -9,7 +9,6 @@ import { TransformationHubViewProvider } from '../codewhisperer/service/transfor import { ExtContext } from '../shared/extensions' import { stopTransformByQ } from '../codewhisperer/commands/startTransformByQ' import { transformByQState } from '../codewhisperer/models/model' -import * as CodeWhispererConstants from '../codewhisperer/models/constants' import { ProposedTransformationExplorer } from '../codewhisperer/service/transformByQ/transformationResultsViewProvider' import { codeTransformTelemetryState } from './telemetry/codeTransformTelemetryState' import { telemetry } from '../shared/telemetry/telemetry' @@ -52,8 +51,6 @@ export async function activate(context: ExtContext) { Commands.register('aws.amazonq.stopTransformationInHub', async (cancelSrc: CancelActionPositions) => { if (transformByQState.isRunning()) { void stopTransformByQ(transformByQState.getJobId(), cancelSrc) - } else { - void vscode.window.showInformationMessage(CodeWhispererConstants.noOngoingJobMessage) } }), diff --git a/packages/core/src/amazonqGumby/chat/controller/controller.ts b/packages/core/src/amazonqGumby/chat/controller/controller.ts index ae82b3ac77f..125922f94b3 100644 --- a/packages/core/src/amazonqGumby/chat/controller/controller.ts +++ b/packages/core/src/amazonqGumby/chat/controller/controller.ts @@ -24,6 +24,7 @@ import { validateCanCompileProject, } from '../../../codewhisperer/commands/startTransformByQ' import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model' +import * as CodeWhispererConstants from '../../../codewhisperer/models/constants' import { JavaHomeNotSetError, NoJavaProjectsFoundError, NoMavenJavaProjectsFoundError } from '../../errors' import MessengerUtils, { ButtonActions, GumbyCommands } from './messenger/messengerUtils' import { CancelActionPositions } from '../../telemetry/codeTransformTelemetry' @@ -213,7 +214,7 @@ export class GumbyController { await this.initiateTransformationOnProject(message) break case ButtonActions.CANCEL_TRANSFORMATION_FORM: - this.messenger.sendJobFinishedMessage(message.tabId, true) + this.messenger.sendJobFinishedMessage(message.tabId, CodeWhispererConstants.jobCancelledChatMessage) break case ButtonActions.VIEW_TRANSFORMATION_HUB: await vscode.commands.executeCommand(GumbyCommands.FOCUS_TRANSFORMATION_HUB) @@ -309,10 +310,10 @@ export class GumbyController { await this.prepareProjectForSubmission(message) } - private async transformationFinished(tabID: string, jobStatus: string = '') { + private async transformationFinished(data: { message: string; tabID: string }) { this.sessionStorage.getSession().conversationState = ConversationState.IDLE // at this point job is either completed, partially_completed, cancelled, or failed - this.messenger.sendJobFinishedMessage(tabID, false) + this.messenger.sendJobFinishedMessage(data.tabID, data.message) } private async processHumanChatMessage(data: { message: string; tabID: string }) { diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts index 05b13ebe970..8bd9ca0e753 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messenger.ts @@ -12,6 +12,7 @@ import { AuthFollowUpType, expiredText, enableQText, reauthenticateText } from ' import { ChatItemType } from '../../../../amazonqFeatureDev/models' import { JDKVersion, TransformationCandidateProject } from '../../../../codewhisperer/models/model' import { FeatureAuthState } from '../../../../codewhisperer/util/authUtil' +import * as CodeWhispererConstants from '../../../../codewhisperer/models/constants' import { AppToWebViewMessageDispatcher, AsyncEventProgressMessage, @@ -35,7 +36,6 @@ export type StaticTextResponseType = export type ErrorTextResponseType = | 'no-project-found' - | 'no-workspace-open' | 'no-java-project-found' | 'no-maven-java-project-found' | 'could-not-compile-project' @@ -65,7 +65,7 @@ export class Messenger { public sendErrorMessage(errorMessage: string, tabID: string) { this.dispatcher.sendErrorMessage( - new ErrorMessage(`Sorry, we encountered a problem when processing your request.`, errorMessage, tabID) + new ErrorMessage(CodeWhispererConstants.genericErrorMessage, errorMessage, tabID) ) } @@ -195,7 +195,7 @@ export class Messenger { } public sendCompilationInProgress(tabID: string) { - const message = `I'm building your project. This can take up to 10 minutes, depending on the size of your project.` + const message = CodeWhispererConstants.buildStartedChatMessage this.dispatcher.sendAsyncEventProgress( new AsyncEventProgressMessage(tabID, { inProgress: true, message: undefined }) @@ -210,7 +210,7 @@ export class Messenger { } public sendCompilationFinished(tabID: string) { - const message = `I was able to build your project. I'll start transforming your code soon.` + const message = CodeWhispererConstants.buildSucceededChatMessage this.dispatcher.sendAsyncEventProgress( new AsyncEventProgressMessage(tabID, { @@ -221,23 +221,22 @@ export class Messenger { } public sendJobSubmittedMessage(tabID: string, disableJobActions: boolean = false) { - const message = `I'm starting to transform your code. It can take 10 to 30 minutes to upgrade your code, depending on the size of your project. To monitor progress, go to the Transformation Hub.` + const message = CodeWhispererConstants.jobStartedChatMessage const buttons: ChatItemButton[] = [] if (!disableJobActions) { // Note: buttons can only be clicked once. - // To get around this, we remove the card after they're clicked and then - // resubmit the message. + // To get around this, we remove the card after it's clicked and then resubmit the message. buttons.push({ keepCardAfterClick: true, - text: 'Open Transformation Hub', + text: CodeWhispererConstants.openTransformationHubButtonText, id: ButtonActions.VIEW_TRANSFORMATION_HUB, }) buttons.push({ keepCardAfterClick: true, - text: 'Stop transformation', + text: CodeWhispererConstants.stopTransformationButtonText, id: ButtonActions.STOP_TRANSFORMATION_JOB, }) } @@ -291,40 +290,29 @@ export class Messenger { let message = '...' switch (type) { - case 'no-workspace-open': - message = 'To begin, please open a workspace.' - break case 'no-project-found': - message = `Sorry, I couldn't find any open projects. Currently, I can only upgrade Java projects built on Maven. - -For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html).` + message = CodeWhispererConstants.noOpenProjectsFoundChatMessage break case 'no-java-project-found': - message = `Sorry, I can't upgrade any of your open projects. Currently, I can only upgrade Java projects built on Maven. - -For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html).` + message = CodeWhispererConstants.noJavaProjectsFoundChatMessage break case 'no-maven-java-project-found': - message = `Sorry, I can't upgrade any of your open projects. I couldn't find a pom.xml file in any of your Java projects. Currently, I can only upgrade Java projects built on Maven. - -For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html).` + message = CodeWhispererConstants.noPomXmlFoundChatMessage break case 'could-not-compile-project': - message = `Sorry, I couldn't run Maven clean install to build your project. To troubleshoot, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#maven-commands-failing).` + message = CodeWhispererConstants.cleanInstallErrorChatMessage break case 'invalid-java-home': - message = `Sorry, I couldn't locate your Java installation. To troubleshoot, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#maven-commands-failing).` + message = CodeWhispererConstants.noJavaHomeFoundChatMessage break case 'unsupported-source-jdk-version': - message = `Sorry, currently I can only upgrade Java 8 or Java 11 projects. - -For more information, see the [Amazon Q documentation.](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html).` + message = CodeWhispererConstants.unsupportedJavaVersionChatMessage } const buttons: ChatItemButton[] = [] buttons.push({ keepCardAfterClick: false, - text: 'Start a new transformation', + text: CodeWhispererConstants.startTransformationButtonText, id: ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW, }) @@ -344,19 +332,11 @@ For more information, see the [Amazon Q documentation.](https://docs.aws.amazon. this.dispatcher.sendCommandMessage(new SendCommandMessage(message.command, message.tabId, message.eventId)) } - public sendJobFinishedMessage(tabID: string, cancelled: boolean, jobStatus: string = '') { - let message = - 'I cancelled your transformation. If you want to start another transformation, choose **Start a new transformation**.' - - if (!cancelled) { - message = - 'The transformation job is over. If you want to start another transformation, choose **Start a new transformation**.' - } - + public sendJobFinishedMessage(tabID: string, message: string = '') { const buttons: ChatItemButton[] = [] buttons.push({ keepCardAfterClick: false, - text: 'Start a new transformation', + text: CodeWhispererConstants.startTransformationButtonText, id: ButtonActions.CONFIRM_START_TRANSFORMATION_FLOW, }) @@ -380,7 +360,7 @@ For more information, see the [Amazon Q documentation.](https://docs.aws.amazon. this.dispatcher.sendAsyncEventProgress( new AsyncEventProgressMessage(tabID, { inProgress: true, - message: "I'm checking for open projects that are eligible for Code Transformation.", + message: CodeWhispererConstants.checkingForProjectsChatMessage, }) ) } diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts index 97bdf4411f4..b499e658f80 100644 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts +++ b/packages/core/src/amazonqGumby/chat/controller/messenger/messengerUtils.ts @@ -6,12 +6,7 @@ import * as os from 'os' import { transformByQState, JDKVersion } from '../../../../codewhisperer/models/model' -import { - enterJavaHomeMessage, - nonWindowsJava11HomeHelpMessage, - nonWindowsJava8HomeHelpMessage, - windowsJavaHomeHelpMessage, -} from './stringConstants' +import * as CodeWhispererConstants from '../../../../codewhisperer/models/constants' // These enums map to string IDs export enum ButtonActions { @@ -32,18 +27,20 @@ export enum GumbyCommands { export default class MessengerUtils { static createJavaHomePrompt = (): string => { - let javaHomePrompt = `${enterJavaHomeMessage} ${transformByQState.getSourceJDKVersion()}. \n` + let javaHomePrompt = `${ + CodeWhispererConstants.enterJavaHomeChatMessage + } ${transformByQState.getSourceJDKVersion()}. \n` if (os.platform() === 'win32') { - javaHomePrompt += windowsJavaHomeHelpMessage.replace( + javaHomePrompt += CodeWhispererConstants.windowsJavaHomeHelpChatMessage.replace( 'JAVA_VERSION_HERE', transformByQState.getSourceJDKVersion()! ) } else { const jdkVersion = transformByQState.getSourceJDKVersion() if (jdkVersion === JDKVersion.JDK8) { - javaHomePrompt += ` ${nonWindowsJava8HomeHelpMessage}` + javaHomePrompt += ` ${CodeWhispererConstants.nonWindowsJava8HomeHelpChatMessage}` } else if (jdkVersion === JDKVersion.JDK11) { - javaHomePrompt += ` ${nonWindowsJava11HomeHelpMessage}` + javaHomePrompt += ` ${CodeWhispererConstants.nonWindowsJava11HomeHelpChatMessage}` } } return javaHomePrompt @@ -77,6 +74,6 @@ export default class MessengerUtils { } } - return `I can upgrade your ${javaVersionString}. To start the transformation, I need some information from you. Choose the project you want to upgrade and the target code version to upgrade to. Then, choose Transform.` + return CodeWhispererConstants.projectPromptChatMessage.replace('JAVA_VERSION_HERE', javaVersionString) } } diff --git a/packages/core/src/amazonqGumby/chat/controller/messenger/stringConstants.ts b/packages/core/src/amazonqGumby/chat/controller/messenger/stringConstants.ts deleted file mode 100644 index 75d0926abc7..00000000000 --- a/packages/core/src/amazonqGumby/chat/controller/messenger/stringConstants.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - * - */ - -export const enterJavaHomeMessage = 'Enter the path to JDK ' - -export const windowsJavaHomeHelpMessage = - 'To find the JAVA_HOME path, run the following command in a new IDE terminal: `cd "C:\\Program Files\\Java" && dir`. If you see your JDK version, run `cd ` and then `cd` to show the path.' - -export const nonWindowsJava8HomeHelpMessage = - 'To find the JAVA_HOME path, run the following command in a new IDE terminal: `/usr/libexec/java_home -v 1.8`' - -export const nonWindowsJava11HomeHelpMessage = - 'To find the JAVA_HOME path, run the following command in a new IDE terminal: `/usr/libexec/java_home -v 11`' - -export const projectSizeTooLargeMessage = - 'Your project size exceeds the Amazon Q Code Transformation upload limit of 1GB. For more information, see the [Code Transformation documentation](LINK_HERE).' - -export const JDK8VersionNumber = '52' - -export const JDK11VersionNumber = '55' diff --git a/packages/core/src/codewhisperer/commands/startTransformByQ.ts b/packages/core/src/codewhisperer/commands/startTransformByQ.ts index 562fdb8ec71..2fa47302827 100644 --- a/packages/core/src/codewhisperer/commands/startTransformByQ.ts +++ b/packages/core/src/codewhisperer/commands/startTransformByQ.ts @@ -4,7 +4,6 @@ */ import * as vscode from 'vscode' -import * as nls from 'vscode-nls' import * as fs from 'fs' import * as os from 'os' import path from 'path' @@ -53,9 +52,6 @@ import { ChatSessionManager } from '../../amazonqGumby/chat/storages/chatSession import { getDependenciesFolderInfo, writeLogs } from '../service/transformByQ/transformFileHandler' import { sleep } from '../../shared/utilities/timeoutUtils' -const localize = nls.loadMessageBundle() -export const stopTransformByQButton = localize('aws.codewhisperer.stop.transform.by.q', 'Stop') - let sessionJobHistory: { timestamp: string; module: string; status: string; duration: string; id: string }[] = [] export async function startTransformByQWithProgress() { @@ -164,7 +160,6 @@ export async function startTransformByQ() { const status = await pollTransformationStatusUntilComplete(jobId) // Set the result state variables for our store and the UI - // At this point job should be completed or partially completed await finalizeTransformationJob(status) } catch (error: any) { await transformationJobErrorHandler(error) @@ -178,6 +173,8 @@ export async function preTransformationUploadCode() { await vscode.commands.executeCommand('aws.amazonq.refresh') await vscode.commands.executeCommand('aws.amazonq.transformationHub.focus') + void vscode.window.showInformationMessage(CodeWhispererConstants.jobStartedNotification) + let uploadId = '' let payloadFilePath = '' throwIfCancelled() @@ -187,6 +184,8 @@ export async function preTransformationUploadCode() { uploadId = await uploadPayload(payloadFilePath) } catch (err) { const errorMessage = `Failed to upload code due to ${(err as Error).message}` + transformByQState.setJobFailureErrorNotification(CodeWhispererConstants.failedToUploadProjectNotification) + transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToUploadProjectChatMessage) getLogger().error(errorMessage) throw err } @@ -202,8 +201,18 @@ export async function startTransformationJob(uploadId: string) { try { jobId = await startJob(uploadId) } catch (error) { - const errorMessage = CodeWhispererConstants.failedToStartJobMessage - transformByQState.setJobFailureErrorMessage(errorMessage) + getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToStartJobNotification}`, error) + if ((error as Error).message.includes('too many active running jobs')) { + transformByQState.setJobFailureErrorNotification( + CodeWhispererConstants.failedToStartJobTooManyJobsNotification + ) + transformByQState.setJobFailureErrorChatMessage( + CodeWhispererConstants.failedToStartJobTooManyJobsChatMessage + ) + } else { + transformByQState.setJobFailureErrorNotification(CodeWhispererConstants.failedToStartJobNotification) + transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToStartJobChatMessage) + } throw new Error('Start job failed') } transformByQState.setJobId(encodeHTML(jobId)) @@ -219,18 +228,18 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string) { try { await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForPlanGenerated) } catch (error) { - const errorMessage = CodeWhispererConstants.failedToCompleteJobMessage - getLogger().error(`CodeTransformation: ${errorMessage}`, error) - transformByQState.setJobFailureErrorMessage(errorMessage) + getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) + transformByQState.setJobFailureErrorNotification(CodeWhispererConstants.failedToCompleteJobNotification) + transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage) throw new Error('Poll job failed') } let plan = undefined try { plan = await getTransformationPlan(jobId) } catch (error) { - const errorMessage = CodeWhispererConstants.failedToCompleteJobMessage - getLogger().error(`CodeTransformation: ${errorMessage}`, error) - transformByQState.setJobFailureErrorMessage(errorMessage) + getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) + transformByQState.setJobFailureErrorNotification(CodeWhispererConstants.failedToGetPlanNotification) + transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToGetPlanChatMessage) throw new Error('Get plan failed') } @@ -248,9 +257,9 @@ export async function pollTransformationStatusUntilComplete(jobId: string) { try { status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl) } catch (error) { - const errorMessage = CodeWhispererConstants.failedToCompleteJobMessage - getLogger().error(`CodeTransformation: ${errorMessage}`, error) - transformByQState.setJobFailureErrorMessage(errorMessage) + getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error) + transformByQState.setJobFailureErrorNotification(CodeWhispererConstants.failedToCompleteJobNotification) + transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage) throw new Error('Poll job failed') } @@ -259,10 +268,10 @@ export async function pollTransformationStatusUntilComplete(jobId: string) { export async function finalizeTransformationJob(status: string) { if (!(status === 'COMPLETED' || status === 'PARTIALLY_COMPLETED')) { - const errorMessage = CodeWhispererConstants.failedToCompleteJobMessage - getLogger().error(`CodeTransformation: ${errorMessage}`) + getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`) sessionPlanProgress['transformCode'] = StepProgress.Failed - transformByQState.setJobFailureErrorMessage(errorMessage) + transformByQState.setJobFailureErrorNotification(CodeWhispererConstants.failedToCompleteJobNotification) + transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.failedToCompleteJobChatMessage) throw new Error('Job was not successful nor partially successful') } @@ -337,7 +346,16 @@ export async function postTransformationJob() { sessionPlanProgress['transformCode'] = StepProgress.Failed } - transformByQState.getChatControllers()?.transformationFinished.fire(ChatSessionManager.Instance.getSession().tabID) + let chatMessage = transformByQState.getJobFailureErrorChatMessage() + if (transformByQState.isSucceeded()) { + chatMessage = CodeWhispererConstants.jobCompletedChatMessage + } else if (transformByQState.isPartiallySucceeded()) { + chatMessage = CodeWhispererConstants.jobPartiallyCompletedChatMessage + } + + transformByQState + .getChatControllers() + ?.transformationFinished.fire({ message: chatMessage, tabID: ChatSessionManager.Instance.getSession().tabID }) const durationInMs = calculateTotalLatency(codeTransformTelemetryState.getStartTime()) const resultStatusMessage = codeTransformTelemetryState.getResultStatus() @@ -366,11 +384,11 @@ export async function postTransformationJob() { ) if (transformByQState.isSucceeded()) { - void vscode.window.showInformationMessage(CodeWhispererConstants.transformByQCompletedMessage) + void vscode.window.showInformationMessage(CodeWhispererConstants.jobCompletedNotification) } else if (transformByQState.isPartiallySucceeded()) { void vscode.window .showInformationMessage( - CodeWhispererConstants.transformByQPartiallyCompletedMessage, + CodeWhispererConstants.jobPartiallyCompletedNotification, CodeWhispererConstants.amazonQFeedbackText ) .then(choice => { @@ -390,9 +408,13 @@ export async function transformationJobErrorHandler(error: any) { // means some other error occurred; cancellation already handled by now with stopTransformByQ transformByQState.setToFailed() codeTransformTelemetryState.setResultStatus('JobFailed') - let displayedErrorMessage = transformByQState.getJobFailureErrorMessage() + // jobFailureErrorNotification should always be defined here + let displayedErrorMessage = transformByQState.getJobFailureErrorNotification() ?? 'Job failed' if (transformByQState.getJobFailureMetadata() !== '') { displayedErrorMessage += ` ${transformByQState.getJobFailureMetadata()}` + transformByQState.setJobFailureErrorChatMessage( + `${transformByQState.getJobFailureErrorChatMessage()} ${transformByQState.getJobFailureMetadata()}` + ) } void vscode.window .showErrorMessage(displayedErrorMessage, CodeWhispererConstants.amazonQFeedbackText) @@ -401,6 +423,8 @@ export async function transformationJobErrorHandler(error: any) { void submitFeedback.execute(placeholder, CodeWhispererConstants.amazonQFeedbackKey) } }) + } else { + transformByQState.setJobFailureErrorChatMessage(CodeWhispererConstants.jobCancelledChatMessage) } getLogger().error(`CodeTransformation: ${error.message}`) } @@ -452,7 +476,7 @@ export async function stopTransformByQ( await stopJob(jobId) void vscode.window .showErrorMessage( - CodeWhispererConstants.transformByQCancelledMessage, + CodeWhispererConstants.jobCancelledNotification, CodeWhispererConstants.amazonQFeedbackText ) .then(choice => { @@ -463,7 +487,7 @@ export async function stopTransformByQ( } catch { void vscode.window .showErrorMessage( - CodeWhispererConstants.errorStoppingJobMessage, + CodeWhispererConstants.errorStoppingJobNotification, CodeWhispererConstants.amazonQFeedbackText ) .then(choice => { diff --git a/packages/core/src/codewhisperer/models/constants.ts b/packages/core/src/codewhisperer/models/constants.ts index abe65ca6e6b..8bc5c524652 100644 --- a/packages/core/src/codewhisperer/models/constants.ts +++ b/packages/core/src/codewhisperer/models/constants.ts @@ -271,158 +271,225 @@ export const newCustomizationMessage = 'You have access to new CodeWhisperer cus export const newCustomizationsAvailableKey = 'aws.amazonq.codewhisperer.newCustomizations' -// Amazon Q Code Transformation +// Start of QCT Strings + +export const uploadZipSizeLimitInBytes = 1000000000 // 1GB + +export const maxBufferSize = 1024 * 1024 * 8 // this is 8MB; the default max buffer size for stdout for spawnSync is 1MB + +export const transformationJobPollingIntervalSeconds = 5 + +export const transformationJobTimeoutSeconds = 60 * 60 // 1 hour, to match backend + +export const defaultLanguage = 'Java' + +export const contentChecksumType = 'SHA_256' + +export const uploadIntent = 'TRANSFORMATION' + +export const transformationType = 'LANGUAGE_UPGRADE' + +// job successfully started +export const validStatesForJobStarted = [ + 'STARTED', + 'PREPARING', + 'PREPARED', + 'PLANNING', + 'PLANNED', + 'TRANSFORMING', + 'TRANSFORMED', +] + +// initial build succeeded +export const validStatesForBuildSucceeded = ['PREPARED', 'PLANNING', 'PLANNED', 'TRANSFORMING', 'TRANSFORMED'] + +// plan must be available +export const validStatesForPlanGenerated = ['PLANNED', 'TRANSFORMING', 'TRANSFORMED'] + +export const failureStates = ['FAILED', 'STOPPING', 'STOPPED', 'REJECTED'] + +// if status is COMPLETED or PARTIALLY_COMPLETED we can download artifacts +export const validStatesForCheckingDownloadUrl = [ + 'COMPLETED', + 'PARTIALLY_COMPLETED', + 'FAILED', + 'STOPPING', + 'STOPPED', + 'REJECTED', +] export const amazonQFeedbackKey = 'Amazon Q' export const amazonQFeedbackText = 'Submit feedback' -export const selectProjectPrompt = 'Select the project you want to transform' +export const jobStartedChatMessage = + "I'm starting to transform your code. It can take 10 to 30 minutes to upgrade your code, depending on the size of your project. To monitor progress, go to the Transformation Hub." -export const compilingProjectMessage = 'Amazon Q is compiling your project. This can take up to 10 minutes.' +export const jobStartedNotification = + 'Amazon Q is transforming your code. It can take 10 to 30 minutes to upgrade your code, depending on the size of your project. To monitor progress, go to the Transformation Hub.' -export const submittedProjectMessage = - 'Your project has been submitted for transformation. The code transformation process may take 10-30 mins depending on the size of your project.' +export const openTransformationHubButtonText = 'Open Transformation Hub' -export const unsupportedJavaVersionSelectedMessage = - 'None of your open projects are supported by Amazon Q Code Transformation. For more information, see the [Amazon Q documentation](LINK_HERE).' +export const startTransformationButtonText = 'Start a new transformation' -export const transformByQWindowTitle = 'Amazon Q Code Transformation' +export const stopTransformationButtonText = 'Stop transformation' -export const failedToStartJobMessage = - "Amazon Q couldn't begin the transformation. Try starting the transformation again." +export const checkingForProjectsChatMessage = + "I'm checking for open projects that are eligible for Code Transformation." -export const failedToCompleteJobMessage = - "Amazon Q couldn't complete the transformation. Try starting the transformation again." +export const buildStartedChatMessage = + "I'm building your project. This can take up to 10 minutes, depending on the size of your project." -export const stopTransformByQMessage = 'Stop transformation?' +export const buildSucceededChatMessage = 'I was able to build your project and will start transforming your code soon.' -export const stoppingTransformByQMessage = 'Stopping transformation...' +export const buildSucceededNotification = + 'Amazon Q was able to build your project and will start transforming your code soon.' -export const transformByQCancelledMessage = 'You cancelled the transformation.' +export const unsupportedJavaVersionChatMessage = + 'Sorry, currently I can only upgrade Java 8 or Java 11 projects. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html#prerequisites).' -export const transformByQCompletedMessage = - 'Amazon Q successfully transformed your code. You can download a summary of the transformation and a diff with proposed changes in the Transformation Hub.' +export const failedToStartJobChatMessage = + "Sorry, I couldn't begin the transformation. Please try starting the transformation again." -export const transformByQPartiallyCompletedMessage = - 'Amazon Q partially transformed your code. You can download a summary of the transformation and a diff with proposed changes in the Transformation Hub.' +export const failedToStartJobNotification = + "Amazon Q couldn't begin the transformation. Please try starting the transformation again." -export const noPomXmlFoundMessage = - 'None of your open Java projects are supported by Amazon Q Code Transformation. Currently, Amazon Q can only upgrade Java projects built on Maven. A pom.xml must be present in the root of your project to upgrade it. For more information, see the [Amazon Q documentation](LINK_HERE).' +export const failedToStartJobTooManyJobsChatMessage = + 'Sorry, I couldn’t begin the transformation. You have too many active transformations running. Please try again after your other transformations have completed.' + +export const failedToStartJobTooManyJobsNotification = + "Amazon Q couldn't begin the transformation. You have too many active transformations running. Please try again after your other transformations have completed." + +export const failedToUploadProjectChatMessage = + "Sorry, I couldn't upload your project. Please try starting the transformation again." export const noOngoingJobMessage = 'No job is in-progress at the moment' -export const jobInProgressMessage = 'Job is already in-progress' +export const failedToUploadProjectNotification = + "Amazon Q couldn't upload your project. Please try starting the transformation again." -export const cancellationInProgressMessage = 'Cancellation is in-progress' +export const failedToGetPlanChatMessage = + "Sorry, I couldn't create the transformation plan to upgrade your project. Please try starting the transformation again." -export const errorStoppingJobMessage = "Amazon Q couldn't stop the transformation." +export const failedToGetPlanNotification = + "Amazon Q couldn't create the transformation plan to upgrade your project. Please try starting the transformation again." -export const errorDownloadingDiffMessage = - "Amazon Q couldn't download the diff with your upgraded code. Try downloading it again. For more information, see the [Amazon Q documentation](LINK_HERE)." +export const failedToCompleteJobChatMessage = + "Sorry, I couldn't complete the transformation. Please try starting the transformation again." -export const emptyDiffMessage = - "Amazon Q didn't make any changes to upgrade your code. Try restarting the transformation." +export const failedToCompleteJobNotification = + "Amazon Q couldn't complete the transformation. Please try starting the transformation again." -export const errorDeserializingDiffMessage = - "Amazon Q couldn't parse the diff with your upgraded code. Try restarting the transformation." +export const genericErrorMessage = + "Sorry, I'm experiencing technical issues at the moment. Please try again in a few minutes." -export const viewProposedChangesMessage = - 'Download complete. You can view a summary of the transformation and accept or reject the proposed changes in the Transformation Hub.' +export const jobCancelledChatMessage = + 'I cancelled your transformation. If you want to start another transformation, choose **Start a new transformation**.' -export const changesAppliedMessage = 'Amazon Q applied the changes to your project.' +export const jobCancelledNotification = 'You cancelled the transformation.' -export const noSupportedJavaProjectsFoundMessage = - 'None of your open projects are supported by Amazon Q Code Transformation. Currently, Amazon Q can only upgrade Java projects built on Maven. For more information, see the [Amazon Q documentation](LINK_HERE).' +export const jobCompletedChatMessage = + 'I upgraded your code to Java 17. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated.' -export const linkToDocsHome = 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html' +export const jobCompletedNotification = + 'Amazon Q upgraded your code to Java 17. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated.' -export const linkToPrerequisites = - 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html#prerequisites' +export const jobPartiallyCompletedChatMessage = + 'I upgraded part of your code to Java 17. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation.' -export const linkToMavenTroubleshooting = - 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#maven-commands-failing' +export const jobPartiallyCompletedNotification = + 'Amazon Q upgraded part of your code to Java 17. You can review the diff to see my proposed changes and accept or reject them. The transformation summary has details about the files I updated and the errors that prevented a complete transformation.' -export const linkToUploadZipTooLarge = - 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#project-size-limit' +export const noPomXmlFoundChatMessage = + "Sorry, I couldn't find a project that I can upgrade. I couldn't find a pom.xml file in any of your open projects. Currently, I can only upgrade Java 8 or Java 11 projects built on Maven. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html#prerequisites)." -export const linkToDownloadZipTooLarge = - 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#output-artifact-size-limit' +export const noPomXmlFoundNotification = + "None of your open projects are supported by Amazon Q Code Transformation. Amazon Q couldn't find a pom.xml file in any of your open projects. Currently, Amazon Q can only upgrade Java 8 or Java 11 projects built on Maven. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html#prerequisites)." -export const dependencyDisclaimer = - 'Please confirm you are ready to proceed with the transformation. Amazon Q Code Transformation will upload the application code and its dependency binaries from your machine to start the upgrade. If you have not yet compiled the application on your local machine, please do so once before starting the upgrade.' +export const noJavaHomeFoundChatMessage = + "Sorry, I couldn't locate your Java installation. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html#prerequisites)." -export const dependencyFolderName = 'transformation_dependencies_temp_' +export const errorStoppingJobChatMessage = "Sorry, I couldn't stop the transformation." -export const installErrorMessage = - "Amazon Q couldn't execute the Maven clean install command. To troubleshoot, see the [Amazon Q Code Transformation documentation](LINK_HERE)." +export const errorStoppingJobNotification = "Amazon Q couldn't stop the transformation." -export const dependencyErrorMessage = - "Amazon Q couldn't execute the Maven copy-dependencies command. To troubleshoot, see the [Amazon Q Code Transformation documentation](LINK_HERE)." +export const errorDownloadingDiffChatMessage = + "Sorry, I couldn't download the diff with your upgraded code. Please try downloading it again. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#output-artifact-size-limit)." -export const planIntroductionMessage = - 'We reviewed your Java JAVA_VERSION_HERE application and generated a transformation plan. Any code changes made to your application will be done in the sandbox so as to not interfere with your working repository. Once the transformation job is done, we will share the new code which you can review before acccepting the code changes. In the meantime, you can work on your codebase and invoke Q Chat to answer questions about your codebase.' +export const errorDownloadingDiffNotification = + "Amazon Q couldn't download the diff with your upgraded code. Please try downloading it again. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#output-artifact-size-limit)." -export const planDisclaimerMessage = '**Proposed transformation changes** \n\n\n' +export const errorDeserializingDiffChatMessage = + "Sorry, I couldn't parse the diff with your upgraded code. Please try starting the transformation again. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#output-artifact-size-limit)." -export const numMillisecondsPerSecond = 1000 +export const errorDeserializingDiffNotification = + "Amazon Q couldn't parse the diff with your upgraded code. Please try starting the transformation again. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#output-artifact-size-limit)." -export const uploadZipSizeLimitInBytes = 1000000000 // 1GB +export const viewProposedChangesChatMessage = + 'Download complete. You can view a summary of the transformation and accept or reject the proposed changes in the Transformation Hub.' -export const maxBufferSize = 1024 * 1024 * 8 // this is 8MB; the default max buffer size for stdout for spawnSync is 1MB +export const viewProposedChangesNotification = + 'Download complete. You can view a summary of the transformation and accept or reject the proposed changes in the Transformation Hub.' -export const transformByQStateRunningMessage = 'running' +export const changesAppliedChatMessage = 'I applied the changes to your project.' -export const transformByQStateCancellingMessage = 'cancelling' +export const changesAppliedNotification = 'Amazon Q applied the changes to your project.' -export const transformByQStateFailedMessage = 'failed' +export const noOpenProjectsFoundChatMessage = + "Sorry, I couldn't find a project that I can upgrade. Currently, I can only upgrade Java 8 or Java 11 projects built on Maven. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html#prerequisites)." -export const transformByQStateSucceededMessage = 'succeeded' +export const noJavaProjectsFoundChatMessage = + "Sorry, I couldn't find a project that I can upgrade. Currently, I can only upgrade Java 8 or Java 11 projects built on Maven. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html#prerequisites)." -export const transformByQStatePartialSuccessMessage = 'partially succeeded' +export const linkToDocsHome = 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/code-transformation.html' -export const transformByQStoppedState = 'STOPPED' +export const linkToPrerequisites = '' -export const transformationJobPollingIntervalSeconds = 5 +export const linkToMavenTroubleshooting = '' -export const transformationJobTimeoutSeconds = 60 * 60 // 1 hour, to match backend +export const linkToUploadZipTooLarge = + 'https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#project-size-limit' -export const defaultLanguage = 'Java' +export const linkToDownloadZipTooLarge = '' -export const contentChecksumType = 'SHA_256' +export const dependencyFolderName = 'transformation_dependencies_temp_' -export const uploadIntent = 'TRANSFORMATION' +export const cleanInstallErrorChatMessage = + "Sorry, I couldn't run the Maven clean install command to build your project. For more information, see the [Amazon Q Code Transformation documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#maven-commands-failing)." -export const transformationType = 'LANGUAGE_UPGRADE' +export const cleanInstallErrorNotification = + "Amazon Q couldn't run the Maven clean install command to build your project. For more information, see the [Amazon Q Code Transformation documentation](https://docs.aws.amazon.com/amazonq/latest/aws-builder-use-ug/troubleshooting-code-transformation.html#maven-commands-failing)." -// job successfully started -export const validStatesForJobStarted = [ - 'STARTED', - 'PREPARING', - 'PREPARED', - 'PLANNING', - 'PLANNED', - 'TRANSFORMING', - 'TRANSFORMED', -] +export const enterJavaHomeChatMessage = 'Enter the path to JDK ' -// initial build succeeded -export const validStatesForBuildSucceeded = ['PREPARED', 'PLANNING', 'PLANNED', 'TRANSFORMING', 'TRANSFORMED'] +export const projectPromptChatMessage = + 'I can upgrade your JAVA_VERSION_HERE. To start the transformation, I need some information from you. Choose the project you want to upgrade and the target code version to upgrade to. Then, choose Transform.' -// plan must be available -export const validStatesForPlanGenerated = ['PLANNED', 'TRANSFORMING', 'TRANSFORMED'] +export const windowsJavaHomeHelpChatMessage = + 'To find the JDK path, run the following command in a new IDE terminal: `cd "C:\\Program Files\\Java" && dir`. If you see your JDK version, run `cd ` and then `cd` to show the path.' -export const failureStates = ['FAILED', 'STOPPING', 'STOPPED', 'REJECTED'] +export const nonWindowsJava8HomeHelpChatMessage = + 'To find the JDK path, run the following command in a new IDE terminal: `/usr/libexec/java_home -v 1.8`' -// if status is COMPLETED or PARTIALLY_COMPLETED we can download artifacts -export const validStatesForCheckingDownloadUrl = [ - 'COMPLETED', - 'PARTIALLY_COMPLETED', - 'FAILED', - 'STOPPING', - 'STOPPED', - 'REJECTED', -] +export const nonWindowsJava11HomeHelpChatMessage = + 'To find the JDK path, run the following command in a new IDE terminal: `/usr/libexec/java_home -v 11`' + +export const projectSizeTooLargeChatMessage = + 'Sorry, your project size exceeds the Amazon Q Code Transformation upload limit of 1GB. For more information, see the [Code Transformation documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/troubleshooting-code-transformation.html#project-size-limit).' + +export const projectSizeTooLargeNotification = + 'Your project size exceeds the Amazon Q Code Transformation upload limit of 1GB. For more information, see the [Code Transformation documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/troubleshooting-code-transformation.html#project-size-limit).' + +export const JDK8VersionNumber = '52' + +export const JDK11VersionNumber = '55' + +export const planIntroductionMessage = + 'We reviewed your Java JAVA_VERSION_HERE application and generated a transformation plan. Any code changes made to your application will be done in the sandbox so as to not interfere with your working repository. Once the transformation job is done, we will share the new code which you can review before acccepting the code changes. In the meantime, you can work on your codebase and invoke Q Chat to answer questions about your codebase.' + +export const planDisclaimerMessage = '**Proposed transformation changes**\n\n' + +// end of QCT Strings export enum UserGroup { Classifier = 'Classifier', diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index 968b05bf687..c5db805f593 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -303,7 +303,9 @@ export class TransformByQState { private payloadFilePath: string = '' - private jobFailureErrorMessage: string = '' + private jobFailureErrorNotification: string | undefined = undefined + + private jobFailureErrorChatMessage: string | undefined = undefined private errorLog: string = '' @@ -393,8 +395,12 @@ export class TransformByQState { return this.payloadFilePath } - public getJobFailureErrorMessage() { - return this.jobFailureErrorMessage + public getJobFailureErrorNotification() { + return this.jobFailureErrorNotification + } + + public getJobFailureErrorChatMessage() { + return this.jobFailureErrorChatMessage } public getErrorLog() { @@ -497,8 +503,12 @@ export class TransformByQState { this.payloadFilePath = payloadFilePath } - public setJobFailureErrorMessage(errorMessage: string) { - this.jobFailureErrorMessage = errorMessage + public setJobFailureErrorNotification(errorNotification: string) { + this.jobFailureErrorNotification = errorNotification + } + + public setJobFailureErrorChatMessage(errorChatMessage: string) { + this.jobFailureErrorChatMessage = errorChatMessage } public setMavenName(mavenName: string) { @@ -544,7 +554,9 @@ export class TransformByQState { public setJobDefaults() { this.setToNotStarted() // so that the "Transform by Q" button resets this.polledJobStatus = '' // reset polled job status too - this.jobFailureErrorMessage = '' + this.jobFailureErrorNotification = undefined + this.jobFailureErrorChatMessage = undefined + this.jobFailureMetadata = '' this.payloadFilePath = '' this.errorLog = '' } diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts index ec2855f2b6d..582b41a2c95 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformApiHandler.ts @@ -27,10 +27,10 @@ import { codeTransformTelemetryState } from '../../../amazonqGumby/telemetry/cod import { calculateTotalLatency } from '../../../amazonqGumby/telemetry/codeTransformTelemetry' import { MetadataResult } from '../../../shared/telemetry/telemetryClient' import request from '../../../common/request' -import { projectSizeTooLargeMessage } from '../../../amazonqGumby/chat/controller/messenger/stringConstants' import { ZipExceedsSizeLimitError } from '../../../amazonqGumby/errors' import { writeLogs } from './transformFileHandler' import { AuthUtil } from '../../util/authUtil' +import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' export function getSha256(buffer: Buffer) { const hasher = crypto.createHash('sha256') @@ -326,9 +326,11 @@ export async function zipCode(dependenciesFolder: FolderInfo) { }) if (exceedsLimit) { - void vscode.window.showErrorMessage( - projectSizeTooLargeMessage.replace('LINK_HERE', CodeWhispererConstants.linkToUploadZipTooLarge) - ) + void vscode.window.showErrorMessage(CodeWhispererConstants.projectSizeTooLargeNotification) + transformByQState.getChatControllers()?.transformationFinished.fire({ + message: CodeWhispererConstants.projectSizeTooLargeChatMessage, + tabID: ChatSessionManager.Instance.getSession().tabID, + }) throw new ZipExceedsSizeLimitError() } @@ -374,7 +376,7 @@ export async function startJob(uploadId: string) { result: MetadataResult.Fail, reason: 'StartTransformationFailed', }) - throw new Error('Start job failed') + throw new Error(`Start job failed: ${errorMessage}`) } } @@ -505,6 +507,12 @@ export async function pollTransformationJob(jobId: string, validStates: string[] } transformByQState.setPolledJobStatus(status) await vscode.commands.executeCommand('aws.amazonq.refresh') + const errorMessage = response.transformationJob.reason + if (errorMessage !== undefined) { + transformByQState.setJobFailureErrorChatMessage(errorMessage) + transformByQState.setJobFailureErrorNotification(errorMessage) + transformByQState.setJobFailureMetadata(` (request ID: ${response.$response.requestId})`) + } if (validStates.includes(status)) { break } @@ -513,9 +521,6 @@ export async function pollTransformationJob(jobId: string, validStates: string[] * is called, we break above on validStatesForCheckingDownloadUrl and check final status in finalizeTransformationJob */ if (CodeWhispererConstants.failureStates.includes(status)) { - transformByQState.setJobFailureMetadata( - `${response.transformationJob.reason} (request ID: ${response.$response.requestId})` - ) throw new Error('Job was rejected, stopped, or failed') } await sleep(CodeWhispererConstants.transformationJobPollingIntervalSeconds * 1000) diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts index 6ed1044a5e6..12f1e07fd31 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformMavenHandler.ts @@ -161,12 +161,7 @@ export async function prepareProjectDependencies(dependenciesFolder: FolderInfo) try { installProjectDependencies(dependenciesFolder) } catch (err) { - void vscode.window.showErrorMessage( - CodeWhispererConstants.installErrorMessage.replace( - 'LINK_HERE', - CodeWhispererConstants.linkToMavenTroubleshooting - ) - ) + void vscode.window.showErrorMessage(CodeWhispererConstants.cleanInstallErrorNotification) // open build-logs.txt file to show user error logs const logFilePath = await writeLogs() const doc = await vscode.workspace.openTextDocument(logFilePath) @@ -175,6 +170,7 @@ export async function prepareProjectDependencies(dependenciesFolder: FolderInfo) } throwIfCancelled() + void vscode.window.showInformationMessage(CodeWhispererConstants.buildSucceededNotification) } export async function getVersionData() { diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformProjectValidationHandler.ts b/packages/core/src/codewhisperer/service/transformByQ/transformProjectValidationHandler.ts index d97359b4335..a241a6d1aa3 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformProjectValidationHandler.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformProjectValidationHandler.ts @@ -14,7 +14,6 @@ import { JDKToTelemetryValue, } from '../../../amazonqGumby/telemetry/codeTransformTelemetry' import { MetadataResult } from '../../../shared/telemetry/telemetryClient' -import { JDK11VersionNumber, JDK8VersionNumber } from '../../../amazonqGumby/chat/controller/messenger/stringConstants' import { NoJavaProjectsFoundError, NoMavenJavaProjectsFoundError, @@ -140,9 +139,9 @@ async function getProjectsValidToTransform( } else { const majorVersionIndex = spawnResult.stdout.indexOf('major version: ') const javaVersion = spawnResult.stdout.slice(majorVersionIndex + 15, majorVersionIndex + 17).trim() - if (javaVersion === JDK8VersionNumber) { + if (javaVersion === CodeWhispererConstants.JDK8VersionNumber) { detectedJavaVersion = JDKVersion.JDK8 - } else if (javaVersion === JDK11VersionNumber) { + } else if (javaVersion === CodeWhispererConstants.JDK11VersionNumber) { detectedJavaVersion = JDKVersion.JDK11 } else { detectedJavaVersion = JDKVersion.UNSUPPORTED @@ -179,7 +178,6 @@ export async function validateOpenProjects( if (javaProjects.length === 0) { if (!onProjectFirstOpen) { - void vscode.window.showErrorMessage(CodeWhispererConstants.noSupportedJavaProjectsFoundMessage) telemetry.codeTransform_isDoubleClickedToTriggerInvalidProject.emit({ codeTransformSessionId: codeTransformTelemetryState.getSessionId(), codeTransformPreValidationError: 'NoJavaProject', @@ -193,12 +191,7 @@ export async function validateOpenProjects( const mavenJavaProjects = await getMavenJavaProjects(javaProjects) if (mavenJavaProjects.length === 0) { if (!onProjectFirstOpen) { - void vscode.window.showErrorMessage( - CodeWhispererConstants.noPomXmlFoundMessage.replace( - 'LINK_HERE', - CodeWhispererConstants.linkToPrerequisites - ) - ) + void vscode.window.showErrorMessage(CodeWhispererConstants.noPomXmlFoundNotification) telemetry.codeTransform_isDoubleClickedToTriggerInvalidProject.emit({ codeTransformSessionId: codeTransformTelemetryState.getSessionId(), codeTransformPreValidationError: 'NonMavenProject', diff --git a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts index 7de39ee8dfc..125687b814d 100644 --- a/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts +++ b/packages/core/src/codewhisperer/service/transformByQ/transformationResultsViewProvider.ts @@ -19,6 +19,7 @@ import { calculateTotalLatency } from '../../../amazonqGumby/telemetry/codeTrans import { MetadataResult } from '../../../shared/telemetry/telemetryClient' import * as CodeWhispererConstants from '../../models/constants' import { createCodeWhispererChatStreamingClient } from '../../../shared/clients/codewhispererChatClient' +import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession' export abstract class ProposedChangeNode { abstract readonly resourcePath: string @@ -138,10 +139,6 @@ export class DiffModel { */ public parseDiff(pathToDiff: string, pathToWorkspace: string): ProposedChangeNode[] { const diffContents = fs.readFileSync(pathToDiff, 'utf8') - if (!diffContents) { - void vscode.window.showErrorMessage(CodeWhispererConstants.emptyDiffMessage) - throw new Error('diff.patch file is empty') - } const changedFiles = parsePatch(diffContents) // path to the directory containing copy of the changed files in the transformed project const pathToTmpSrcDir = this.copyProject(pathToWorkspace, changedFiles) @@ -315,12 +312,11 @@ export class ProposedTransformationExplorer { } catch (e: any) { downloadErrorMessage = (e as Error).message // This allows the customer to retry the download - void vscode.window.showErrorMessage( - CodeWhispererConstants.errorDownloadingDiffMessage.replace( - 'LINK_HERE', - CodeWhispererConstants.linkToDownloadZipTooLarge - ) - ) + void vscode.window.showErrorMessage(CodeWhispererConstants.errorDownloadingDiffNotification) + transformByQState.getChatControllers()?.transformationFinished.fire({ + message: CodeWhispererConstants.errorDownloadingDiffChatMessage, + tabID: ChatSessionManager.Instance.getSession().tabID, + }) await vscode.commands.executeCommand( 'setContext', 'gumby.reviewState', @@ -382,12 +378,20 @@ export class ProposedTransformationExplorer { }) // Do not await this so that the summary reveals without user needing to close this notification - void vscode.window.showInformationMessage(CodeWhispererConstants.viewProposedChangesMessage) + void vscode.window.showInformationMessage(CodeWhispererConstants.viewProposedChangesNotification) + transformByQState.getChatControllers()?.transformationFinished.fire({ + message: CodeWhispererConstants.viewProposedChangesChatMessage, + tabID: ChatSessionManager.Instance.getSession().tabID, + }) await vscode.commands.executeCommand('aws.amazonq.transformationHub.summary.reveal') } catch (e: any) { deserializeErrorMessage = (e as Error).message getLogger().error(`CodeTransformation: ParseDiff error = ${deserializeErrorMessage}`) - void vscode.window.showErrorMessage(CodeWhispererConstants.errorDeserializingDiffMessage) + transformByQState.getChatControllers()?.transformationFinished.fire({ + message: CodeWhispererConstants.errorDeserializingDiffChatMessage, + tabID: ChatSessionManager.Instance.getSession().tabID, + }) + void vscode.window.showErrorMessage(CodeWhispererConstants.errorDeserializingDiffNotification) } finally { telemetry.codeTransform_jobArtifactDownloadAndDeserializeTime.emit({ codeTransformSessionId: codeTransformTelemetryState.getSessionId(), @@ -407,7 +411,11 @@ export class ProposedTransformationExplorer { await vscode.commands.executeCommand('setContext', 'gumby.transformationProposalReviewInProgress', false) await vscode.commands.executeCommand('setContext', 'gumby.reviewState', TransformByQReviewStatus.NotStarted) transformDataProvider.refresh() - await vscode.window.showInformationMessage(CodeWhispererConstants.changesAppliedMessage) + await vscode.window.showInformationMessage(CodeWhispererConstants.changesAppliedNotification) + transformByQState.getChatControllers()?.transformationFinished.fire({ + message: CodeWhispererConstants.changesAppliedChatMessage, + tabID: ChatSessionManager.Instance.getSession().tabID, + }) // delete result archive and copied source code after changes accepted fs.rmSync(transformByQState.getResultArchiveFilePath(), { recursive: true, force: true }) fs.rmSync(transformByQState.getProjectCopyFilePath(), { recursive: true, force: true }) diff --git a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts index 60af299025f..236689f15a2 100644 --- a/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts +++ b/packages/core/src/test/codewhisperer/commands/transformByQ.test.ts @@ -13,11 +13,9 @@ import * as startTransformByQ from '../../../codewhisperer/commands/startTransfo import { HttpResponse } from 'aws-sdk' import * as codeWhisperer from '../../../codewhisperer/client/codewhisperer' import * as CodeWhispererConstants from '../../../codewhisperer/models/constants' -import { getTestWindow } from '../../shared/vscode/window' -import AdmZip from 'adm-zip' -import { stopTransformByQMessage } from '../../../codewhisperer/models/constants' import { convertToTimeString, convertDateToTimestamp } from '../../../shared/utilities/textUtilities' import path from 'path' +import AdmZip from 'adm-zip' import { createTestWorkspaceFolder, toFile } from '../../testUtil' import { NoJavaProjectsFoundError, @@ -78,12 +76,6 @@ describe('transformByQ', function () { }) it('WHEN job is stopped THEN status is updated to cancelled', async function () { - const testWindow = getTestWindow() - testWindow.onDidShowMessage(message => { - if (message.message === stopTransformByQMessage) { - message.selectItem(startTransformByQ.stopTransformByQButton) - } - }) model.transformByQState.setToRunning() await startTransformByQ.stopTransformByQ('abc-123') assert.strictEqual(model.transformByQState.getStatus(), 'Cancelled') From 1bdd6cad3c3696c1a08d57fb862764296549a651 Mon Sep 17 00:00:00 2001 From: Santiago Martin <143631912+sannicm@users.noreply.github.com> Date: Thu, 18 Apr 2024 20:37:03 +0200 Subject: [PATCH 14/14] fix(amazonqFeatureDev): race conditions on session creation #4724 Problem: When a customer starts a /dev chat with a message, e.g. /dev , the connector logic calls two event handlers in amazonqFeatureDev logic parallely that try to create a session, this creates a race condition where the customer could get to code generation with the wrong session, i.e. one with no conversationId. Solution: Lock the getSession function to mitigate concurrent calls. This is a similar approach to the one taken in JetBrain's toolkit: https://github.com/aws/aws-toolkit-jetbrains/blob/ec168fc6e9306f22f07945fd516d6acdc7fe396e/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/storage/ChatSessionStorage.kt#L18 --- .../amazonqFeatureDev/storages/chatSession.ts | 22 +++++++++++----- .../session/chatSessionStorage.test.ts | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/test/amazonqFeatureDev/session/chatSessionStorage.test.ts diff --git a/packages/core/src/amazonqFeatureDev/storages/chatSession.ts b/packages/core/src/amazonqFeatureDev/storages/chatSession.ts index 02093089084..bbf6a6086d4 100644 --- a/packages/core/src/amazonqFeatureDev/storages/chatSession.ts +++ b/packages/core/src/amazonqFeatureDev/storages/chatSession.ts @@ -3,11 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +import AsyncLock from 'async-lock' import { Messenger } from '../controllers/chat/messenger/messenger' import { Session } from '../session/session' import { createSessionConfig } from '../session/sessionConfigFactory' export class ChatSessionStorage { + private lock = new AsyncLock() + private sessions: Map = new Map() constructor(private readonly messenger: Messenger) {} @@ -20,12 +23,19 @@ export class ChatSessionStorage { } public async getSession(tabID: string): Promise { - const sessionFromStorage = this.sessions.get(tabID) - if (sessionFromStorage === undefined) { - // If a session doesn't already exist just create it - return this.createSession(tabID) - } - return sessionFromStorage + /** + * The lock here is added in order to mitigate amazon Q's eventing fire & forget design when integrating with mynah-ui that creates a race condition here. + * The race condition happens when handleDevFeatureCommand in src/amazonq/webview/ui/quickActions/handler.ts is firing two events after each other to amazonqFeatureDev controller + * This eventually may make code generation fail as at the moment of that event it may get from the storage a session that has not been properly updated. + */ + return this.lock.acquire(tabID, async () => { + const sessionFromStorage = this.sessions.get(tabID) + if (sessionFromStorage === undefined) { + // If a session doesn't already exist just create it + return this.createSession(tabID) + } + return sessionFromStorage + }) } // Find all sessions that are currently waiting to be authenticated diff --git a/packages/core/src/test/amazonqFeatureDev/session/chatSessionStorage.test.ts b/packages/core/src/test/amazonqFeatureDev/session/chatSessionStorage.test.ts new file mode 100644 index 00000000000..4248b6ce745 --- /dev/null +++ b/packages/core/src/test/amazonqFeatureDev/session/chatSessionStorage.test.ts @@ -0,0 +1,26 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import * as assert from 'assert' + +import { Messenger } from '../../../amazonqFeatureDev/controllers/chat/messenger/messenger' +import { ChatSessionStorage } from '../../../amazonqFeatureDev/storages/chatSession' +import { createMessenger } from '../utils' + +describe('chatSession', () => { + const tabID = '1234' + let chatStorage: ChatSessionStorage + let messenger: Messenger + + beforeEach(() => { + messenger = createMessenger() + chatStorage = new ChatSessionStorage(messenger) + }) + + it('locks getSession', async () => { + const results = await Promise.allSettled([chatStorage.getSession(tabID), chatStorage.getSession(tabID)]) + assert.equal(results.length, 2) + assert.deepStrictEqual(results[0], results[1]) + }) +})