diff --git a/.changeset/config.json b/.changeset/config.json index ed49f8a22..1c1c17346 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -18,6 +18,7 @@ "@vscode-logging/wrapper" ], ["guided-development", "guided-development-frontend"], + ["vscode-cf-tools", "cloud-foundry-tools-frontend"], [ "@sap-devx/inquirer-gui", "@sap-devx/inquirer-gui-auto-complete-plugin", @@ -59,7 +60,8 @@ "sample-navigator", "@sap-devx/inquirer-gui-date-plugin", "sample-inquirer-gui-vscode-extension", - "websocketserver" + "websocketserver", + "vscode-wing-cf-tools" ], "privatePackages": { "version": true, diff --git a/.eslintrc.js b/.eslintrc.js index 0c386f9b9..209f4db09 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,9 @@ module.exports = { ignorePatterns: [ "projects/task-explorer/**/dist/", "projects/task-explorer/**/coverage/", + "projects/cloud-foundry-tools/**/dist/", + "projects/cloud-foundry-tools/**/coverage/", + "projects/cloud-foundry-tools/**/out/", // Vendored compiled outputs of the RPC library bundled into rpc-example-ws for demo purposes. "projects/vscode-webview-rpc-lib/examples/rpc-example-ws/src/static/rpc/", ], @@ -351,6 +354,43 @@ module.exports = { "@typescript-eslint/only-throw-error": "off", }, }, + { + // cloud-foundry-tools was integrated with these rules relaxed for the migrated code. + // TODO: clean up violations and tighten incrementally. + files: ["projects/cloud-foundry-tools/**"], + rules: { + "eslint-comments/require-description": "off", + "no-unused-vars": "off", + "prefer-spread": "off", + "prefer-rest-params": "off", + }, + }, + { + // Additional TypeScript rules for cloud-foundry-tools packages. + // Legacy code migrated as-is; relaxed to match the historical behaviour. + // TODO: clean up violations and tighten incrementally. + files: ["projects/cloud-foundry-tools/**/*.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/unbound-method": "off", + "@typescript-eslint/prefer-promise-reject-errors": "off", + "@typescript-eslint/no-redundant-type-constituents": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + "@typescript-eslint/no-unsafe-enum-comparison": "off", + "@typescript-eslint/no-base-to-string": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-duplicate-type-constituents": "off", + }, + }, { // vscode-webview-rpc-lib was integrated with these rules relaxed for the migrated code. // TODO: clean up violations and tighten incrementally. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7b5eeeda..bcc64c8e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,8 @@ jobs: projects/task-explorer/packages/tasks_panel/*.vsix projects/task-explorer/examples/npm_task_contrib/*.vsix projects/task-explorer/examples/vscode_task_contrib/*.vsix + projects/cloud-foundry-tools/packages/backend/*.vsix + projects/cloud-foundry-tools/packages/wrapper/*.vsix # default is 90 days; however, we want to avoid excessive storage usage... retention-days: ${{ env.artifact-retention-days }} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8414e46c8..7cd21c2e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ overrides: mem-fs-editor: 12.0.4 yeoman-environment@3>mem-fs: 2.3.0 yeoman-environment@3>mem-fs-editor: 9.7.0 + vue: 3.5.34 importers: @@ -193,7 +194,7 @@ importers: version: 2.0.8 copy-webpack-plugin: specifier: 11.0.0 - version: 11.0.0(webpack@5.104.1(webpack-cli@6.0.1)) + version: 11.0.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) fs-extra: specifier: 11.3.0 version: 11.3.0 @@ -205,10 +206,10 @@ importers: version: 3.0.0 string-replace-loader: specifier: 3.1.0 - version: 3.1.0(webpack@5.104.1(webpack-cli@6.0.1)) + version: 3.1.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) ts-loader: specifier: 9.4.2 - version: 9.4.2(typescript@5.7.3)(webpack@5.104.1(webpack-cli@6.0.1)) + version: 9.4.2(typescript@5.7.3)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) packages/app-studio-toolkit: dependencies: @@ -287,7 +288,7 @@ importers: version: 2.0.8 copy-webpack-plugin: specifier: 11.0.0 - version: 11.0.0(webpack@5.104.1(webpack-cli@6.0.1)) + version: 11.0.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) fs-extra: specifier: 11.3.0 version: 11.3.0 @@ -299,10 +300,10 @@ importers: version: 3.0.0 string-replace-loader: specifier: 3.1.0 - version: 3.1.0(webpack@5.104.1(webpack-cli@6.0.1)) + version: 3.1.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) ts-loader: specifier: 9.4.2 - version: 9.4.2(typescript@5.7.3)(webpack@5.104.1(webpack-cli@6.0.1)) + version: 9.4.2(typescript@5.7.3)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) packages/app-studio-toolkit-themes: {} @@ -589,6 +590,194 @@ importers: specifier: 2.1.3 version: 2.1.3 + projects/cloud-foundry-tools/packages/backend: + dependencies: + '@sap-devx/webview-rpc': + specifier: 0.4.1 + version: 0.4.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@sap/bas-sdk': + specifier: ^3.13.2 + version: 3.13.7(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@sap/cf-tools': + specifier: ^3.2.2 + version: 3.3.1 + '@sap/swa-for-sapbas-vsx': + specifier: ^2.0.7 + version: 2.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@sap/wing-run-config-types': + specifier: ^4.2.0 + version: 4.2.0 + '@vscode-logging/types': + specifier: 2.0.0 + version: 2.0.0 + '@vscode-logging/wrapper': + specifier: 2.0.0 + version: 2.0.0 + cheerio: + specifier: ^1.0.0 + version: 1.2.0 + comment-json: + specifier: ^4.2.2 + version: 4.2.3 + lodash: + specifier: 4.17.21 + version: 4.17.21 + properties-reader: + specifier: 2.3.0 + version: 2.3.0 + devDependencies: + '@types/cheerio': + specifier: ^0.22.35 + version: 0.22.35 + '@types/comment-json': + specifier: 2.4.2 + version: 2.4.2 + '@types/node': + specifier: ^14.14.6 + version: 14.18.63 + '@types/properties-reader': + specifier: ^2.1.3 + version: 2.1.3 + '@types/proxyquire': + specifier: ^1.3.28 + version: 1.3.31 + '@types/rimraf': + specifier: 3.0.0 + version: 3.0.0 + '@types/sinon': + specifier: ^17.0.3 + version: 17.0.4 + '@types/vscode': + specifier: ^1.43.0 + version: 1.75.0 + '@vscode/vsce': + specifier: 2.24.0 + version: 2.24.0 + clean-webpack-plugin: + specifier: ^4.0.0 + version: 4.0.0(webpack@5.106.2) + mocha: + specifier: ^9.2.2 + version: 9.2.2 + proxyquire: + specifier: 2.1.3 + version: 2.1.3 + sinon: + specifier: ^19.0.2 + version: 19.0.5 + standard-version: + specifier: ^9.5.0 + version: 9.5.0 + temp-dir: + specifier: ^2.0.0 + version: 2.0.0 + ts-loader: + specifier: ^9.2.8 + version: 9.6.2(loader-utils@2.0.4)(typescript@5.7.3)(webpack@5.106.2) + ts-node: + specifier: ^10.7.0 + version: 10.9.2(@types/node@14.18.63)(typescript@5.7.3) + webpack: + specifier: ^5.94.0 + version: 5.106.2(webpack-cli@4.10.0) + webpack-cli: + specifier: ^4.9.2 + version: 4.10.0(webpack@5.106.2) + + projects/cloud-foundry-tools/packages/frontend: + dependencies: + '@sap-devx/webview-rpc': + specifier: 0.4.1 + version: 0.4.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@vscode-elements/elements': + specifier: 1.16.1 + version: 1.16.1 + lodash: + specifier: 4.17.21 + version: 4.17.21 + vue: + specifier: 3.5.34 + version: 3.5.34(typescript@5.7.3) + vue-loader: + specifier: 17.2.2 + version: 17.2.2(@vue/compiler-sfc@3.3.8)(vue@3.5.34(typescript@5.7.3))(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) + vue-loading-overlay: + specifier: 6.0.3 + version: 6.0.3(vue@3.5.34(typescript@5.7.3)) + devDependencies: + '@babel/preset-env': + specifier: ^7.22.20 + version: 7.29.7(@babel/core@7.29.0) + '@babel/preset-typescript': + specifier: ^7.23.0 + version: 7.29.7(@babel/core@7.29.0) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.10.0(@testing-library/dom@9.3.4) + '@vitejs/plugin-vue': + specifier: ^5.2.0 + version: 5.2.4(vite@6.3.4(@types/node@14.18.63)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3)) + '@vue/compiler-sfc': + specifier: 3.3.8 + version: 3.3.8 + '@vue/test-utils': + specifier: 2.4.1 + version: 2.4.1(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.7.3)))(vue@3.5.34(typescript@5.7.3)) + '@vue/vue3-jest': + specifier: 29.2.6 + version: 29.2.6(@babel/core@7.29.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)))(typescript@5.7.3)(vue@3.5.34(typescript@5.7.3)) + eslint-plugin-vue: + specifier: 9.18.1 + version: 9.18.1(eslint@8.57.1) + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + jest-environment-jsdom: + specifier: ^29.7.0 + version: 29.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ts-jest: + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)))(typescript@5.7.3) + vite: + specifier: ^6.0.6 + version: 6.3.4(@types/node@14.18.63)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3) + + projects/cloud-foundry-tools/packages/wrapper: + devDependencies: + '@types/node': + specifier: ^14.14.6 + version: 14.18.63 + '@types/sinon': + specifier: ^17.0.3 + version: 17.0.4 + '@types/vscode': + specifier: ^1.43.0 + version: 1.75.0 + '@vscode-logging/types': + specifier: ^2.0.0 + version: 2.0.10 + '@vscode/vsce': + specifier: 2.24.0 + version: 2.24.0 + mocha: + specifier: ^9.2.2 + version: 9.2.2 + sinon: + specifier: ^19.0.2 + version: 19.0.5 + ts-loader: + specifier: ^9.2.8 + version: 9.6.2(loader-utils@2.0.4)(typescript@5.7.3)(webpack@5.106.2) + ts-node: + specifier: ^10.7.0 + version: 10.9.2(@types/node@14.18.63)(typescript@5.7.3) + webpack: + specifier: ^5.94.0 + version: 5.106.2(webpack-cli@4.10.0) + webpack-cli: + specifier: ^4.9.2 + version: 4.10.0(webpack@5.106.2) + projects/feature-toggle-node/packages/feature-toggle-node: dependencies: node-cache: @@ -883,10 +1072,10 @@ importers: version: 0.5.21 string-replace-loader: specifier: ^3.0.1 - version: 3.1.0(webpack@5.104.1(webpack-cli@6.0.1)) + version: 3.1.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) ts-loader: specifier: ^8.0.17 - version: 8.4.0(typescript@5.7.3)(webpack@5.104.1(webpack-cli@6.0.1)) + version: 8.4.0(typescript@5.7.3)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) ts-node: specifier: ^9.1.1 version: 9.1.1(typescript@5.7.3) @@ -921,7 +1110,7 @@ importers: specifier: ^5.0.1 version: 5.0.1 vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vue-loading-overlay: specifier: ^6.0.3 @@ -1031,7 +1220,7 @@ importers: specifier: ^5.0.1 version: 5.0.1 vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.3.10 @@ -1039,19 +1228,19 @@ importers: devDependencies: '@vue/cli-plugin-babel': specifier: ~5.0.0 - version: 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(core-js@3.49.0)(encoding@0.1.13)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1)) + version: 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(core-js@3.49.0)(encoding@0.1.13)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1)) '@vue/cli-plugin-eslint': specifier: ~5.0.0 - version: 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13)(eslint@8.57.1)(webpack-cli@6.0.1(webpack@5.104.1)) + version: 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13)(eslint@8.57.1)(webpack-cli@6.0.1(webpack@5.104.1)) '@vue/cli-service': specifier: ~5.0.0 - version: 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) + version: 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) copyfiles: specifier: ^2.4.1 version: 2.4.1 sass-loader: specifier: ^13.3.2 - version: 13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)) + version: 13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) projects/inquirer-gui/examples/sample-app-vite: dependencies: @@ -1068,7 +1257,7 @@ importers: specifier: ^5.0.1 version: 5.0.1 vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.3.10 @@ -1099,7 +1288,7 @@ importers: specifier: '*' version: 0.10.0 vue: - specifier: ^3.3.8 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.3.10 @@ -1133,7 +1322,7 @@ importers: projects/inquirer-gui/examples/sample-plugin: dependencies: vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) devDependencies: '@vitejs/plugin-vue': @@ -1195,7 +1384,7 @@ importers: specifier: ^3.2.0 version: 3.5.2(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3)) vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.5.7 @@ -1247,7 +1436,7 @@ importers: specifier: ^6.0.0 version: 6.0.0 vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.4.0 @@ -1296,7 +1485,7 @@ importers: specifier: ^3.4.13 version: 3.4.14(typescript@5.7.3)(vite-plugin-vuetify@2.1.3(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)) vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.5.7 @@ -1345,7 +1534,7 @@ importers: specifier: ^3.4.13 version: 3.4.14(typescript@5.7.3)(vite-plugin-vuetify@2.1.3(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)) vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.5.7 @@ -1400,7 +1589,7 @@ importers: specifier: ^6.0.0 version: 6.0.0 vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.5.7 @@ -1470,7 +1659,7 @@ importers: projects/inquirer-gui/packages/label-plugin: dependencies: vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) devDependencies: '@babel/preset-env': @@ -1492,7 +1681,7 @@ importers: specifier: ^3.4.13 version: 3.4.14(typescript@5.7.3)(vite-plugin-vuetify@2.1.3(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)) vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.5.7 @@ -1538,7 +1727,7 @@ importers: projects/inquirer-gui/packages/radio-button-plugin: dependencies: vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.5.7 @@ -1584,7 +1773,7 @@ importers: projects/inquirer-gui/packages/single-checkbox-plugin: dependencies: vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vuetify: specifier: ^3.5.7 @@ -1630,7 +1819,7 @@ importers: projects/inquirer-gui/packages/tiles-plugin: dependencies: vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) devDependencies: '@babel/preset-env': @@ -1843,17 +2032,17 @@ importers: specifier: 5.0.1 version: 5.0.1 vue: - specifier: 3.4.3 - version: 3.4.3(typescript@5.7.3) + specifier: 3.5.34 + version: 3.5.34(typescript@5.7.3) vue-loader: specifier: 17.4.2 - version: 17.4.2(@vue/compiler-sfc@3.4.3)(vue@3.4.3(typescript@5.7.3))(webpack@5.104.1(webpack-cli@6.0.1)) + version: 17.4.2(@vue/compiler-sfc@3.4.3)(vue@3.5.34(typescript@5.7.3))(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) vue-loading-overlay: specifier: 6.0.3 - version: 6.0.3(vue@3.4.3(typescript@5.7.3)) + version: 6.0.3(vue@3.5.34(typescript@5.7.3)) vuetify: specifier: 3.4.9 - version: 3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.4.3(typescript@5.7.3)) + version: 3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.5.34(typescript@5.7.3)) devDependencies: '@babel/core': specifier: ^7.23.0 @@ -1878,19 +2067,19 @@ importers: version: 6.1.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3))) '@testing-library/vue': specifier: 8.0.1 - version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.3(typescript@5.7.3)) + version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.5.34(typescript@5.7.3)) '@vitejs/plugin-vue': specifier: 5.0.2 - version: 5.0.2(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.4.3(typescript@5.7.3)) + version: 5.0.2(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3)) '@vue/compiler-sfc': specifier: 3.4.3 version: 3.4.3 '@vue/test-utils': specifier: 2.4.3 - version: 2.4.3(@vue/server-renderer@3.5.34(vue@3.4.3(typescript@5.7.3)))(vue@3.4.3(typescript@5.7.3)) + version: 2.4.3(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.7.3)))(vue@3.5.34(typescript@5.7.3)) '@vue/vue3-jest': specifier: 29.2.6 - version: 29.2.6(@babel/core@7.29.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)))(typescript@5.7.3)(vue@3.4.3(typescript@5.7.3)) + version: 29.2.6(@babel/core@7.29.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)))(typescript@5.7.3)(vue@3.5.34(typescript@5.7.3)) babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.29.0) @@ -1902,7 +2091,7 @@ importers: version: 20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10) istanbul-instrumenter-loader: specifier: 3.0.1 - version: 3.0.1(webpack@5.104.1(webpack-cli@6.0.1)) + version: 3.0.1(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) jest: specifier: 29.7.0 version: 29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)) @@ -1929,7 +2118,7 @@ importers: version: 5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0) vite-plugin-vuetify: specifier: 2.0.1 - version: 2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.4.3(typescript@5.7.3))(vuetify@3.4.9) + version: 2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3))(vuetify@3.4.9) projects/vscode-logging/examples/extension: dependencies: @@ -2077,7 +2266,7 @@ importers: version: 0.2.6 copy-webpack-plugin: specifier: 9.0.1 - version: 9.0.1(webpack@5.104.1(webpack-cli@6.0.1)) + version: 9.0.1(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) mock-spawn: specifier: 0.2.6 version: 0.2.6 @@ -2089,7 +2278,7 @@ importers: version: 0.2.3 ts-loader: specifier: 8.0.16 - version: 8.0.16(typescript@5.7.3)(webpack@5.104.1(webpack-cli@6.0.1)) + version: 8.0.16(typescript@5.7.3)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) vscode-test: specifier: 1.5.2 version: 1.5.2 @@ -2474,7 +2663,7 @@ importers: version: 11.0.0 copy-webpack-plugin: specifier: ^12.0.2 - version: 12.0.2(webpack@5.104.1(webpack-cli@6.0.1)) + version: 12.0.2(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) cross-env: specifier: ^10.1.0 version: 10.1.0 @@ -2563,7 +2752,7 @@ importers: specifier: 6.1.0 version: 6.1.0 vue: - specifier: ^3.3.4 + specifier: 3.5.34 version: 3.5.34(typescript@5.7.3) vue-loading-overlay: specifier: ^6.0.0 @@ -2652,11 +2841,11 @@ importers: dependencies: yeoman-environment: specifier: 3.19.3 - version: 3.19.3(@types/node@22.20.1) + version: 3.19.3(@types/node@14.18.63) devDependencies: '@types/yeoman-environment': specifier: 2.10.11 - version: 2.10.11(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4) + version: 2.10.11(@yeoman/types@1.11.1(@types/node@14.18.63)(@yeoman/adapter@4.0.2(@types/node@14.18.63))(mem-fs@4.1.4))(mem-fs@4.1.4) packages: @@ -4471,6 +4660,10 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@hutson/parse-repository-url@3.0.2': + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + '@inquirer/ansi@2.0.7': resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -5638,6 +5831,10 @@ packages: '@sap/swa-for-sapbas-vsx@2.0.4': resolution: {integrity: sha512-jG3PU68go30PvCtBfuzjAuWn3eY+2BNuk+v9KqbjUKYEI4ZyxucmrXTF7nrlcosUzUuKnO2//ujJ9T/IE1Wbgw==} + '@sap/wing-run-config-types@4.2.0': + resolution: {integrity: sha512-4pwFxv6APiiWkVZRs1nLrQgYu91ojIK0qEKgXEIhJfGiUQyuHmCatBC3bblUOtd1wMTjbtwE2MsxOK9dBToziA==} + engines: {vscode: ^1.36.0} + '@sap_oss/task_contrib_types@3.1.0': resolution: {integrity: sha512-kqGkb8hNCbqv05qY37peOtQFsjRoSbJvwTi+PAAkzfUrGEmKinJWXAVqGIoPKM1KYCSNWDEpSU7uEjoYXykrJg==} @@ -5780,12 +5977,21 @@ packages: '@sinonjs/fake-timers@11.3.1': resolution: {integrity: sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==} + '@sinonjs/fake-timers@13.0.5': + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + + '@sinonjs/fake-timers@15.4.0': + resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} + '@sinonjs/fake-timers@8.1.0': resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} '@sinonjs/samsam@8.0.0': resolution: {integrity: sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==} + '@sinonjs/samsam@8.0.3': + resolution: {integrity: sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==} + '@sinonjs/text-encoding@0.7.3': resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} deprecated: |- @@ -5833,12 +6039,19 @@ packages: vitest: optional: true + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0. + peerDependencies: + '@testing-library/dom': '>=10 <11' + '@testing-library/vue@8.0.1': resolution: {integrity: sha512-l51ZEpjTQ6glq3wM+asQ1GbKJMGcxwgHEygETx0aCRN4TjFEGvMZy4YdWKs/y7bu4bmLrxcxhbEPP7iPSW/2OQ==} engines: {node: '>=14'} peerDependencies: '@vue/compiler-sfc': '>= 3' - vue: '>= 3' + vue: 3.5.34 '@textlint/ast-node-types@14.8.4': resolution: {integrity: sha512-+fI7miec/r9VeniFV9ppL4jRCmHNsTxieulTUf/4tvGII3db5hGriKHC4p/diq1SkQ9Sgs7kg6UyydxZtpTz1Q==} @@ -6158,6 +6371,9 @@ packages: '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/rimraf@3.0.0': + resolution: {integrity: sha512-7WhJ0MdpFgYQPXlF4Dx+DhgvlPCfz/x5mHaeDQAKhcenvQP1KCpLQ18JklAqeGMYSAT2PxLpzd0g2/HE7fj7hQ==} + '@types/rimraf@3.0.2': resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} @@ -6409,6 +6625,9 @@ packages: resolution: {integrity: sha512-oRhjSzcVjX8ExyaF8hC0zzTqxlVuRlgMHL/Bh4w3xB9+wjbm0FpXylVU/lBrn+kgphwYTrOk3tp+AVShGmlYCg==} engines: {node: '>=18.0.0'} + '@ungap/promise-all-settled@1.1.2': + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} @@ -6417,25 +6636,28 @@ packages: engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.0.0 || ^5.0.0 - vue: ^3.2.25 + vue: 3.5.34 '@vitejs/plugin-vue@5.0.2': resolution: {integrity: sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 - vue: ^3.2.25 + vue: 3.5.34 '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 || ^6.0.0 - vue: ^3.2.25 + vue: 3.5.34 '@vscode-elements/elements@1.11.0': resolution: {integrity: sha512-vC1QDaelqERypHinavJbe4Bl4/g66CVJWKFWFXZyp1seBvTxkgscayDTiE//H95V8b75BgLPDAS1y3pbSIKXag==} + '@vscode-elements/elements@1.16.1': + resolution: {integrity: sha512-AhNPWjEAmPvbya2mWdr/wxx+Iqrg96xFDZr6uM35Q7QjSDwewxI38WdCakXclUZCY2PhedMtbcNXRSdcBl5yIQ==} + '@vscode-logging/logger@2.0.10': resolution: {integrity: sha512-Mg8aW5Bkim3p4vF2S+n5eEKzg/OBSdS9iom3GmmBUGtmZ07WlOROGYm+2i2sN0R05wnqoafuJbN9LeCwMuZIbw==} @@ -6448,12 +6670,18 @@ packages: '@vscode-logging/types@0.1.4': resolution: {integrity: sha512-uxuHQfpX9RbkgSj5unJFmciXRczyFSaAI2aA829MYYkE8jxlhZLRLoiJLymTNiojNVdV7fFE3CILF5Q6M+EBsA==} + '@vscode-logging/types@2.0.0': + resolution: {integrity: sha512-P42r5SPYeJKgMDYb5Ez9rjPlpnGEZ1eDFVjT0azxueaJ65iE259hpROmvSPUd80HAALn9/59L+CgcGLmwZcCmg==} + '@vscode-logging/types@2.0.10': resolution: {integrity: sha512-Y8Qn0Xc7ej8BC+cbDAi5LUKJs3qMGni0zUGZgqhDrXq3sQS5SQ2KG4sk320Si2rdL/nqivJ/gaEAcxD61m1+3w==} '@vscode-logging/types@2.0.8': resolution: {integrity: sha512-9mc5+Iob1BqJ00QExLwdz4+w0A1odSYjvzpKYfP0lVzr7Tdf4iWGspLADyTSC7DtJVQQ+tu411cBju+cYJxE/g==} + '@vscode-logging/wrapper@2.0.0': + resolution: {integrity: sha512-XA8T61WL3vsrfEMUog1km8USEgnD/3H2qXk7fizFzuEFqNRGpVq9AYjxSUuBMsffIdtwfAOFYZJ8HIwzy+cGjA==} + '@vscode-logging/wrapper@2.0.9': resolution: {integrity: sha512-kTx73Ev1O85oS51s6CnIep/F02uwTGrpuUZMi5/uMaxcU8AdzYWpHVeD1T1BuvInaQhHdADzVnF2gpehKYUbog==} @@ -6553,7 +6781,7 @@ packages: peerDependencies: '@babel/core': '*' core-js: ^3 - vue: ^2 || ^3.2.13 + vue: 3.5.34 peerDependenciesMeta: core-js: optional: true @@ -6564,7 +6792,7 @@ packages: resolution: {integrity: sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==} peerDependencies: '@babel/core': ^7.0.0-0 - vue: '*' + vue: 3.5.34 peerDependenciesMeta: vue: optional: true @@ -6676,6 +6904,9 @@ packages: '@vue/compiler-core@3.3.4': resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} + '@vue/compiler-core@3.3.8': + resolution: {integrity: sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==} + '@vue/compiler-core@3.4.3': resolution: {integrity: sha512-u8jzgFg0EDtSrb/hG53Wwh1bAOQFtc1ZCegBpA/glyvTlgHl+tq13o1zvRfLbegYUw/E4mSTGOiCnAJ9SJ+lsg==} @@ -6685,6 +6916,9 @@ packages: '@vue/compiler-dom@3.3.4': resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} + '@vue/compiler-dom@3.3.8': + resolution: {integrity: sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==} + '@vue/compiler-dom@3.4.3': resolution: {integrity: sha512-oGF1E9/htI6JWj/lTJgr6UgxNCtNHbM6xKVreBWeZL9QhRGABRVoWGAzxmtBfSOd+w0Zi5BY0Es/tlJrN6WgEg==} @@ -6694,6 +6928,9 @@ packages: '@vue/compiler-sfc@3.3.4': resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} + '@vue/compiler-sfc@3.3.8': + resolution: {integrity: sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==} + '@vue/compiler-sfc@3.4.3': resolution: {integrity: sha512-NuJqb5is9I4uzv316VRUDYgIlPZCG8D+ARt5P4t5UDShIHKL25J3TGZAUryY/Aiy0DsY7srJnZL5ryB6DD63Zw==} @@ -6703,6 +6940,9 @@ packages: '@vue/compiler-ssr@3.3.4': resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} + '@vue/compiler-ssr@3.3.8': + resolution: {integrity: sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==} + '@vue/compiler-ssr@3.4.3': resolution: {integrity: sha512-wnYQtMBkeFSxgSSQbYGQeXPhQacQiog2c6AlvMldQH6DB+gSXK/0F6DVXAJfEiuBSgBhUc8dwrrG5JQcqwalsA==} @@ -6715,29 +6955,18 @@ packages: '@vue/reactivity-transform@3.3.4': resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} - '@vue/reactivity@3.4.3': - resolution: {integrity: sha512-q5f9HLDU+5aBKizXHAx0w4whkIANs1Muiq9R5YXm0HtorSlflqv9u/ohaMxuuhHWCji4xqpQ1eL04WvmAmGnFg==} + '@vue/reactivity-transform@3.3.8': + resolution: {integrity: sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==} '@vue/reactivity@3.5.34': resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} - '@vue/runtime-core@3.4.3': - resolution: {integrity: sha512-C1r6QhB1qY7D591RCSFhMULyzL9CuyrGc+3PpB0h7dU4Qqw6GNyo4BNFjHZVvsWncrUlKX3DIKg0Y7rNNr06NQ==} - '@vue/runtime-core@3.5.34': resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} - '@vue/runtime-dom@3.4.3': - resolution: {integrity: sha512-wrsprg7An5Ec+EhPngWdPuzkp0BEUxAKaQtN9dPU/iZctPyD9aaXmVtehPJerdQxQale6gEnhpnfywNw3zOv2A==} - '@vue/runtime-dom@3.5.34': resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} - '@vue/server-renderer@3.4.3': - resolution: {integrity: sha512-BUxt8oVGMKKsqSkM1uU3d3Houyfy4WAc2SpSQRebNd+XJGATVkW/rO129jkyL+kpB/2VRKzE63zwf5RtJ3XuZw==} - peerDependencies: - vue: 3.4.3 - '@vue/server-renderer@3.5.34': resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} peerDependencies: @@ -6746,6 +6975,9 @@ packages: '@vue/shared@3.3.4': resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} + '@vue/shared@3.3.8': + resolution: {integrity: sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==} + '@vue/shared@3.4.3': resolution: {integrity: sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ==} @@ -6756,7 +6988,7 @@ packages: resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==} peerDependencies: '@vue/server-renderer': ^3.0.1 - vue: ^3.0.1 + vue: 3.5.34 peerDependenciesMeta: '@vue/server-renderer': optional: true @@ -6765,7 +6997,7 @@ packages: resolution: {integrity: sha512-F4K7mF+ad++VlTrxMJVRnenKSJmO6fkQt2wpRDiKDesQMkfpniGWsqEi/JevxGBo2qEkwwjvTUAoiGJLNx++CA==} peerDependencies: '@vue/server-renderer': ^3.0.1 - vue: ^3.0.1 + vue: 3.5.34 peerDependenciesMeta: '@vue/server-renderer': optional: true @@ -6781,7 +7013,7 @@ packages: jest: 27.x ts-jest: 27.x typescript: '>= 3.x' - vue: ^3.0.0-0 + vue: 3.5.34 peerDependenciesMeta: ts-jest: optional: true @@ -6796,7 +7028,7 @@ packages: babel-jest: 29.x jest: 29.x typescript: '>= 4.3' - vue: ^3.0.0-0 + vue: 3.5.34 peerDependenciesMeta: typescript: optional: true @@ -6807,13 +7039,13 @@ packages: '@vuetify/loader-shared@1.7.1': resolution: {integrity: sha512-kLUvuAed6RCvkeeTNJzuy14pqnkur8lTuner7v7pNE/kVhPR97TuyXwBSBMR1cJeiLiOfu6SF5XlCYbXByEx1g==} peerDependencies: - vue: ^3.0.0 + vue: 3.5.34 vuetify: ^3.0.0-beta.4 '@vuetify/loader-shared@2.1.2': resolution: {integrity: sha512-X+1jBLmXHkpQEnC0vyOb4rtX2QSkBiFhaFXz8yhQqN2A4vQ6k2nChxN4Ol7VAY5KoqMdFoRMnmNdp/1qYXDQig==} peerDependencies: - vue: ^3.0.0 + vue: 3.5.34 vuetify: '>=3' '@webassemblyjs/ast@1.14.1': @@ -6861,6 +7093,12 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@webpack-cli/configtest@1.2.0': + resolution: {integrity: sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==} + peerDependencies: + webpack: 4.x.x || 5.x.x + webpack-cli: 4.x.x + '@webpack-cli/configtest@2.1.1': resolution: {integrity: sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==} engines: {node: '>=14.15.0'} @@ -6875,6 +7113,11 @@ packages: webpack: ^5.82.0 webpack-cli: 6.x.x + '@webpack-cli/info@1.5.0': + resolution: {integrity: sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==} + peerDependencies: + webpack-cli: 4.x.x + '@webpack-cli/info@2.0.2': resolution: {integrity: sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==} engines: {node: '>=14.15.0'} @@ -6889,6 +7132,15 @@ packages: webpack: ^5.82.0 webpack-cli: 6.x.x + '@webpack-cli/serve@1.7.0': + resolution: {integrity: sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==} + peerDependencies: + webpack-cli: 4.x.x + webpack-dev-server: '*' + peerDependenciesMeta: + webpack-dev-server: + optional: true + '@webpack-cli/serve@2.0.5': resolution: {integrity: sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==} engines: {node: '>=14.15.0'} @@ -7029,6 +7281,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + address@1.2.2: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} @@ -7817,6 +8072,10 @@ packages: resolution: {integrity: sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==} engines: {node: '>=0.10.0'} + camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + camelcase@2.1.1: resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==} engines: {node: '>=0.10.0'} @@ -7932,6 +8191,10 @@ packages: chevrotain@7.1.1: resolution: {integrity: sha512-wy3mC1x4ye+O+QkEinVJkPf5u2vsrDIYW9G7ZuwFl6v/Yu0LwUuT2POsb+NUWApebyxfkQq6+yDfRExbnI5rcw==} + chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -7977,6 +8240,12 @@ packages: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} + clean-webpack-plugin@4.0.0: + resolution: {integrity: sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==} + engines: {node: '>=10.0.0'} + peerDependencies: + webpack: '>=4.0.0 <6.0.0' + cli-cursor@2.1.0: resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} engines: {node: '>=4'} @@ -8213,6 +8482,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + condense-newlines@0.2.1: resolution: {integrity: sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==} engines: {node: '>=0.10.0'} @@ -8411,19 +8684,97 @@ packages: continuation-local-storage@3.2.1: resolution: {integrity: sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==} + conventional-changelog-angular@5.0.13: + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} + engines: {node: '>=10'} + conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} + conventional-changelog-atom@2.0.8: + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} + engines: {node: '>=10'} + deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. + + conventional-changelog-codemirror@2.0.8: + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} + engines: {node: '>=10'} + deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. + + conventional-changelog-config-spec@2.1.0: + resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==} + + conventional-changelog-conventionalcommits@4.6.3: + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} + engines: {node: '>=10'} + conventional-changelog-conventionalcommits@7.0.2: resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} engines: {node: '>=16'} + conventional-changelog-core@4.2.4: + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} + engines: {node: '>=10'} + deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead. + + conventional-changelog-ember@2.0.9: + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} + engines: {node: '>=10'} + deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. + + conventional-changelog-eslint@3.0.9: + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} + engines: {node: '>=10'} + deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. + + conventional-changelog-express@2.0.6: + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} + engines: {node: '>=10'} + deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. + + conventional-changelog-jquery@3.0.11: + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} + engines: {node: '>=10'} + deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. + + conventional-changelog-jshint@2.0.9: + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} + engines: {node: '>=10'} + deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead. + + conventional-changelog-preset-loader@2.3.4: + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} + engines: {node: '>=10'} + + conventional-changelog-writer@5.0.1: + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} + engines: {node: '>=10'} + hasBin: true + + conventional-changelog@3.1.25: + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} + engines: {node: '>=10'} + + conventional-commits-filter@2.0.7: + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} + engines: {node: '>=10'} + + conventional-commits-parser@3.2.4: + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} + engines: {node: '>=10'} + hasBin: true + conventional-commits-parser@5.0.0: resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} engines: {node: '>=16'} hasBin: true + conventional-recommended-bump@6.1.0: + resolution: {integrity: sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==} + engines: {node: '>=10'} + hasBin: true + convert-source-map@1.8.0: resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} @@ -8761,6 +9112,15 @@ packages: supports-color: optional: true + debug@4.3.3: + resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -8795,6 +9155,10 @@ packages: decache@4.6.2: resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==} + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -8917,6 +9281,10 @@ packages: resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} engines: {node: '>=0.10.0'} + del@4.1.1: + resolution: {integrity: sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==} + engines: {node: '>=6'} + delay@5.0.0: resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} engines: {node: '>=10'} @@ -9002,10 +9370,18 @@ packages: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + diff@9.0.0: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} @@ -9036,6 +9412,9 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -9093,6 +9472,10 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} + dotgitignore@2.1.0: + resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} + engines: {node: '>=6'} + download-stats@0.3.4: resolution: {integrity: sha512-ic2BigbyUWx7/CBbsfGjf71zUNZB4edBGC3oRliSzsoNmvyVx3Ycfp1w3vp2Y78Ee0eIIkjIEO5KzW0zThDGaA==} engines: {node: '>=0.10.0'} @@ -9452,6 +9835,12 @@ packages: peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + eslint-plugin-vue@9.18.1: + resolution: {integrity: sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + eslint-plugin-vue@9.33.0: resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} engines: {node: ^14.17.0 || >=16.0.0} @@ -9823,6 +10212,10 @@ packages: resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==} engines: {node: '>=0.10.0'} + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -10066,6 +10459,11 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true + get-port@7.0.0: resolution: {integrity: sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==} engines: {node: '>=16'} @@ -10121,12 +10519,31 @@ packages: resolution: {integrity: sha512-F/mS+fsWQMo1zfgG9MD8KWvTWPPzzhuVwY++fhQ5Ggd+0P+CAMHtzMZhNxG+TqGfHDChJKsbh6otfMGqO2AKBw==} engines: {node: '>=4'} + git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. + hasBin: true + git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. hasBin: true + git-remote-origin-url@2.0.0: + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} + engines: {node: '>=4'} + + git-semver-tags@4.1.1: + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} + engines: {node: '>=10'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. + hasBin: true + + gitconfiglocal@1.0.0: + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -10237,6 +10654,10 @@ packages: resolution: {integrity: sha512-JmsqJalahxxgW8V2ecSQ2G7UjPlI9cpKdrkG9KoNiXhd/YslXOTEB0cViENWUznuovIuNT+FkMbraDGjr4FCUg==} engines: {node: '>=20'} + globby@6.1.0: + resolution: {integrity: sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==} + engines: {node: '>=0.10.0'} + globby@8.0.2: resolution: {integrity: sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==} engines: {node: '>=4'} @@ -10273,6 +10694,10 @@ packages: resolution: {integrity: sha512-c5NDCWO0XiXuJAhOegMiNotkDmgORN+VNo3+YHMhWpoWG/u2+8im8byqsOe3/myI9YcC//plRdqGa2AE3Qsdjw==} engines: {node: '>=8.0.0'} + growl@1.10.5: + resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} + engines: {node: '>=4.x'} + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -10298,6 +10723,10 @@ packages: engines: {node: '>=6'} deprecated: this library is no longer supported + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + has-ansi@2.0.0: resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} engines: {node: '>=0.10.0'} @@ -10729,6 +11158,10 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + interpret@3.1.1: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} @@ -10921,6 +11354,18 @@ packages: is-object@1.0.2: resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + + is-path-in-cwd@2.1.0: + resolution: {integrity: sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==} + engines: {node: '>=6'} + + is-path-inside@2.1.0: + resolution: {integrity: sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==} + engines: {node: '>=6'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -11016,6 +11461,10 @@ packages: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} + is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + is-text-path@2.0.0: resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} engines: {node: '>=8'} @@ -11899,6 +12348,10 @@ packages: resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} engines: {node: '>=8.9.0'} + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -11947,6 +12400,9 @@ packages: lodash.isinteger@4.0.4: resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + lodash.isnumber@3.0.3: resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} @@ -12134,6 +12590,10 @@ packages: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + map-visit@1.0.0: resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} engines: {node: '>=0.10.0'} @@ -12227,6 +12687,10 @@ packages: resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==} engines: {node: '>=0.10.0'} + meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} @@ -12355,6 +12819,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@4.2.1: + resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==} + engines: {node: '>=10'} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -12371,6 +12839,10 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + minimist@1.2.0: resolution: {integrity: sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==} @@ -12469,6 +12941,11 @@ packages: engines: {node: '>= 14.0.0'} hasBin: true + mocha@9.2.2: + resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} + engines: {node: '>= 12.0.0'} + hasBin: true + mock-require@3.0.3: resolution: {integrity: sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==} engines: {node: '>=4.3.0'} @@ -12480,6 +12957,10 @@ packages: mock-spawn@0.2.6: resolution: {integrity: sha512-cMSfGmQlTiCF54a7taY1cUVXuuEKqDR8vLrfN7pI4LCskxwRedupMQB7o5d5Hg5L6rctvtyZJwUxMl3czTd4tg==} + modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + module-alias@2.3.4: resolution: {integrity: sha512-bOclZt8hkpuGgSSoG07PKmvzTizROilUTvLNyrMqvlC9snhs7y7GzjNWAVbISIOlhCP1T14rH1PDAV9iNyBq/w==} @@ -12534,6 +13015,11 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.1: + resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -12573,6 +13059,9 @@ packages: nise@6.0.0: resolution: {integrity: sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==} + nise@6.1.5: + resolution: {integrity: sha512-SnRDPDBjxZZoU2n0+gzzLtSvo1OZo7j6jnbXsoh3AFxEGhaFU7ZF0TmefuKERq79wxR2U+MPn7ArW+Tl+clC3A==} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -12674,6 +13163,10 @@ packages: normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + normalize-package-data@5.0.0: resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -13033,6 +13526,10 @@ packages: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -13045,6 +13542,10 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -13117,6 +13618,10 @@ packages: resolution: {integrity: sha512-tb3/zIwbU6Z9RMDxZM3/UsyL5LpIUQj7Drq7iXWG9ilPpzyGG28EEFRRrGTsxHf3sOSOiQEiwevQH/VWtHbZfg==} engines: {node: '>=18.19.0'} + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -13259,6 +13764,9 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + path-key@2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} @@ -13292,6 +13800,9 @@ packages: path-to-regexp@6.2.2: resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@1.1.0: resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} engines: {node: '>=0.10.0'} @@ -13787,6 +14298,14 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + deprecated: |- + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) + qjobs@1.2.0: resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==} engines: {node: '>=0.9'} @@ -13820,6 +14339,10 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -13892,6 +14415,10 @@ packages: resolution: {integrity: sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==} engines: {node: '>=16'} + read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} + read-pkg-up@4.0.0: resolution: {integrity: sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==} engines: {node: '>=6'} @@ -13966,6 +14493,10 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} + rechoir@0.7.1: + resolution: {integrity: sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==} + engines: {node: '>= 0.10'} + rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} @@ -14594,6 +15125,9 @@ packages: sinon@18.0.1: resolution: {integrity: sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==} + sinon@19.0.5: + resolution: {integrity: sha512-r15s9/s+ub/d4bxNXqIUmwp6imVSdTorIRaxoecYjqTVLZ8RuoXr/4EDGwIBo6Waxn7f2gnURX9zuhAfCwaF6Q==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -14769,10 +15303,16 @@ packages: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -14826,6 +15366,11 @@ packages: stacktrace-js@2.0.2: resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + standard-version@9.5.0: + resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==} + engines: {node: '>=10'} + hasBin: true + static-extend@0.1.2: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} @@ -14935,6 +15480,10 @@ packages: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} + stringify-package@1.0.1: + resolution: {integrity: sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==} + deprecated: This module is not used anymore, and has been replaced by @npmcli/package-json + strip-ansi@3.0.1: resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} engines: {node: '>=0.10.0'} @@ -15158,6 +15707,10 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -15202,6 +15755,9 @@ packages: through2@3.0.2: resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==} + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -15318,6 +15874,10 @@ packages: resolution: {integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==} engines: {node: '>=0.10.0'} + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + trim-right@1.0.1: resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} engines: {node: '>=0.10.0'} @@ -15524,6 +16084,10 @@ packages: resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} engines: {node: '>=4'} + type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -15595,6 +16159,9 @@ packages: typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} @@ -15908,7 +16475,7 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: '>=5' - vue: ^3.0.0 + vue: 3.5.34 vuetify: ^3.0.0 vite-plugin-vuetify@2.1.3: @@ -15916,7 +16483,7 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: '>=5' - vue: ^3.0.0 + vue: 3.5.34 vuetify: '>=3' vite@4.5.11: @@ -16159,6 +16726,18 @@ packages: vue-template-compiler: optional: true + vue-loader@17.2.2: + resolution: {integrity: sha512-aqNvKJvnz2A/6VWeJZodAo8XLoAlVwBv+2Z6dama+LHsAF+P/xijQ+OfWrxIs0wcGSJduvdzvTuATzXbNKkpiw==} + peerDependencies: + '@vue/compiler-sfc': '*' + vue: '*' + webpack: ^4.1.0 || ^5.0.0-0 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + vue: + optional: true + vue-loader@17.4.2: resolution: {integrity: sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==} peerDependencies: @@ -16175,13 +16754,13 @@ packages: resolution: {integrity: sha512-Ab0hqnVKasVS2mNUo8Wkm95mSbk5fXp/karZmMou5yP0hTUOox/kZuBLNwgzJ4kFP829wFJl7O7FN1Dr0kRaVQ==} engines: {node: '>=12.13.0'} peerDependencies: - vue: ^3.2.0 + vue: 3.5.34 vue-loading-overlay@6.0.6: resolution: {integrity: sha512-ZPrWawjCoNKGbCG9z4nePgbs/K9KXPa1j1oAJXP6T8FQho3NO+/chhjx4MLYFzfpwr+xkiQ8SNrV1kUG1bZPAw==} engines: {node: '>=12.13.0'} peerDependencies: - vue: ^3.2.0 + vue: 3.5.34 vue-style-loader@4.1.3: resolution: {integrity: sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==} @@ -16192,14 +16771,6 @@ packages: vue-template-es2015-compiler@1.9.1: resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==} - vue@3.4.3: - resolution: {integrity: sha512-GjN+culMAGv/mUbkIv8zMKItno8npcj5gWlXkSxf1SPTQf8eJ4A+YfHIvQFyL1IfuJcMl3soA7SmN1fRxbf/wA==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - vue@3.5.34: resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} peerDependencies: @@ -16213,7 +16784,7 @@ packages: peerDependencies: typescript: '>=4.7' vite-plugin-vuetify: '>=2.1.0' - vue: ^3.5.0 + vue: 3.5.34 webpack-plugin-vuetify: '>=3.1.0' peerDependenciesMeta: typescript: @@ -16229,7 +16800,7 @@ packages: peerDependencies: typescript: '>=4.7' vite-plugin-vuetify: '>=1.0.0-alpha.12' - vue: ^3.3.0 + vue: 3.5.34 vue-i18n: ^9.0.0 webpack-plugin-vuetify: '>=2.0.0-alpha.11' peerDependenciesMeta: @@ -16303,6 +16874,26 @@ packages: engines: {node: '>=8'} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + webpack-cli@4.10.0: + resolution: {integrity: sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + '@webpack-cli/generators': '*' + '@webpack-cli/migrate': '*' + webpack: 4.x.x || 5.x.x + webpack-bundle-analyzer: '*' + webpack-dev-server: '*' + peerDependenciesMeta: + '@webpack-cli/generators': + optional: true + '@webpack-cli/migrate': + optional: true + webpack-bundle-analyzer: + optional: true + webpack-dev-server: + optional: true + webpack-cli@5.1.4: resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} engines: {node: '>=14.15.0'} @@ -16366,7 +16957,7 @@ packages: engines: {node: '>=12'} peerDependencies: '@vue/compiler-sfc': ^3.2.6 - vue: ^3.2.6 + vue: 3.5.34 vuetify: ^3.0.0-beta.4 webpack: ^5.0.0 @@ -16550,6 +17141,9 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workerpool@6.2.0: + resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==} + workerpool@6.5.1: resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} @@ -16595,18 +17189,6 @@ packages: utf-8-validate: optional: true - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -16733,6 +17315,10 @@ packages: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} + yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -17035,14 +17621,14 @@ snapshots: '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.8 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0) + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -17063,7 +17649,7 @@ snapshots: '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -17071,7 +17657,7 @@ snapshots: '@babel/generator@7.29.7': dependencies: '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -17116,7 +17702,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.29.7 '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.0) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -17193,7 +17779,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -17219,7 +17805,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.28.6 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color @@ -17237,13 +17823,13 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-member-expression-to-functions': 7.29.7 '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -17286,7 +17872,7 @@ snapshots: '@babel/helpers@7.29.2': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.8 + '@babel/types': 7.29.7 '@babel/highlight@7.25.9': dependencies: @@ -18323,13 +18909,13 @@ snapshots: dependencies: '@babel/code-frame': 7.29.7 '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 + '@babel/types': 7.29.7 '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 + '@babel/types': 7.29.7 '@babel/traverse@7.29.0': dependencies: @@ -18348,7 +18934,7 @@ snapshots: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 '@babel/types': 7.29.7 debug: 4.4.3 @@ -18965,6 +19551,8 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@hutson/parse-repository-url@3.0.2': {} + '@inquirer/ansi@2.0.7': {} '@inquirer/checkbox@5.2.1(@types/node@14.18.63)': @@ -18976,16 +19564,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/checkbox@5.2.1(@types/node@22.20.1)': - dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/confirm@6.1.1(@types/node@14.18.63)': dependencies: '@inquirer/core': 11.2.1(@types/node@14.18.63) @@ -18993,14 +19571,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/confirm@6.1.1(@types/node@22.20.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/core@11.2.1(@types/node@14.18.63)': dependencies: '@inquirer/ansi': 2.0.7 @@ -19013,19 +19583,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/core@11.2.1(@types/node@22.20.1)': - dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.20.1) - cli-width: 4.1.0 - fast-wrap-ansi: 0.2.2 - mute-stream: 3.0.0 - signal-exit: 4.1.0 - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/editor@5.2.2(@types/node@14.18.63)': dependencies: '@inquirer/core': 11.2.1(@types/node@14.18.63) @@ -19034,15 +19591,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/editor@5.2.2(@types/node@22.20.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/external-editor': 3.0.3(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/expand@5.1.1(@types/node@14.18.63)': dependencies: '@inquirer/core': 11.2.1(@types/node@14.18.63) @@ -19050,13 +19598,12 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/expand@5.1.1(@types/node@22.20.1)': + '@inquirer/external-editor@1.0.3(@types/node@14.18.63)': dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) + chardet: 2.1.1 + iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 22.20.1 - optional: true + '@types/node': 14.18.63 '@inquirer/external-editor@1.0.3(@types/node@22.20.1)': dependencies: @@ -19072,14 +19619,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/external-editor@3.0.3(@types/node@22.20.1)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/figures@2.0.7': {} '@inquirer/input@5.1.2(@types/node@14.18.63)': @@ -19089,14 +19628,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/input@5.1.2(@types/node@22.20.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/number@4.1.1(@types/node@14.18.63)': dependencies: '@inquirer/core': 11.2.1(@types/node@14.18.63) @@ -19104,14 +19635,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/number@4.1.1(@types/node@22.20.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/password@5.1.1(@types/node@14.18.63)': dependencies: '@inquirer/ansi': 2.0.7 @@ -19120,15 +19643,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/password@5.1.1(@types/node@22.20.1)': - dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/prompts@8.5.2(@types/node@14.18.63)': dependencies: '@inquirer/checkbox': 5.2.1(@types/node@14.18.63) @@ -19144,22 +19658,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/prompts@8.5.2(@types/node@22.20.1)': - dependencies: - '@inquirer/checkbox': 5.2.1(@types/node@22.20.1) - '@inquirer/confirm': 6.1.1(@types/node@22.20.1) - '@inquirer/editor': 5.2.2(@types/node@22.20.1) - '@inquirer/expand': 5.1.1(@types/node@22.20.1) - '@inquirer/input': 5.1.2(@types/node@22.20.1) - '@inquirer/number': 4.1.1(@types/node@22.20.1) - '@inquirer/password': 5.1.1(@types/node@22.20.1) - '@inquirer/rawlist': 5.3.1(@types/node@22.20.1) - '@inquirer/search': 4.2.1(@types/node@22.20.1) - '@inquirer/select': 5.2.1(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/rawlist@5.3.1(@types/node@14.18.63)': dependencies: '@inquirer/core': 11.2.1(@types/node@14.18.63) @@ -19167,14 +19665,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/rawlist@5.3.1(@types/node@22.20.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/search@4.2.1(@types/node@14.18.63)': dependencies: '@inquirer/core': 11.2.1(@types/node@14.18.63) @@ -19183,15 +19673,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/search@4.2.1(@types/node@22.20.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/select@5.2.1(@types/node@14.18.63)': dependencies: '@inquirer/ansi': 2.0.7 @@ -19201,25 +19682,10 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - '@inquirer/select@5.2.1(@types/node@22.20.1)': - dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@22.20.1) - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@inquirer/type@4.0.7(@types/node@14.18.63)': optionalDependencies: '@types/node': 14.18.63 - '@inquirer/type@4.0.7(@types/node@22.20.1)': - optionalDependencies: - '@types/node': 22.20.1 - optional: true - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -19253,7 +19719,7 @@ snapshots: '@jest/console@27.5.1': dependencies: '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 jest-message-util: 27.5.1 jest-util: 27.5.1 @@ -19262,7 +19728,7 @@ snapshots: '@jest/console@28.1.3': dependencies: '@jest/types': 28.1.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 jest-message-util: 28.1.3 jest-util: 28.1.3 @@ -19271,7 +19737,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -19284,7 +19750,7 @@ snapshots: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.8.1 @@ -19314,6 +19780,41 @@ snapshots: - ts-node - utf-8-validate + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.63 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3))': dependencies: '@jest/console': 29.7.0 @@ -19321,14 +19822,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)) + jest-config: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -19353,14 +19854,14 @@ snapshots: dependencies: '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-mock: 27.5.1 '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -19378,7 +19879,7 @@ snapshots: dependencies: '@jest/types': 27.5.1 '@sinonjs/fake-timers': 8.1.0 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-message-util: 27.5.1 jest-mock: 27.5.1 jest-util: 27.5.1 @@ -19387,7 +19888,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -19414,7 +19915,7 @@ snapshots: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -19445,7 +19946,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -19574,7 +20075,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/yargs': 16.0.11 chalk: 4.1.2 @@ -19583,7 +20084,7 @@ snapshots: '@jest/schemas': 28.1.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -19592,7 +20093,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -20641,7 +21142,7 @@ snapshots: '@sap-devx/webview-rpc@0.3.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: - ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 7.4.6(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -20822,6 +21323,8 @@ snapshots: - supports-color - utf-8-validate + '@sap/wing-run-config-types@4.2.0': {} + '@sap_oss/task_contrib_types@3.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -21001,6 +21504,14 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@13.0.5': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@15.4.0': + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers@8.1.0': dependencies: '@sinonjs/commons': 1.8.6 @@ -21011,6 +21522,11 @@ snapshots: lodash.get: 4.4.2 type-detect: 4.1.0 + '@sinonjs/samsam@8.0.3': + dependencies: + '@sinonjs/commons': 3.0.1 + type-detect: 4.1.0 + '@sinonjs/text-encoding@0.7.3': {} '@so-ric/colorspace@1.1.6': @@ -21060,13 +21576,23 @@ snapshots: '@types/jest': 29.5.14 jest: 29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)) - '@testing-library/vue@8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.3(typescript@5.7.3))': + '@testing-library/jest-dom@6.10.0(@testing-library/dom@9.3.4)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 9.3.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/vue@8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.5.34(typescript@5.7.3))': dependencies: '@babel/runtime': 7.26.0 '@testing-library/dom': 9.3.4 '@vue/compiler-sfc': 3.4.3 '@vue/test-utils': 2.4.6 - vue: 3.4.3(typescript@5.7.3) + vue: 3.5.34(typescript@5.7.3) '@textlint/ast-node-types@14.8.4': {} @@ -21129,7 +21655,7 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 @@ -21155,7 +21681,7 @@ snapshots: '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/chai-as-promised@7.1.8': dependencies: @@ -21165,7 +21691,7 @@ snapshots: '@types/cheerio@0.22.35': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/comment-json@1.1.1': {} @@ -21176,19 +21702,19 @@ snapshots: '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 4.17.35 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/connect@3.4.35': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/cors@2.8.13': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/cors@2.8.19': dependencies: @@ -21227,7 +21753,7 @@ snapshots: '@types/express-serve-static-core@4.17.35': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 @@ -21246,16 +21772,16 @@ snapshots: '@types/fs-extra@5.0.5': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/glob@7.2.0': dependencies: '@types/minimatch': 3.0.5 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/html-minifier-terser@6.1.0': {} @@ -21263,7 +21789,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/http-terminator@2.0.5': dependencies: @@ -21311,7 +21837,7 @@ snapshots: '@types/jsdom@20.0.1': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -21323,11 +21849,11 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/keyv@3.1.4': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/lodash-es@4.17.12': dependencies: @@ -21362,12 +21888,12 @@ snapshots: '@types/node-fetch@2.6.13': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 form-data: 4.0.5 '@types/node-forge@1.3.14': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/node@12.20.55': {} @@ -21397,7 +21923,7 @@ snapshots: '@types/npm-registry-fetch@8.0.9': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/node-fetch': 2.6.13 '@types/npm-package-arg': 6.1.4 '@types/npmlog': 7.0.0 @@ -21405,7 +21931,7 @@ snapshots: '@types/npmlog@7.0.0': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/object-hash@2.2.1': {} @@ -21425,14 +21951,19 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/retry@0.12.0': {} + '@types/rimraf@3.0.0': + dependencies: + '@types/glob': 7.2.0 + '@types/node': 14.18.63 + '@types/rimraf@3.0.2': dependencies: '@types/glob': 7.2.0 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/sarif@2.1.7': {} @@ -21443,7 +21974,7 @@ snapshots: '@types/send@0.17.1': dependencies: '@types/mime': 1.3.2 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/serve-index@1.9.4': dependencies: @@ -21452,7 +21983,7 @@ snapshots: '@types/serve-static@1.15.1': dependencies: '@types/mime': 3.0.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/shimmer@1.2.0': {} @@ -21468,11 +21999,11 @@ snapshots: '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/ssri@7.1.5': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/stack-utils@2.0.3': {} @@ -21488,7 +22019,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/tmp@0.2.6': {} @@ -21505,7 +22036,7 @@ snapshots: '@types/vinyl@2.0.12': dependencies: '@types/expect': 1.20.4 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/vscode@1.138.0': {} @@ -21527,11 +22058,11 @@ snapshots: '@types/ws@7.4.7': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/ws@8.18.1': dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/yargs-parser@21.0.3': {} @@ -21551,15 +22082,15 @@ snapshots: dependencies: '@types/node': 22.20.1 - '@types/yeoman-environment@2.10.11(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4)': + '@types/yeoman-environment@2.10.11(@yeoman/types@1.11.1(@types/node@14.18.63)(@yeoman/adapter@4.0.2(@types/node@14.18.63))(mem-fs@4.1.4))(mem-fs@4.1.4)': dependencies: '@types/diff': 8.0.0 '@types/inquirer': 8.2.13 '@types/mem-fs': 2.2.0 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/text-table': 0.2.5 '@types/vinyl': 2.0.12 - '@types/yeoman-generator': 6.0.0(@types/node@22.20.1)(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4) + '@types/yeoman-generator': 6.0.0(@types/node@14.18.63)(@yeoman/types@1.11.1(@types/node@14.18.63)(@yeoman/adapter@4.0.2(@types/node@14.18.63))(mem-fs@4.1.4))(mem-fs@4.1.4) chalk: 4.1.2 commander: 9.5.0 execa: 5.1.1 @@ -21571,9 +22102,9 @@ snapshots: - react-native-b4a - supports-color - '@types/yeoman-generator@6.0.0(@types/node@22.20.1)(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4)': + '@types/yeoman-generator@6.0.0(@types/node@14.18.63)(@yeoman/types@1.11.1(@types/node@14.18.63)(@yeoman/adapter@4.0.2(@types/node@14.18.63))(mem-fs@4.1.4))(mem-fs@4.1.4)': dependencies: - yeoman-generator: 8.2.2(@types/node@22.20.1)(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4) + yeoman-generator: 8.2.2(@types/node@14.18.63)(@yeoman/types@1.11.1(@types/node@14.18.63)(@yeoman/adapter@4.0.2(@types/node@14.18.63))(mem-fs@4.1.4))(mem-fs@4.1.4) transitivePeerDependencies: - '@types/node' - '@yeoman/types' @@ -21767,6 +22298,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@ungap/promise-all-settled@1.1.2': {} + '@ungap/structured-clone@1.3.1': {} '@vitejs/plugin-vue@4.5.2(vite@4.5.11(@types/node@22.20.1)(sass@1.56.1)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3))': @@ -21779,10 +22312,15 @@ snapshots: vite: 4.5.14(@types/node@22.20.1)(sass@1.104.0)(terser@5.48.0) vue: 3.5.34(typescript@5.7.3) - '@vitejs/plugin-vue@5.0.2(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.4.3(typescript@5.7.3))': + '@vitejs/plugin-vue@5.0.2(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3))': dependencies: vite: 5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0) - vue: 3.4.3(typescript@5.7.3) + vue: 3.5.34(typescript@5.7.3) + + '@vitejs/plugin-vue@5.2.4(vite@6.3.4(@types/node@14.18.63)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))': + dependencies: + vite: 6.3.4(@types/node@14.18.63)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3) + vue: 3.5.34(typescript@5.7.3) '@vitejs/plugin-vue@5.2.4(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))': dependencies: @@ -21793,6 +22331,10 @@ snapshots: dependencies: lit: 3.3.3 + '@vscode-elements/elements@1.16.1': + dependencies: + lit: 3.3.3 + '@vscode-logging/logger@2.0.10': dependencies: '@vscode-logging/types': 2.0.10 @@ -21809,7 +22351,7 @@ snapshots: '@vscode-logging/logger@2.0.9': dependencies: - '@vscode-logging/types': 2.0.8 + '@vscode-logging/types': 2.0.10 fast-safe-stringify: 2.1.1 fs-extra: 11.2.0 lodash: 4.18.1 @@ -21825,10 +22367,19 @@ snapshots: '@vscode-logging/types@0.1.4': {} + '@vscode-logging/types@2.0.0': {} + '@vscode-logging/types@2.0.10': {} '@vscode-logging/types@2.0.8': {} + '@vscode-logging/wrapper@2.0.0': + dependencies: + '@vscode-logging/logger': 2.0.10 + '@vscode-logging/types': 2.0.10 + transitivePeerDependencies: + - supports-color + '@vscode-logging/wrapper@2.0.9': dependencies: '@vscode-logging/logger': 2.0.9 @@ -21981,7 +22532,7 @@ snapshots: '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.7 '@babel/types': 7.29.8 '@vue/babel-helper-vue-transform-on': 1.5.0 '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0) @@ -22096,11 +22647,11 @@ snapshots: '@vue/cli-overlay@5.0.9': {} - '@vue/cli-plugin-babel@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(core-js@3.49.0)(encoding@0.1.13)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))': + '@vue/cli-plugin-babel@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(core-js@3.49.0)(encoding@0.1.13)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))': dependencies: '@babel/core': 7.29.0 '@vue/babel-preset-app': 5.0.9(@babel/core@7.29.0)(core-js@3.49.0)(vue@3.5.34(typescript@5.7.3)) - '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) + '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) '@vue/cli-shared-utils': 5.0.9(encoding@0.1.13) babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) thread-loader: 3.0.4(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) @@ -22134,9 +22685,9 @@ snapshots: - vue - webpack-cli - '@vue/cli-plugin-eslint@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13)(eslint@8.57.1)(webpack-cli@6.0.1(webpack@5.104.1))': + '@vue/cli-plugin-eslint@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13)(eslint@8.57.1)(webpack-cli@6.0.1(webpack@5.104.1))': dependencies: - '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) + '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) '@vue/cli-shared-utils': 5.0.9(encoding@0.1.13) eslint: 8.57.1 eslint-webpack-plugin: 3.2.0(eslint@8.57.1)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) @@ -22166,9 +22717,9 @@ snapshots: - uglify-js - webpack-cli - '@vue/cli-plugin-router@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13)': + '@vue/cli-plugin-router@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13)': dependencies: - '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) + '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) '@vue/cli-shared-utils': 5.0.9(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -22200,23 +22751,23 @@ snapshots: - encoding - supports-color - '@vue/cli-plugin-vuex@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))': + '@vue/cli-plugin-vuex@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))': dependencies: - '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) + '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) '@vue/cli-plugin-vuex@5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@8.0.2(sass@1.56.1)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))': dependencies: '@vue/cli-service': 5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@8.0.2(sass@1.56.1)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1) - '@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1)': + '@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1)': dependencies: '@babel/helper-compilation-targets': 7.28.6 '@soda/friendly-errors-webpack-plugin': 1.8.1(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) '@soda/get-current-script': 1.0.2 '@types/minimist': 1.2.5 '@vue/cli-overlay': 5.0.9 - '@vue/cli-plugin-router': 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13) - '@vue/cli-plugin-vuex': 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1)) + '@vue/cli-plugin-router': 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1))(encoding@0.1.13) + '@vue/cli-plugin-vuex': 5.0.9(@vue/cli-service@5.0.9(@vue/compiler-sfc@3.5.34)(bufferutil@4.1.0)(ejs@3.1.10)(encoding@0.1.13)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(underscore@1.13.1)(utf-8-validate@5.0.10)(vue-template-compiler@2.7.16)(vue@3.5.34(typescript@5.7.3))(webpack-cli@6.0.1(webpack@5.104.1))(webpack-sources@3.4.1)) '@vue/cli-shared-utils': 5.0.9(encoding@0.1.13) '@vue/component-compiler-utils': 3.3.0(ejs@3.1.10)(handlebars@4.7.9)(lodash@4.18.1)(underscore@1.13.1) '@vue/vue-loader-v15': vue-loader@15.11.1(@vue/compiler-sfc@3.5.34)(css-loader@6.11.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))))(ejs@3.1.10)(handlebars@4.7.9)(lodash@4.18.1)(prettier@2.4.1)(underscore@1.13.1)(vue-template-compiler@2.7.16)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) @@ -22266,7 +22817,7 @@ snapshots: webpack-virtual-modules: 0.4.6 whatwg-fetch: 3.6.20 optionalDependencies: - sass-loader: 13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)) + sass-loader: 13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))) vue-template-compiler: 2.7.16 webpack-sources: 3.4.1 transitivePeerDependencies: @@ -22494,6 +23045,13 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.3.8': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.3.8 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-core@3.4.3': dependencies: '@babel/parser': 7.29.8 @@ -22515,6 +23073,11 @@ snapshots: '@vue/compiler-core': 3.3.4 '@vue/shared': 3.3.4 + '@vue/compiler-dom@3.3.8': + dependencies: + '@vue/compiler-core': 3.3.8 + '@vue/shared': 3.3.8 + '@vue/compiler-dom@3.4.3': dependencies: '@vue/compiler-core': 3.4.3 @@ -22538,6 +23101,19 @@ snapshots: postcss: 8.5.15 source-map-js: 1.2.1 + '@vue/compiler-sfc@3.3.8': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.3.8 + '@vue/compiler-dom': 3.3.8 + '@vue/compiler-ssr': 3.3.8 + '@vue/reactivity-transform': 3.3.8 + '@vue/shared': 3.3.8 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + '@vue/compiler-sfc@3.4.3': dependencies: '@babel/parser': 7.29.8 @@ -22552,7 +23128,7 @@ snapshots: '@vue/compiler-sfc@3.5.34': dependencies: - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@vue/compiler-core': 3.5.34 '@vue/compiler-dom': 3.5.34 '@vue/compiler-ssr': 3.5.34 @@ -22567,6 +23143,11 @@ snapshots: '@vue/compiler-dom': 3.3.4 '@vue/shared': 3.3.4 + '@vue/compiler-ssr@3.3.8': + dependencies: + '@vue/compiler-dom': 3.3.8 + '@vue/shared': 3.3.8 + '@vue/compiler-ssr@3.4.3': dependencies: '@vue/compiler-dom': 3.4.3 @@ -22652,30 +23233,23 @@ snapshots: estree-walker: 2.0.2 magic-string: 0.30.21 - '@vue/reactivity@3.4.3': + '@vue/reactivity-transform@3.3.8': dependencies: - '@vue/shared': 3.4.3 + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.3.8 + '@vue/shared': 3.3.8 + estree-walker: 2.0.2 + magic-string: 0.30.21 '@vue/reactivity@3.5.34': dependencies: '@vue/shared': 3.5.34 - '@vue/runtime-core@3.4.3': - dependencies: - '@vue/reactivity': 3.4.3 - '@vue/shared': 3.4.3 - '@vue/runtime-core@3.5.34': dependencies: '@vue/reactivity': 3.5.34 '@vue/shared': 3.5.34 - '@vue/runtime-dom@3.4.3': - dependencies: - '@vue/runtime-core': 3.4.3 - '@vue/shared': 3.4.3 - csstype: 3.2.3 - '@vue/runtime-dom@3.5.34': dependencies: '@vue/reactivity': 3.5.34 @@ -22683,19 +23257,6 @@ snapshots: '@vue/shared': 3.5.34 csstype: 3.2.3 - '@vue/server-renderer@3.4.3(vue@3.4.3(typescript@5.7.3))': - dependencies: - '@vue/compiler-ssr': 3.4.3 - '@vue/shared': 3.4.3 - vue: 3.4.3(typescript@5.7.3) - - '@vue/server-renderer@3.5.34(vue@3.4.3(typescript@5.7.3))': - dependencies: - '@vue/compiler-ssr': 3.5.34 - '@vue/shared': 3.5.34 - vue: 3.4.3(typescript@5.7.3) - optional: true - '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.7.3))': dependencies: '@vue/compiler-ssr': 3.5.34 @@ -22704,6 +23265,8 @@ snapshots: '@vue/shared@3.3.4': {} + '@vue/shared@3.3.8': {} + '@vue/shared@3.4.3': {} '@vue/shared@3.5.34': {} @@ -22716,13 +23279,13 @@ snapshots: optionalDependencies: '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@5.7.3)) - '@vue/test-utils@2.4.3(@vue/server-renderer@3.5.34(vue@3.4.3(typescript@5.7.3)))(vue@3.4.3(typescript@5.7.3))': + '@vue/test-utils@2.4.3(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.7.3)))(vue@3.5.34(typescript@5.7.3))': dependencies: js-beautify: 1.15.4 - vue: 3.4.3(typescript@5.7.3) + vue: 3.5.34(typescript@5.7.3) vue-component-type-helpers: 1.8.27 optionalDependencies: - '@vue/server-renderer': 3.5.34(vue@3.4.3(typescript@5.7.3)) + '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@5.7.3)) '@vue/test-utils@2.4.6': dependencies: @@ -22747,18 +23310,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@vue/vue3-jest@29.2.6(@babel/core@7.29.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)))(typescript@5.7.3)(vue@3.4.3(typescript@5.7.3))': + '@vue/vue3-jest@29.2.6(@babel/core@7.29.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)))(typescript@5.7.3)(vue@3.5.34(typescript@5.7.3))': dependencies: '@babel/core': 7.29.0 - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.0) babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 2.4.2 convert-source-map: 1.8.0 css-tree: 2.3.1 - jest: 29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)) + jest: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) source-map: 0.5.6 tsconfig: 7.0.0 - vue: 3.4.3(typescript@5.7.3) + vue: 3.5.34(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 transitivePeerDependencies: @@ -22767,7 +23330,7 @@ snapshots: '@vue/vue3-jest@29.2.6(@babel/core@7.29.0)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)))(typescript@5.7.3)(vue@3.5.34(typescript@5.7.3))': dependencies: '@babel/core': 7.29.0 - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.0) babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 2.4.2 convert-source-map: 1.8.0 @@ -22790,17 +23353,17 @@ snapshots: vue: 3.5.34(typescript@5.7.3) vuetify: 3.12.6(typescript@5.7.3)(vite-plugin-vuetify@2.1.3(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.56.1)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6))(vue@3.5.34(typescript@5.7.3))(webpack-plugin-vuetify@2.0.1(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1)))) - '@vuetify/loader-shared@2.1.2(vue@3.4.3(typescript@5.7.3))(vuetify@3.4.9)': + '@vuetify/loader-shared@2.1.2(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)': dependencies: upath: 2.0.1 - vue: 3.4.3(typescript@5.7.3) - vuetify: 3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.4.3(typescript@5.7.3)) + vue: 3.5.34(typescript@5.7.3) + vuetify: 3.12.6(typescript@5.7.3)(vite-plugin-vuetify@2.1.3(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.56.1)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6))(vue@3.5.34(typescript@5.7.3))(webpack-plugin-vuetify@2.0.1(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1)))) - '@vuetify/loader-shared@2.1.2(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)': + '@vuetify/loader-shared@2.1.2(vue@3.5.34(typescript@5.7.3))(vuetify@3.4.9)': dependencies: upath: 2.0.1 vue: 3.5.34(typescript@5.7.3) - vuetify: 3.12.6(typescript@5.7.3)(vite-plugin-vuetify@2.1.3(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.56.1)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6))(vue@3.5.34(typescript@5.7.3))(webpack-plugin-vuetify@2.0.1(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1)))) + vuetify: 3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.5.34(typescript@5.7.3)) '@webassemblyjs/ast@1.14.1': dependencies: @@ -22878,6 +23441,11 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@webpack-cli/configtest@1.2.0(webpack-cli@4.10.0)(webpack@5.106.2)': + dependencies: + webpack: 5.106.2(webpack-cli@4.10.0) + webpack-cli: 4.10.0(webpack@5.106.2) + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.106.2)': dependencies: webpack: 5.106.2(webpack-cli@5.1.4) @@ -22893,6 +23461,11 @@ snapshots: webpack: 5.104.1(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.104.1) + '@webpack-cli/info@1.5.0(webpack-cli@4.10.0)': + dependencies: + envinfo: 7.14.0 + webpack-cli: 4.10.0(webpack@5.106.2) + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.106.2)': dependencies: webpack: 5.106.2(webpack-cli@5.1.4) @@ -22908,6 +23481,10 @@ snapshots: webpack: 5.104.1(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack@5.104.1) + '@webpack-cli/serve@1.7.0(webpack-cli@4.10.0)': + dependencies: + webpack-cli: 4.10.0(webpack@5.106.2) + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.106.2)': dependencies: webpack: 5.106.2(webpack-cli@5.1.4) @@ -22940,20 +23517,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@yeoman/adapter@4.0.2(@types/node@22.20.1)': - dependencies: - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/prompts': 8.5.2(@types/node@22.20.1) - chalk: 5.6.2 - inquirer: 13.4.3(@types/node@22.20.1) - log-symbols: 7.0.1 - ora: 9.4.1 - p-queue: 9.3.1 - text-table: 0.2.0 - transitivePeerDependencies: - - '@types/node' - optional: true - '@yeoman/conflicter@4.1.0(@types/node@14.18.63)(@yeoman/types@1.11.1(@types/node@14.18.63)(@yeoman/adapter@4.0.2(@types/node@14.18.63))(mem-fs@4.1.4))(mem-fs@4.1.4)': dependencies: '@types/node': 14.18.63 @@ -22992,13 +23555,6 @@ snapshots: '@yeoman/adapter': 4.0.2(@types/node@14.18.63) mem-fs: 4.1.4 - '@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4)': - dependencies: - '@types/node': 22.20.1 - optionalDependencies: - '@yeoman/adapter': 4.0.2(@types/node@22.20.1) - mem-fs: 4.1.4 - JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -23061,6 +23617,8 @@ snapshots: acorn@8.16.0: {} + add-stream@1.0.0: {} + address@1.2.2: {} agent-base@6.0.2: @@ -24048,6 +24606,12 @@ snapshots: camelcase: 2.1.1 map-obj: 1.0.1 + camelcase-keys@6.2.2: + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + camelcase@2.1.1: {} camelcase@5.3.1: {} @@ -24057,7 +24621,7 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.28.8 - caniuse-lite: 1.0.30001810 + caniuse-lite: 1.0.30001792 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -24202,6 +24766,18 @@ snapshots: dependencies: regexp-to-ast: 0.5.0 + chokidar@3.5.3: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -24246,6 +24822,11 @@ snapshots: clean-stack@2.2.0: {} + clean-webpack-plugin@4.0.0(webpack@5.106.2): + dependencies: + del: 4.1.1 + webpack: 5.106.2(webpack-cli@4.10.0) + cli-cursor@2.1.0: dependencies: restore-cursor: 2.0.0 @@ -24474,6 +25055,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + condense-newlines@0.2.1: dependencies: extend-shallow: 2.0.1 @@ -24520,14 +25108,115 @@ snapshots: async-listener: 0.6.10 emitter-listener: 1.1.2 + conventional-changelog-angular@5.0.13: + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 + conventional-changelog-atom@2.0.8: + dependencies: + q: 1.5.1 + + conventional-changelog-codemirror@2.0.8: + dependencies: + q: 1.5.1 + + conventional-changelog-config-spec@2.1.0: {} + + conventional-changelog-conventionalcommits@4.6.3: + dependencies: + compare-func: 2.0.0 + lodash: 4.18.1 + q: 1.5.1 + conventional-changelog-conventionalcommits@7.0.2: dependencies: compare-func: 2.0.0 + conventional-changelog-core@4.2.4: + dependencies: + add-stream: 1.0.0 + conventional-changelog-writer: 5.0.1 + conventional-commits-parser: 3.2.4 + dateformat: 3.0.3 + get-pkg-repo: 4.2.1 + git-raw-commits: 2.0.11 + git-remote-origin-url: 2.0.0 + git-semver-tags: 4.1.1 + lodash: 4.18.1 + normalize-package-data: 3.0.3 + q: 1.5.1 + read-pkg: 3.0.0 + read-pkg-up: 3.0.0 + through2: 4.0.2 + + conventional-changelog-ember@2.0.9: + dependencies: + q: 1.5.1 + + conventional-changelog-eslint@3.0.9: + dependencies: + q: 1.5.1 + + conventional-changelog-express@2.0.6: + dependencies: + q: 1.5.1 + + conventional-changelog-jquery@3.0.11: + dependencies: + q: 1.5.1 + + conventional-changelog-jshint@2.0.9: + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + + conventional-changelog-preset-loader@2.3.4: {} + + conventional-changelog-writer@5.0.1: + dependencies: + conventional-commits-filter: 2.0.7 + dateformat: 3.0.3 + handlebars: 4.7.9 + json-stringify-safe: 5.0.1 + lodash: 4.18.1 + meow: 8.1.2 + semver: 6.3.1 + split: 1.0.1 + through2: 4.0.2 + + conventional-changelog@3.1.25: + dependencies: + conventional-changelog-angular: 5.0.13 + conventional-changelog-atom: 2.0.8 + conventional-changelog-codemirror: 2.0.8 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-changelog-core: 4.2.4 + conventional-changelog-ember: 2.0.9 + conventional-changelog-eslint: 3.0.9 + conventional-changelog-express: 2.0.6 + conventional-changelog-jquery: 3.0.11 + conventional-changelog-jshint: 2.0.9 + conventional-changelog-preset-loader: 2.3.4 + + conventional-commits-filter@2.0.7: + dependencies: + lodash.ismatch: 4.4.0 + modify-values: 1.0.1 + + conventional-commits-parser@3.2.4: + dependencies: + JSONStream: 1.3.5 + is-text-path: 1.0.1 + lodash: 4.18.1 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + conventional-commits-parser@5.0.0: dependencies: JSONStream: 1.3.5 @@ -24535,6 +25224,17 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + conventional-recommended-bump@6.1.0: + dependencies: + concat-stream: 2.0.0 + conventional-changelog-preset-loader: 2.3.4 + conventional-commits-filter: 2.0.7 + conventional-commits-parser: 3.2.4 + git-raw-commits: 2.0.11 + git-semver-tags: 4.1.1 + meow: 8.1.2 + q: 1.5.1 + convert-source-map@1.8.0: dependencies: safe-buffer: 5.1.2 @@ -24547,7 +25247,7 @@ snapshots: copy-descriptor@0.1.1: {} - copy-webpack-plugin@11.0.0(webpack@5.104.1(webpack-cli@6.0.1)): + copy-webpack-plugin@11.0.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: fast-glob: 3.2.12 glob-parent: 6.0.2 @@ -24555,7 +25255,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.0.0 serialize-javascript: 6.0.0 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) copy-webpack-plugin@11.0.0(webpack@5.89.0): dependencies: @@ -24567,7 +25267,7 @@ snapshots: serialize-javascript: 6.0.0 webpack: 5.89.0(webpack-cli@5.1.4) - copy-webpack-plugin@12.0.2(webpack@5.104.1(webpack-cli@6.0.1)): + copy-webpack-plugin@12.0.2(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -24575,18 +25275,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.104.1(webpack-cli@6.0.1) - - copy-webpack-plugin@9.0.1(webpack@5.104.1(webpack-cli@6.0.1)): - dependencies: - fast-glob: 3.3.3 - glob-parent: 6.0.2 - globby: 11.1.0 - normalize-path: 3.0.0 - p-limit: 3.1.0 - schema-utils: 3.1.1 - serialize-javascript: 6.0.2 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) copy-webpack-plugin@9.0.1(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: @@ -24611,7 +25300,7 @@ snapshots: core-js-compat@3.49.0: dependencies: - browserslist: 4.28.2 + browserslist: 4.28.8 core-js@2.6.12: {} @@ -24662,6 +25351,21 @@ snapshots: dependencies: capture-stack-trace: 1.0.2 + create-jest@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + create-jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)): dependencies: '@jest/types': 29.6.3 @@ -24934,6 +25638,12 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + debug@4.3.3(supports-color@8.1.1): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 + debug@4.3.4: dependencies: ms: 2.1.2 @@ -24954,6 +25664,11 @@ snapshots: dependencies: callsite: 1.0.0 + decamelize-keys@1.1.1: + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + decamelize@1.2.0: {} decamelize@2.0.0: @@ -25075,6 +25790,16 @@ snapshots: is-descriptor: 1.0.4 isobject: 3.0.1 + del@4.1.1: + dependencies: + '@types/glob': 7.2.0 + globby: 6.1.0 + is-path-cwd: 2.2.0 + is-path-in-cwd: 2.1.0 + p-map: 2.1.0 + pify: 4.0.1 + rimraf: 2.7.1 + delay@5.0.0: {} delayed-stream@1.0.0: {} @@ -25131,8 +25856,12 @@ snapshots: diff@4.0.4: {} + diff@5.0.0: {} + diff@5.2.0: {} + diff@7.0.0: {} + diff@9.0.0: {} diffie-hellman@5.0.3: @@ -25164,6 +25893,8 @@ snapshots: dom-accessibility-api@0.5.16: {} + dom-accessibility-api@0.6.3: {} + dom-converter@0.2.0: dependencies: utila: 0.4.0 @@ -25232,6 +25963,11 @@ snapshots: dotenv@16.4.7: {} + dotgitignore@2.1.0: + dependencies: + find-up: 3.0.0 + minimatch: 3.1.2 + download-stats@0.3.4: dependencies: JSONStream: 1.3.5 @@ -25336,7 +26072,7 @@ snapshots: engine.io@6.6.7(bufferutil@4.1.0)(utf-8-validate@5.0.10): dependencies: '@types/cors': 2.8.13 - '@types/node': 22.20.1 + '@types/node': 14.18.63 '@types/ws': 8.18.1 accepts: 1.3.8 base64id: 2.0.0 @@ -25730,6 +26466,19 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-vue@9.18.1(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + eslint: 8.57.1 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@8.57.1) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + eslint-plugin-vue@9.33.0(eslint@8.57.1): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) @@ -26307,6 +27056,10 @@ snapshots: path-exists: 2.1.0 pinkie-promise: 2.0.1 + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -26583,6 +27336,13 @@ snapshots: get-package-type@0.1.0: {} + get-pkg-repo@4.2.1: + dependencies: + '@hutson/parse-repository-url': 3.0.2 + hosted-git-info: 4.0.2 + through2: 2.0.5 + yargs: 16.2.0 + get-port@7.0.0: {} get-proto@1.0.1: @@ -26633,12 +27393,34 @@ snapshots: got: 7.1.0 is-plain-obj: 1.1.0 + git-raw-commits@2.0.11: + dependencies: + dargs: 7.0.0 + lodash: 4.18.1 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + git-raw-commits@4.0.0: dependencies: dargs: 8.1.0 meow: 12.1.1 split2: 4.2.0 + git-remote-origin-url@2.0.0: + dependencies: + gitconfiglocal: 1.0.0 + pify: 2.3.0 + + git-semver-tags@4.1.1: + dependencies: + meow: 8.1.2 + semver: 6.3.1 + + gitconfiglocal@1.0.0: + dependencies: + ini: 1.3.8 + github-from-package@0.0.0: optional: true @@ -26795,6 +27577,14 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.4.0 + globby@6.1.0: + dependencies: + array-union: 1.0.2 + glob: 7.2.0 + object-assign: 4.1.1 + pify: 2.3.0 + pinkie-promise: 2.0.1 + globby@8.0.2: dependencies: array-union: 1.0.2 @@ -26870,6 +27660,8 @@ snapshots: grouped-queue@2.1.0: {} + growl@1.10.5: {} + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -26904,6 +27696,8 @@ snapshots: ajv: 6.15.0 har-schema: 2.0.0 + hard-rejection@2.1.0: {} + has-ansi@2.0.0: dependencies: ansi-regex: 2.1.1 @@ -27333,19 +28127,6 @@ snapshots: optionalDependencies: '@types/node': 14.18.63 - inquirer@13.4.3(@types/node@22.20.1): - dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@22.20.1) - '@inquirer/prompts': 8.5.2(@types/node@22.20.1) - '@inquirer/type': 4.0.7(@types/node@22.20.1) - mute-stream: 3.0.0 - run-async: 4.0.6 - rxjs: 7.8.2 - optionalDependencies: - '@types/node': 22.20.1 - optional: true - inquirer@7.3.3: dependencies: ansi-escapes: 4.3.2 @@ -27362,9 +28143,9 @@ snapshots: strip-ansi: 6.0.0 through: 2.3.8 - inquirer@8.2.7(@types/node@22.20.1): + inquirer@8.2.7(@types/node@14.18.63): dependencies: - '@inquirer/external-editor': 1.0.3(@types/node@22.20.1) + '@inquirer/external-editor': 1.0.3(@types/node@14.18.63) ansi-escapes: 4.3.2 chalk: 4.1.2 cli-cursor: 3.1.0 @@ -27390,6 +28171,8 @@ snapshots: interpret@1.4.0: {} + interpret@2.2.0: {} + interpret@3.1.1: {} invariant@2.2.4: @@ -27556,6 +28339,16 @@ snapshots: is-object@1.0.2: {} + is-path-cwd@2.2.0: {} + + is-path-in-cwd@2.1.0: + dependencies: + is-path-inside: 2.1.0 + + is-path-inside@2.1.0: + dependencies: + path-is-inside: 1.0.2 + is-path-inside@3.0.3: {} is-path-inside@4.0.0: {} @@ -27626,6 +28419,10 @@ snapshots: has-symbols: 1.1.0 safe-regex-test: 1.1.0 + is-text-path@1.0.1: + dependencies: + text-extensions: 1.9.0 + is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 @@ -27689,13 +28486,13 @@ snapshots: isstream@0.1.2: {} - istanbul-instrumenter-loader@3.0.1(webpack@5.104.1(webpack-cli@6.0.1)): + istanbul-instrumenter-loader@3.0.1(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: convert-source-map: 1.8.0 istanbul-lib-instrument: 1.10.2 loader-utils: 1.4.2 schema-utils: 0.3.0 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) transitivePeerDependencies: - supports-color @@ -27857,7 +28654,7 @@ snapshots: '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -27882,7 +28679,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -27923,6 +28720,25 @@ snapshots: - ts-node - utf-8-validate + jest-cli@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + exit: 0.1.2 + import-local: 3.0.3 + jest-config: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest-cli@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)) @@ -27976,6 +28792,68 @@ snapshots: - supports-color - utf-8-validate + jest-config@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.0 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 14.18.63 + ts-node: 10.9.2(@types/node@14.18.63)(typescript@5.7.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.0 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 14.18.63 + ts-node: 10.9.2(@types/node@22.20.1)(typescript@5.7.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)): dependencies: '@babel/core': 7.29.0 @@ -28057,7 +28935,7 @@ snapshots: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-mock: 27.5.1 jest-util: 27.5.1 jsdom: 16.7.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -28073,7 +28951,7 @@ snapshots: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) @@ -28087,7 +28965,7 @@ snapshots: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-mock: 27.5.1 jest-util: 27.5.1 @@ -28096,7 +28974,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -28110,7 +28988,7 @@ snapshots: dependencies: '@jest/types': 27.5.1 '@types/graceful-fs': 4.1.9 - '@types/node': 22.20.1 + '@types/node': 14.18.63 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -28127,7 +29005,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.20.1 + '@types/node': 14.18.63 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -28145,7 +29023,7 @@ snapshots: '@jest/source-map': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 co: 4.6.0 expect: 27.5.1 @@ -28236,12 +29114,12 @@ snapshots: jest-mock@27.5.1: dependencies: '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): @@ -28305,7 +29183,7 @@ snapshots: '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 emittery: 0.8.1 graceful-fs: 4.2.11 @@ -28334,7 +29212,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -28389,7 +29267,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -28417,16 +29295,16 @@ snapshots: jest-serializer@27.5.1: dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 graceful-fs: 4.2.11 jest-snapshot@27.5.1: dependencies: '@babel/core': 7.29.0 - '@babel/generator': 7.29.8 + '@babel/generator': 7.29.1 '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/traverse': 7.29.8 - '@babel/types': 7.29.8 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 '@types/babel__traverse': 7.28.0 @@ -28477,7 +29355,7 @@ snapshots: jest-util@27.5.1: dependencies: '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -28486,7 +29364,7 @@ snapshots: jest-util@28.1.3: dependencies: '@jest/types': 28.1.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -28495,7 +29373,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -28534,7 +29412,7 @@ snapshots: dependencies: '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 22.20.1 + '@types/node': 14.18.63 ansi-escapes: 4.3.2 chalk: 4.1.2 jest-util: 27.5.1 @@ -28544,7 +29422,7 @@ snapshots: dependencies: '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.10.2 @@ -28555,7 +29433,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.20.1 + '@types/node': 14.18.63 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -28564,19 +29442,19 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@28.1.3: dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.20.1 + '@types/node': 14.18.63 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -28593,6 +29471,18 @@ snapshots: - ts-node - utf-8-validate + jest@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + '@jest/types': 29.6.3 + import-local: 3.0.3 + jest-cli: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)) @@ -29132,6 +30022,11 @@ snapshots: emojis-list: 3.0.0 json5: 2.2.3 + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -29171,6 +30066,8 @@ snapshots: lodash.isinteger@4.0.4: {} + lodash.ismatch@4.4.0: {} + lodash.isnumber@3.0.3: {} lodash.isplainobject@4.0.6: {} @@ -29426,6 +30323,8 @@ snapshots: map-obj@1.0.1: {} + map-obj@4.3.0: {} + map-visit@1.0.0: dependencies: object-visit: 1.0.1 @@ -29603,6 +30502,20 @@ snapshots: redent: 1.0.0 trim-newlines: 1.0.0 + meow@8.1.2: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + merge-descriptors@1.0.1: {} merge-descriptors@1.0.3: {} @@ -29709,6 +30622,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@4.2.1: + dependencies: + brace-expansion: 1.1.11 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -29725,6 +30642,12 @@ snapshots: dependencies: brace-expansion: 2.1.0 + minimist-options@4.1.0: + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + minimist@1.2.0: {} minimist@1.2.8: {} @@ -29852,6 +30775,33 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 + mocha@9.2.2: + dependencies: + '@ungap/promise-all-settled': 1.1.2 + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.3(supports-color@8.1.1) + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 4.2.1 + ms: 2.1.3 + nanoid: 3.3.1 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + which: 2.0.2 + workerpool: 6.2.0 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + mock-require@3.0.3: dependencies: get-caller-file: 1.0.3 @@ -29863,6 +30813,8 @@ snapshots: dependencies: through: 2.3.8 + modify-values@1.0.1: {} + module-alias@2.3.4: {} module-details-from-path@1.0.3: {} @@ -29912,6 +30864,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.1: {} + nanoid@3.3.12: {} nanomatch@1.2.13: @@ -29955,6 +30909,13 @@ snapshots: just-extend: 6.2.0 path-to-regexp: 6.2.2 + nise@6.1.5: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 15.4.0 + just-extend: 6.2.0 + path-to-regexp: 8.4.2 + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -30085,6 +31046,13 @@ snapshots: semver: 5.7.1 validate-npm-package-license: 3.0.4 + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.0.2 + is-core-module: 2.16.1 + semver: 7.6.3 + validate-npm-package-license: 3.0.4 + normalize-package-data@5.0.0: dependencies: hosted-git-info: 6.1.3 @@ -30530,7 +31498,7 @@ snapshots: log-symbols: 6.0.0 stdin-discarder: 0.2.2 string-width: 7.2.0 - strip-ansi: 7.2.0 + strip-ansi: 7.1.0 ora@9.4.1: dependencies: @@ -30576,6 +31544,10 @@ snapshots: p-finally@1.0.0: {} + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -30588,6 +31560,10 @@ snapshots: dependencies: yocto-queue: 1.1.1 + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + p-locate@3.0.0: dependencies: p-limit: 2.3.0 @@ -30660,6 +31636,8 @@ snapshots: '@types/node': 22.20.1 p-queue: 8.1.1 + p-try@1.0.0: {} + p-try@2.2.0: {} p-wait-for@3.2.0: @@ -30871,6 +31849,8 @@ snapshots: path-is-absolute@1.0.1: {} + path-is-inside@1.0.2: {} + path-key@2.0.1: {} path-key@3.1.1: {} @@ -30898,6 +31878,8 @@ snapshots: path-to-regexp@6.2.2: {} + path-to-regexp@8.4.2: {} + path-type@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -31336,6 +32318,8 @@ snapshots: pure-rand@6.1.0: {} + q@1.5.1: {} + qjobs@1.2.0: {} qs@6.13.0: @@ -31362,6 +32346,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-lru@4.0.1: {} + quick-lru@5.1.1: {} randombytes@2.1.0: @@ -31448,6 +32434,11 @@ snapshots: read-pkg: 8.1.0 type-fest: 4.41.0 + read-pkg-up@3.0.0: + dependencies: + find-up: 2.1.0 + read-pkg: 3.0.0 + read-pkg-up@4.0.0: dependencies: find-up: 3.0.0 @@ -31526,7 +32517,7 @@ snapshots: readable-stream@2.3.8: dependencies: - core-util-is: 1.0.3 + core-util-is: 1.0.2 inherits: 2.0.4 isarray: 1.0.0 process-nextick-args: 2.0.1 @@ -31565,6 +32556,10 @@ snapshots: dependencies: resolve: 1.22.10 + rechoir@0.7.1: + dependencies: + resolve: 1.22.12 + rechoir@0.8.0: dependencies: resolve: 1.22.10 @@ -31707,7 +32702,7 @@ snapshots: dependencies: debug: 4.4.3 module-details-from-path: 1.0.3 - resolve: 1.22.10 + resolve: 1.22.12 transitivePeerDependencies: - supports-color @@ -31911,10 +32906,10 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@13.3.3(sass@1.104.0)(webpack@5.104.1(webpack-cli@6.0.1)): + sass-loader@13.3.3(sass@1.104.0)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: neo-async: 2.6.2 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) optionalDependencies: sass: 1.104.0 @@ -32318,6 +33313,15 @@ snapshots: nise: 6.0.0 supports-color: 7.2.0 + sinon@19.0.5: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 13.0.5 + '@sinonjs/samsam': 8.0.3 + diff: 7.0.0 + nise: 6.1.5 + supports-color: 7.2.0 + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29 @@ -32558,8 +33562,16 @@ snapshots: dependencies: extend-shallow: 3.0.2 + split2@3.2.2: + dependencies: + readable-stream: 3.6.2 + split2@4.2.0: {} + split@1.0.1: + dependencies: + through: 2.3.8 + sprintf-js@1.0.3: {} ssh-config@4.2.1: {} @@ -32619,6 +33631,23 @@ snapshots: stack-generator: 2.0.5 stacktrace-gps: 3.0.4 + standard-version@9.5.0: + dependencies: + chalk: 2.4.2 + conventional-changelog: 3.1.25 + conventional-changelog-config-spec: 2.1.0 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-recommended-bump: 6.1.0 + detect-indent: 6.1.0 + detect-newline: 3.1.0 + dotgitignore: 2.1.0 + figures: 3.2.0 + find-up: 5.0.0 + git-semver-tags: 4.1.1 + semver: 7.6.3 + stringify-package: 1.0.1 + yargs: 16.2.0 + static-extend@0.1.2: dependencies: define-property: 0.2.5 @@ -32674,11 +33703,11 @@ snapshots: schema-utils: 3.1.1 webpack: 5.106.2(webpack-cli@5.1.4) - string-replace-loader@3.1.0(webpack@5.104.1(webpack-cli@6.0.1)): + string-replace-loader@3.1.0(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: loader-utils: 2.0.4 schema-utils: 3.1.1 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) string-replace-loader@3.1.0(webpack@5.89.0): dependencies: @@ -32705,13 +33734,13 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.2.0 + strip-ansi: 7.1.0 string-width@7.2.0: dependencies: emoji-regex: 10.6.0 get-east-asian-width: 1.6.0 - strip-ansi: 7.2.0 + strip-ansi: 7.1.0 string-width@8.2.1: dependencies: @@ -32757,6 +33786,8 @@ snapshots: is-obj: 1.0.1 is-regexp: 1.0.0 + stringify-package@1.0.1: {} + strip-ansi@3.0.1: dependencies: ansi-regex: 2.1.1 @@ -32977,7 +34008,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.48.0 - webpack: 5.106.2(webpack-cli@5.1.4) + webpack: 5.106.2(webpack-cli@4.10.0) terser-webpack-plugin@5.5.0(webpack@5.89.0): dependencies: @@ -33012,6 +34043,8 @@ snapshots: transitivePeerDependencies: - react-native-b4a + text-extensions@1.9.0: {} + text-extensions@2.4.0: {} text-hex@1.0.0: {} @@ -33055,6 +34088,10 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + through@2.3.8: {} thunky@1.1.0: {} @@ -33154,6 +34191,8 @@ snapshots: trim-newlines@1.0.0: {} + trim-newlines@3.0.1: {} + trim-right@1.0.1: {} triple-beam@1.4.1: {} @@ -33189,6 +34228,23 @@ snapshots: '@types/jest': 27.5.2 babel-jest: 27.5.1(@babel/core@7.29.0) + ts-jest@29.1.1(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)))(typescript@5.7.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.7.3)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.3 + typescript: 5.7.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + ts-jest@29.1.1(@babel/core@7.29.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@22.20.1)(ts-node@10.9.2(@types/node@22.20.1)(typescript@5.7.3)))(typescript@5.7.3): dependencies: bs-logger: 0.2.6 @@ -33226,7 +34282,7 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 29.7.0 - ts-loader@8.0.16(typescript@5.7.3)(webpack@5.104.1(webpack-cli@6.0.1)): + ts-loader@8.0.16(typescript@5.7.3)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: chalk: 4.1.2 enhanced-resolve: 4.5.0 @@ -33234,9 +34290,9 @@ snapshots: micromatch: 4.0.8 semver: 7.6.3 typescript: 5.7.3 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) - ts-loader@8.4.0(typescript@5.7.3)(webpack@5.104.1(webpack-cli@6.0.1)): + ts-loader@8.4.0(typescript@5.7.3)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: chalk: 4.1.2 enhanced-resolve: 4.5.0 @@ -33244,16 +34300,16 @@ snapshots: micromatch: 4.0.8 semver: 7.6.3 typescript: 5.7.3 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) - ts-loader@9.4.2(typescript@5.7.3)(webpack@5.104.1(webpack-cli@6.0.1)): + ts-loader@9.4.2(typescript@5.7.3)(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: chalk: 4.1.2 enhanced-resolve: 5.21.1 micromatch: 4.0.8 semver: 7.6.3 typescript: 5.7.3 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) ts-loader@9.6.2(loader-utils@2.0.4)(typescript@5.7.3)(webpack@5.106.2): dependencies: @@ -33261,7 +34317,7 @@ snapshots: picomatch: 4.0.4 source-map: 0.7.6 typescript: 5.7.3 - webpack: 5.106.2(webpack-cli@5.1.4) + webpack: 5.106.2(webpack-cli@4.10.0) optionalDependencies: loader-utils: 2.0.4 @@ -33369,6 +34425,8 @@ snapshots: type-detect@4.1.0: {} + type-fest@0.18.1: {} + type-fest@0.20.2: {} type-fest@0.21.3: {} @@ -33446,6 +34504,8 @@ snapshots: dependencies: is-typedarray: 1.0.0 + typedarray@0.0.6: {} + typescript@4.9.5: {} typescript@5.7.3: {} @@ -33727,14 +34787,14 @@ snapshots: dependencies: vite: 6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3) - vite-plugin-vuetify@2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.4.3(typescript@5.7.3))(vuetify@3.4.9): + vite-plugin-vuetify@2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3))(vuetify@3.4.9): dependencies: - '@vuetify/loader-shared': 2.1.2(vue@3.4.3(typescript@5.7.3))(vuetify@3.4.9) + '@vuetify/loader-shared': 2.1.2(vue@3.5.34(typescript@5.7.3))(vuetify@3.4.9) debug: 4.4.3 upath: 2.0.1 vite: 5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0) - vue: 3.4.3(typescript@5.7.3) - vuetify: 3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.4.3(typescript@5.7.3)) + vue: 3.5.34(typescript@5.7.3) + vuetify: 3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.5.34(typescript@5.7.3)) transitivePeerDependencies: - supports-color @@ -33806,6 +34866,22 @@ snapshots: sass: 1.68.0 terser: 5.48.0 + vite@6.3.4(@types/node@14.18.63)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3): + dependencies: + esbuild: 0.25.5 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.63.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 14.18.63 + fsevents: 2.3.3 + jiti: 1.21.6 + sass: 1.104.0 + terser: 5.48.0 + yaml: 2.8.3 + vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3): dependencies: esbuild: 0.25.5 @@ -34048,15 +35124,25 @@ snapshots: - walrus - whiskers - vue-loader@17.4.2(@vue/compiler-sfc@3.4.3)(vue@3.4.3(typescript@5.7.3))(webpack@5.104.1(webpack-cli@6.0.1)): + vue-loader@17.2.2(@vue/compiler-sfc@3.3.8)(vue@3.5.34(typescript@5.7.3))(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: chalk: 4.1.2 hash-sum: 2.0.0 watchpack: 2.5.1 - webpack: 5.104.1(webpack-cli@6.0.1) + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) + optionalDependencies: + '@vue/compiler-sfc': 3.3.8 + vue: 3.5.34(typescript@5.7.3) + + vue-loader@17.4.2(@vue/compiler-sfc@3.4.3)(vue@3.5.34(typescript@5.7.3))(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): + dependencies: + chalk: 4.1.2 + hash-sum: 2.0.0 + watchpack: 2.5.1 + webpack: 5.106.2(webpack-cli@6.0.1(webpack@5.104.1)) optionalDependencies: '@vue/compiler-sfc': 3.4.3 - vue: 3.4.3(typescript@5.7.3) + vue: 3.5.34(typescript@5.7.3) vue-loader@17.4.2(@vue/compiler-sfc@3.5.34)(vue@3.5.34(typescript@5.7.3))(webpack@5.106.2(webpack-cli@6.0.1(webpack@5.104.1))): dependencies: @@ -34068,9 +35154,9 @@ snapshots: '@vue/compiler-sfc': 3.5.34 vue: 3.5.34(typescript@5.7.3) - vue-loading-overlay@6.0.3(vue@3.4.3(typescript@5.7.3)): + vue-loading-overlay@6.0.3(vue@3.5.34(typescript@5.7.3)): dependencies: - vue: 3.4.3(typescript@5.7.3) + vue: 3.5.34(typescript@5.7.3) vue-loading-overlay@6.0.6(vue@3.5.34(typescript@5.7.3)): dependencies: @@ -34088,16 +35174,6 @@ snapshots: vue-template-es2015-compiler@1.9.1: {} - vue@3.4.3(typescript@5.7.3): - dependencies: - '@vue/compiler-dom': 3.4.3 - '@vue/compiler-sfc': 3.4.3 - '@vue/runtime-dom': 3.4.3 - '@vue/server-renderer': 3.4.3(vue@3.4.3(typescript@5.7.3)) - '@vue/shared': 3.4.3 - optionalDependencies: - typescript: 5.7.3 - vue@3.5.34(typescript@5.7.3): dependencies: '@vue/compiler-dom': 3.5.34 @@ -34113,7 +35189,7 @@ snapshots: vue: 3.5.34(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 - vite-plugin-vuetify: 2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.4.3(typescript@5.7.3))(vuetify@3.4.9) + vite-plugin-vuetify: 2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3))(vuetify@3.4.9) vuetify@3.12.6(typescript@5.7.3)(vite-plugin-vuetify@2.1.3(vite@4.5.11(@types/node@22.20.1)(sass@1.56.1)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6))(vue@3.5.34(typescript@5.7.3)): dependencies: @@ -34144,12 +35220,12 @@ snapshots: typescript: 5.7.3 vite-plugin-vuetify: 2.1.3(vite@6.3.4(@types/node@22.20.1)(jiti@1.21.6)(sass@1.104.0)(terser@5.48.0)(yaml@2.8.3))(vue@3.5.34(typescript@5.7.3))(vuetify@3.12.6) - vuetify@3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.4.3(typescript@5.7.3)): + vuetify@3.4.9(typescript@5.7.3)(vite-plugin-vuetify@2.0.1)(vue@3.5.34(typescript@5.7.3)): dependencies: - vue: 3.4.3(typescript@5.7.3) + vue: 3.5.34(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 - vite-plugin-vuetify: 2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.4.3(typescript@5.7.3))(vuetify@3.4.9) + vite-plugin-vuetify: 2.0.1(vite@5.0.10(@types/node@22.20.1)(sass@1.68.0)(terser@5.48.0))(vue@3.5.34(typescript@5.7.3))(vuetify@3.4.9) w3c-hr-time@1.0.2: dependencies: @@ -34219,6 +35295,22 @@ snapshots: deepmerge: 1.5.2 javascript-stringify: 2.1.0 + webpack-cli@4.10.0(webpack@5.106.2): + dependencies: + '@discoveryjs/json-ext': 0.5.7 + '@webpack-cli/configtest': 1.2.0(webpack-cli@4.10.0)(webpack@5.106.2) + '@webpack-cli/info': 1.5.0(webpack-cli@4.10.0) + '@webpack-cli/serve': 1.7.0(webpack-cli@4.10.0) + colorette: 2.0.16 + commander: 7.2.0 + cross-spawn: 7.0.6 + fastest-levenshtein: 1.0.12 + import-local: 3.0.3 + interpret: 2.2.0 + rechoir: 0.7.1 + webpack: 5.106.2(webpack-cli@4.10.0) + webpack-merge: 5.10.0 + webpack-cli@5.1.4(webpack@5.106.2): dependencies: '@discoveryjs/json-ext': 0.5.7 @@ -34387,6 +35479,39 @@ snapshots: - esbuild - uglify-js + webpack@5.106.2(webpack-cli@4.10.0): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.21.1 + es-module-lexer: 2.1.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.5.0(webpack@5.106.2) + watchpack: 2.5.1 + webpack-sources: 3.4.1 + optionalDependencies: + webpack-cli: 4.10.0(webpack@5.106.2) + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.106.2(webpack-cli@5.1.4): dependencies: '@types/eslint-scope': 3.7.7 @@ -34679,6 +35804,8 @@ snapshots: wordwrap@1.0.0: {} + workerpool@6.2.0: {} + workerpool@6.5.1: {} wrap-ansi@3.0.1: @@ -34702,7 +35829,7 @@ snapshots: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 - strip-ansi: 7.2.0 + strip-ansi: 7.1.0 wrappy@1.0.2: {} @@ -34727,11 +35854,6 @@ snapshots: bufferutil: 4.1.0 utf-8-validate: 5.0.10 - ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10): - optionalDependencies: - bufferutil: 4.1.0 - utf-8-validate: 5.0.10 - ws@8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.1.0 @@ -34806,6 +35928,8 @@ snapshots: camelcase: 5.3.1 decamelize: 1.2.0 + yargs-parser@20.2.4: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} @@ -34921,7 +36045,7 @@ snapshots: - supports-color optional: true - yeoman-environment@3.19.3(@types/node@22.20.1): + yeoman-environment@3.19.3(@types/node@14.18.63): dependencies: '@npmcli/arborist': 4.3.1 are-we-there-yet: 2.0.0 @@ -34939,7 +36063,7 @@ snapshots: find-up: 5.0.0 globby: 11.1.0 grouped-queue: 2.1.0 - inquirer: 8.2.7(@types/node@22.20.1) + inquirer: 8.2.7(@types/node@14.18.63) is-scoped: 2.1.0 isbinaryfile: 4.0.10 lodash: 4.18.1 @@ -35216,33 +36340,6 @@ snapshots: - react-native-b4a - supports-color - yeoman-generator@8.2.2(@types/node@22.20.1)(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4): - dependencies: - '@types/debug': 4.1.13 - '@types/lodash-es': 4.17.12 - '@yeoman/namespace': 2.1.0 - '@yeoman/types': 1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4) - chalk: 5.6.2 - debug: 4.4.3 - execa: 9.6.1 - latest-version: 9.0.0 - lodash-es: 4.18.1 - mem-fs: 4.1.4 - mem-fs-editor: 12.0.4(@types/node@22.20.1)(mem-fs@4.1.4) - minimist: 1.2.8 - read-package-up: 12.0.0 - semver: 7.8.1 - simple-git: 3.36.0 - sort-keys: 6.0.1 - text-table: 0.2.0 - type-fest: 5.8.0 - optionalDependencies: - '@types/node': 22.20.1 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - supports-color - yn@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d237d6999..2818eb3f7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,7 @@ overrides: 'mem-fs-editor': '12.0.4' 'yeoman-environment@3>mem-fs': '2.3.0' 'yeoman-environment@3>mem-fs-editor': '9.7.0' + vue: '3.5.34' allowBuilds: '@parcel/watcher': true '@vscode/vsce-sign': true diff --git a/projects/cloud-foundry-tools/.gitignore b/projects/cloud-foundry-tools/.gitignore new file mode 100644 index 000000000..b7fb87f98 --- /dev/null +++ b/projects/cloud-foundry-tools/.gitignore @@ -0,0 +1,4 @@ +# Build + coverage output (redirected here so extension packages stay out of the root merged gate) +**/reports +# Compiled TypeScript output (backend/wrapper compile to out/ before webpack bundles to dist/) +**/out diff --git a/projects/cloud-foundry-tools/README.md b/projects/cloud-foundry-tools/README.md new file mode 100644 index 000000000..aecd25e05 --- /dev/null +++ b/projects/cloud-foundry-tools/README.md @@ -0,0 +1,20 @@ +# Cloud Foundry Tools + +This project, migrated from [SAP/cloud-foundry-tools](https://github.com/SAP/cloud-foundry-tools), contains the VS Code extension that provides tools for easy application development with Cloud Foundry (login, create service, bind service, unbind service, and more) in [SAP Business Application Studio][sap bas] and [VS Code][vscode]. + +## Packages + +- [packages/backend](./packages/backend) — `vscode-cf-tools`: The Cloud Foundry Tools VS Code extension (VSIX). +- [packages/frontend](./packages/frontend) — `cloud-foundry-tools-frontend`: Vue 3 frontend bundled into the extension. +- [packages/wrapper](./packages/wrapper) — `vscode-wing-cf-tools`: Deprecated wrapper extension kept for backward compatibility; redirects users to `vscode-cf-tools`. + +## Support + +Open an [issue](https://github.com/SAP/app-studio-toolkit/issues) on GitHub. + +## Contributing + +See [CONTRIBUTING.md](../../CONTRIBUTING.md). + +[sap bas]: https://help.sap.com/viewer/product/SAP%20Business%20Application%20Studio/Cloud/en-US +[vscode]: https://code.visualstudio.com/ diff --git a/projects/cloud-foundry-tools/packages/backend/.mocharc.js b/projects/cloud-foundry-tools/packages/backend/.mocharc.js new file mode 100644 index 000000000..ff3566669 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/.mocharc.js @@ -0,0 +1,5 @@ +module.exports = { + require: ["source-map-support/register"], + spec: "./out/tests/**/*.spec.js", + timeout: 80000, +}; diff --git a/projects/cloud-foundry-tools/packages/backend/.vscodeignore b/projects/cloud-foundry-tools/packages/backend/.vscodeignore new file mode 100644 index 000000000..8e1acee11 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/.vscodeignore @@ -0,0 +1,9 @@ +# ignore all files: +**/** + +# except for: +!resources +!dist +!LICENSE +!README.md +!package.json \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/backend/CHANGELOG.md b/projects/cloud-foundry-tools/packages/backend/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/projects/cloud-foundry-tools/packages/backend/CHANGELOG.old.md b/projects/cloud-foundry-tools/packages/backend/CHANGELOG.old.md new file mode 100644 index 000000000..8ebb62c95 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/CHANGELOG.old.md @@ -0,0 +1,162 @@ +# Historic Changelog + +> This file contains the changelog history from the standalone [SAP/cloud-foundry-tools](https://github.com/SAP/cloud-foundry-tools) repository, prior to migration into this monorepo. New changes are recorded in this package's `CHANGELOG.md`. + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [8.1.2](https://github.com/SAP/cloud-foundry-tools/compare/v8.1.0...v8.1.2) (2026-06-21) + +### Bug Fixes + +- fall back to direct CF lookup when UPS not found in list ([#311](https://github.com/SAP/cloud-foundry-tools/issues/311)) ([4086b95](https://github.com/SAP/cloud-foundry-tools/commit/4086b95aadcd60b740b1ddb47d378fd2d2ba9fcc)) + +## [8.1.1](https://github.com/SAP/cloud-foundry-tools/compare/v8.1.0...v8.1.1) (2026-06-21) + +### Bug Fixes + +- fall back to direct CF lookup when UPS not found in list ([#311](https://github.com/SAP/cloud-foundry-tools/issues/311)) ([4086b95](https://github.com/SAP/cloud-foundry-tools/commit/4086b95aadcd60b740b1ddb47d378fd2d2ba9fcc)) + +# [8.1.0](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.2...v8.1.0) (2026-02-11) + +### Bug Fixes + +- format ([ffab236](https://github.com/SAP/cloud-foundry-tools/commit/ffab236413168d63f783be9e8f484cb27c0289fb)) + +### Features + +- update lodash dependencies ([e2fd273](https://github.com/SAP/cloud-foundry-tools/commit/e2fd273a9090dda7f54eece3fcac6b00ffac0493)) + +## [8.0.2](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.1...v8.0.2) (2025-12-09) + +**Note:** Version bump only for package vscode-cf-tools + +## [8.0.1](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.0...v8.0.1) (2025-08-24) + +**Note:** Version bump only for package vscode-cf-tools + +# [8.0.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.5.2...v8.0.0) (2025-08-20) + +### Features + +- package name and publisher are changed ([#304](https://github.com/sap-staging/cloud-foundry-tools/issues/304)) ([33e586d](https://github.com/sap-staging/cloud-foundry-tools/commit/33e586d67270602128d3c4c17179ac632a7c82a5)) + +### BREAKING CHANGES + +- VS Code Marketplace Publishing Support + +## [7.5.2](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.5.1...v7.5.2) (2025-08-06) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.5.1](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.5.0...v7.5.1) (2025-06-24) + +**Note:** Version bump only for package vscode-wing-cf-tools + +# [7.5.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.4.5...v7.5.0) (2025-02-10) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.4.5](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.4.4...v7.4.5) (2024-10-14) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.4.4](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.4.3...v7.4.4) (2024-05-30) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.4.3](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.4.2...v7.4.3) (2024-05-05) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.4.2](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.4.1...v7.4.2) (2023-11-26) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.4.1](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.4.0...v7.4.1) (2023-11-12) + +**Note:** Version bump only for package vscode-wing-cf-tools + +# [7.4.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.3.4...v7.4.0) (2023-10-03) + +### Features + +- migrate to vite.js and upgrade deps ([#271](https://github.com/sap-staging/cloud-foundry-tools/issues/271)) ([6e4381c](https://github.com/sap-staging/cloud-foundry-tools/commit/6e4381cd889da81424da5ce0e11bd5b78b15121c)) +- migrating to Vue3 ([#258](https://github.com/sap-staging/cloud-foundry-tools/issues/258)) ([1bbe976](https://github.com/sap-staging/cloud-foundry-tools/commit/1bbe9764f3af8989d15cd5ba48cb706fc97c83d3)) + +## [7.3.4](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.3.3...v7.3.4) (2023-08-09) + +### Bug Fixes + +- update bas-sdk version ([ade94ba](https://github.com/sap-staging/cloud-foundry-tools/commit/ade94bae6f6992047bfb8e98a9594eb045033cae)) + +## [7.3.3](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.3.2...v7.3.3) (2023-07-09) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.3.2](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.3.1...v7.3.2) (2023-07-06) + +### Bug Fixes + +- update bas-sdk to 3.4.2 version ([a4f721d](https://github.com/sap-staging/cloud-foundry-tools/commit/a4f721dac7a1749b7ad60583114109b411afab5f)) + +## [7.3.1](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.3.0...v7.3.1) (2023-07-04) + +**Note:** Version bump only for package vscode-wing-cf-tools + +# [7.3.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.2.2...v7.3.0) (2023-07-04) + +### Features + +- support new functionality of bas-sdk cfendpoint api ([#260](https://github.com/sap-staging/cloud-foundry-tools/issues/260)) ([071fc6c](https://github.com/sap-staging/cloud-foundry-tools/commit/071fc6cd199c5177017daeada712a7ebb70582ca)), closes [#259](https://github.com/sap-staging/cloud-foundry-tools/issues/259) + +## [7.2.2](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.2.1...v7.2.2) (2023-06-29) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [7.2.1](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.2.0...v7.2.1) (2023-02-15) + +**Note:** Version bump only for package vscode-wing-cf-tools + +# [7.2.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.1.1...v7.2.0) (2023-02-15) + +### Features + +- support bind/unbind with quoted VCAP_SERVICES ([#253](https://github.com/sap-staging/cloud-foundry-tools/issues/253)) ([6cabac6](https://github.com/sap-staging/cloud-foundry-tools/commit/6cabac6a5c5b42435d86a90ce2e78c0f942db55a)) + +## [7.1.1](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.1.0...v7.1.1) (2022-11-16) + +**Note:** Version bump only for package vscode-wing-cf-tools + +# [7.1.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.0.4...v7.1.0) (2022-11-16) + +### Bug Fixes + +- make dropdown control searchable ([#248](https://github.com/sap-staging/cloud-foundry-tools/issues/248)) ([126d27b](https://github.com/sap-staging/cloud-foundry-tools/commit/126d27b7c24da77fc91bf3fac7b59e771a9e5c74)) + +## [7.0.4](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.0.1...v7.0.4) (2022-11-03) + +### Bug Fixes + +- [DEVXBUGS-10581] CF sign doesn't detect the last used API endpoint ([#243](https://github.com/sap-staging/cloud-foundry-tools/issues/243)) ([a2b7c93](https://github.com/sap-staging/cloud-foundry-tools/commit/a2b7c93b24db17bd292d6b30dfc3310f67680d47)) +- adding tooltips to target ([e4f53e4](https://github.com/sap-staging/cloud-foundry-tools/commit/e4f53e4009971ef61f1b5305fcba4b784fb3d5dd)) +- bugs ([dc5d257](https://github.com/sap-staging/cloud-foundry-tools/commit/dc5d2574c86d1f9b1bca01ea44983c1e97151988)) +- panel should dispose in any case ([a5d69fd](https://github.com/sap-staging/cloud-foundry-tools/commit/a5d69fdddd0950f2af584b299865116b27f0d603)) +- remove the important from the style ([a426e5c](https://github.com/sap-staging/cloud-foundry-tools/commit/a426e5cb88df128c623c08dc4cedfcc98d9376a6)) + +## [7.0.1](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.0.0...v7.0.1) (2022-06-29) + +### Bug Fixes + +- fix login from targets view ([#227](https://github.com/sap-staging/cloud-foundry-tools/issues/227)) ([3b9523d](https://github.com/sap-staging/cloud-foundry-tools/commit/3b9523d8d01551b423699acae5d7db429feec84b)) +- leftovers ([#225](https://github.com/sap-staging/cloud-foundry-tools/issues/225)) ([c6cedcb](https://github.com/sap-staging/cloud-foundry-tools/commit/c6cedcb0a67f40f3040d258b442128cb19fc70cf)) + +# [7.0.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v6.5.0...v7.0.0) (2022-06-28) + +### Features + +- support 2fa in the cf tools ui ([#224](https://github.com/sap-staging/cloud-foundry-tools/issues/224)) ([5ed4d20](https://github.com/sap-staging/cloud-foundry-tools/commit/5ed4d20a88b86df180cf98db3dbf615e5e20ddda)) + +## [6.5.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v6.4.2...v6.5.0) (2022-01-30) diff --git a/projects/cloud-foundry-tools/packages/backend/README.md b/projects/cloud-foundry-tools/packages/backend/README.md new file mode 100644 index 000000000..8d6dedf21 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/README.md @@ -0,0 +1,63 @@ +# Cloud Foundry Tools + +This VS Code extension provides tools for easy application development with Cloud Foundry. + +## Features + +- Login to Cloud Foundry +- Create, bind, and unbind services +- Adds a new View-Container in VS Code titled **Cloud Foundry** +- Provides commands for Cloud Foundry development and management + +--- + +## Table of Contents + +- [Features](#features) +- [Prerequisite Dependencies](#prerequisite-dependencies) +- [How to Run Locally](#how-to-run-locally) +- [Logger Preferences](#logger-preferences) +- [Known Issues](#known-issues) +- [Contributing](#contributing) + +--- + +## Prerequisite Dependencies + +- [cf-cli v8.7.4](https://github.com/cloudfoundry/cli/releases/tag/v8.7.4) +- [cf-cli targets plugin](https://github.com/guidowb/cf-targets-plugin) +- `cf-service-info` (cf plugin for resource binding functionality): + + ```sh + npm install -g '@sap/cf-tools-local' + ``` + +--- + +## How to Run Locally + +1. Clone the repository +2. Open it in VS Code +3. Run `yarn` and then `yarn ci:artifacts_only` +4. Launch the extension using the 'Run Extension' launch configuration +5. Access available commands via the Command Palette (prefix: `CF`) + +--- + +## Logger Preferences + +You can configure logger preferences in the extension settings: + +![Screenshot showing the settings of the Cloud Foundry Tools extension](https://raw.githubusercontent.com/SAP/app-studio-toolkit/main/projects/cloud-foundry-tools/packages/backend/media/settings.png) + +--- + +## Known Issues + +- Improved failure handling and cancellation options for long-running steps are needed. + +--- + +## Contributing + +See [CONTRIBUTING.md](https://github.com/SAP/app-studio-toolkit/blob/main/CONTRIBUTING.md) for contribution guidelines. diff --git a/projects/cloud-foundry-tools/packages/backend/media/run-ext.png b/projects/cloud-foundry-tools/packages/backend/media/run-ext.png new file mode 100644 index 000000000..3cbfc3a3e Binary files /dev/null and b/projects/cloud-foundry-tools/packages/backend/media/run-ext.png differ diff --git a/projects/cloud-foundry-tools/packages/backend/media/settings.png b/projects/cloud-foundry-tools/packages/backend/media/settings.png new file mode 100644 index 000000000..92456750f Binary files /dev/null and b/projects/cloud-foundry-tools/packages/backend/media/settings.png differ diff --git a/projects/cloud-foundry-tools/packages/backend/nyc.config.js b/projects/cloud-foundry-tools/packages/backend/nyc.config.js new file mode 100644 index 000000000..5c498b868 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/nyc.config.js @@ -0,0 +1,23 @@ +module.exports = { + reporter: ["text", "lcov"], + "check-coverage": true, + all: true, + extension: [".js", ".ts", ".vue"], + // Redirect coverage output away from the default .nyc_output so this VS Code + // extension is NOT pulled into the root merged 100% gate (which targets the + // library packages). It keeps its own realistic thresholds, matching the + // guided-development / yeoman-ui extension-backend precedent. + "temp-dir": "./reports/.nyc_output", + "report-dir": "./reports/coverage", + branches: 90, + lines: 95, + functions: 91, + statements: 95, + include: ["**/src/**"], + exclude: [ + "**/src/run-configuration-*", + "**/src/sql-tools.ts", + "**/src/logger/logger-wrapper.ts", + "**/usage/*.ts", + ], +}; diff --git a/projects/cloud-foundry-tools/packages/backend/package.json b/projects/cloud-foundry-tools/packages/backend/package.json new file mode 100644 index 000000000..d8699bf93 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/package.json @@ -0,0 +1,365 @@ +{ + "name": "vscode-cf-tools", + "displayName": "Cloud Foundry Tools", + "version": "8.1.2", + "private": true, + "description": "Vscode extension of Cloud Foundry tools", + "categories": [ + "Other" + ], + "bugs": { + "url": "https://github.com/SAP/app-studio-toolkit/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/SAP/app-studio-toolkit.git", + "directory": "projects/cloud-foundry-tools/packages/backend" + }, + "license": "Apache 2.0", + "publisher": "SAPOSS", + "main": "./dist/extension.js", + "scripts": { + "build": "npm-run-all clean compile webpack-dev frontend:all", + "bundle": "webpack --mode production", + "ci": "npm-run-all clean compile copy:test-resources coverage bundle frontend:all package coverage:copy", + "ci:artifacts_only": "npm-run-all clean compile webpack-dev frontend:all package", + "clean": "npm-run-all clean:dist clean:out clean:vsix clean:reports", + "clean:dist": "rimraf ./dist", + "clean:frontend": "cd dist/ && shx rm -rf ./media", + "clean:out": "rimraf ./out", + "clean:vsix": "rimraf ./*.vsix", + "compile": "tsc -p ./", + "copy:test-resources": "shx cp -r ./tests/resources ./out/tests/resources", + "coverage": "nyc mocha --colors", + "frontend:all": "cd ../frontend && npm-run-all clean build && cd ../backend && npm run frontend:copy", + "frontend:copy": "npm-run-all clean:frontend && shx cp -r ../frontend/dist/. ./dist/media/", + "package": "vsce package --no-dependencies", + "test": "mocha --colors", + "watch": "tsc -watch -p ./", + "webpack-dev": "webpack --mode development", + "webpack-dev:watch": "webpack --mode development --watch", + "clean:reports": "rimraf ./reports", + "coverage:copy": "shx mkdir -p ../../../../coverage && shx cp -u ./reports/coverage/lcov.info ../../../../coverage/lcov_cf_backend.info" + }, + "contributes": { + "commands": [ + { + "command": "cf.login", + "category": "CF", + "title": "Login to Cloud Foundry", + "icon": { + "light": "resources/light/login.svg", + "dark": "resources/dark/login.svg" + } + }, + { + "command": "cf.login.weak", + "category": "CF", + "title": "Login to Cloud Foundry", + "icon": { + "light": "resources/light/login.svg", + "dark": "resources/dark/login.svg" + } + }, + { + "command": "cf.set.orgspace", + "category": "CF", + "title": "Set Org and Space" + }, + { + "command": "cf.select.space", + "category": "CF", + "title": "Select a space from your allowed spaces" + }, + { + "command": "cf.targets.create", + "category": "CF", + "title": "Create a Cloud Foundry target", + "icon": { + "light": "resources/light/add.svg", + "dark": "resources/dark/add.svg" + } + }, + { + "command": "cf.targets.reload", + "category": "CF", + "title": "Reload Targets Tree", + "icon": { + "light": "resources/light/reload.svg", + "dark": "resources/dark/reload.svg" + } + }, + { + "command": "cf.target.set", + "category": "CF", + "title": "Set Cloud Foundry Target", + "icon": { + "light": "resources/light/set-target.svg", + "dark": "resources/dark/set-target.svg" + } + }, + { + "command": "cf.target.delete", + "category": "CF", + "title": "Delete Cloud Foundry Target" + }, + { + "command": "cf.services.bind.local", + "category": "CF", + "title": "Bind a service to a locally run application" + }, + { + "command": "cf.services.unbind", + "title": "Unbind a service from a locally run application - (Hidden from command palette)" + }, + { + "command": "cf.services.bind", + "title": "Bind a service to a locally run application - (Hidden from command palette)" + }, + { + "command": "cf.services.binding.state", + "title": "Get bind state to a service - (Hidden from command palette)" + }, + { + "command": "cf.services.create", + "category": "CF", + "title": "Create a service instance", + "icon": { + "light": "resources/light/add.svg", + "dark": "resources/dark/add.svg" + } + }, + { + "command": "cf.ups.create", + "category": "CF", + "title": "Create a user provided service instance" + }, + { + "command": "cf.deploy-service.api", + "category": "CF", + "title": "Make a rest API call to the Deploy Service" + }, + { + "command": "cf.services.get-space-services", + "category": "CF", + "title": "Fetch available services (and ups) from particular space" + }, + { + "command": "cf.services.get-ups-services", + "category": "CF", + "title": "Fetch available User Provided services from particular space" + }, + { + "command": "cf.services.get-services", + "category": "CF", + "title": "Fetch available services from particular space" + } + ], + "configuration": { + "type": "object", + "title": "CloudFoundryTools", + "properties": { + "CloudFoundryTools.logging.loggingLevel": { + "type": "string", + "enum": [ + "off", + "fatal", + "error", + "warn", + "info", + "debug", + "trace" + ], + "default": "error", + "description": "The verbosity of logging. The Order is None < fatal < error < warn < info < debug < trace.", + "scope": "resource" + }, + "CloudFoundryTools.logging.sourceLocationTracking": { + "type": "boolean", + "default": false, + "description": "Should Source Code Location Info be added to log entries, DANGER - May be very slow, only use in debugging scenarios", + "scope": "resource" + }, + "CloudFoundryTools.showTargetInformation": { + "type": "boolean", + "default": false, + "description": "Display the current Cloud Foundry target information in the status bar", + "scope": "resource" + } + } + }, + "menus": { + "commandPalette": [ + { + "when": "false", + "command": "cf.login.weak" + }, + { + "when": "false", + "command": "cf.target.delete" + }, + { + "when": "false", + "command": "cf.targets.create" + }, + { + "when": "false", + "command": "cf.deploy-service.api" + }, + { + "when": "false", + "command": "cf.target.set" + }, + { + "when": "false", + "command": "cf.services.unbind" + }, + { + "when": "false", + "command": "cf.services.bind" + }, + { + "when": "false", + "command": "cf.services.binding.state" + }, + { + "when": "false", + "command": "cf.services.get-space-services" + }, + { + "when": "false", + "command": "cf.services.get-ups-services" + }, + { + "when": "false", + "command": "cf.services.get-services" + } + ], + "view/title": [ + { + "command": "cf.targets.create", + "when": "view == cfView", + "group": "navigation@1" + }, + { + "command": "cf.targets.reload", + "when": "view == cfView", + "group": "navigation@2" + } + ], + "view/item/context": [ + { + "command": "cf.services.create", + "when": "view == cfView && viewItem =~ /^services-active$/", + "group": "inline" + }, + { + "command": "cf.login", + "when": "view == cfView && viewItem == cf-login-required", + "group": "inline" + }, + { + "command": "cf.services.bind.local", + "when": "view == cfView && viewItem == cf-service" + }, + { + "command": "cf.target.set", + "when": "view == cfView && viewItem =~ /^cf-target$|^cf-target-not-current$/", + "group": "inline" + }, + { + "command": "cf.target.set", + "when": "view == cfView && viewItem =~ /^cf-target$/", + "group": "appearance@1" + }, + { + "command": "cf.target.delete", + "when": "view == cfView && viewItem =~ /^cf-target(-active)?$/", + "group": "appearance@2" + } + ] + }, + "problemMatchers": [ + { + "name": "chisel-client", + "pattern": [ + { + "regexp": "!!!DO NOT MATCH ME!!!", + "file": 1, + "location": 2, + "message": 3 + } + ], + "background": { + "activeOnStart": true, + "beginsPattern": "^.+client: Connecting.+", + "endsPattern": "^.+client: Connected.+" + } + } + ], + "views": { + "cf-view": [ + { + "id": "cfView", + "name": "Targets" + } + ] + }, + "viewsContainers": { + "activitybar": [ + { + "id": "cf-view", + "title": "Cloud Foundry", + "icon": "resources/common/cf.svg" + } + ] + } + }, + "activationEvents": [ + "onStartupFinished" + ], + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "dependencies": { + "@sap-devx/webview-rpc": "0.4.1", + "@sap/bas-sdk": "^3.13.2", + "@sap/cf-tools": "^3.2.2", + "@sap/swa-for-sapbas-vsx": "^2.0.7", + "@sap/wing-run-config-types": "^4.2.0", + "@vscode-logging/types": "2.0.0", + "@vscode-logging/wrapper": "2.0.0", + "cheerio": "^1.0.0", + "comment-json": "^4.2.2", + "lodash": "4.17.21", + "properties-reader": "2.3.0" + }, + "devDependencies": { + "@types/cheerio": "^0.22.35", + "@types/comment-json": "2.4.2", + "@types/node": "^14.14.6", + "@types/properties-reader": "^2.1.3", + "@types/proxyquire": "^1.3.28", + "@types/rimraf": "3.0.0", + "@types/sinon": "^17.0.3", + "@types/vscode": "^1.43.0", + "@vscode-logging/types": "^2.0.0", + "@vscode/vsce": "2.24.0", + "clean-webpack-plugin": "^4.0.0", + "mocha": "^9.2.2", + "proxyquire": "2.1.3", + "sinon": "^19.0.2", + "standard-version": "^9.5.0", + "temp-dir": "^2.0.0", + "ts-loader": "^9.2.8", + "ts-node": "^10.7.0", + "webpack": "^5.94.0", + "webpack-cli": "^4.9.2" + }, + "engines": { + "vscode": "^1.50.0" + }, + "icon": "resources/icon.png" +} diff --git a/projects/cloud-foundry-tools/packages/backend/resources/common/cf.svg b/projects/cloud-foundry-tools/packages/backend/resources/common/cf.svg new file mode 100644 index 000000000..570153e86 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/common/cf.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/backend/resources/common/login.png b/projects/cloud-foundry-tools/packages/backend/resources/common/login.png new file mode 100644 index 000000000..c1b0678bb Binary files /dev/null and b/projects/cloud-foundry-tools/packages/backend/resources/common/login.png differ diff --git a/projects/cloud-foundry-tools/packages/backend/resources/dark/add.svg b/projects/cloud-foundry-tools/packages/backend/resources/dark/add.svg new file mode 100644 index 000000000..53941283b --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/dark/add.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/dark/info.svg b/projects/cloud-foundry-tools/packages/backend/resources/dark/info.svg new file mode 100644 index 000000000..54a1b8d7b --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/dark/info.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/dark/login.svg b/projects/cloud-foundry-tools/packages/backend/resources/dark/login.svg new file mode 100644 index 000000000..36d335428 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/dark/login.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/dark/reload.svg b/projects/cloud-foundry-tools/packages/backend/resources/dark/reload.svg new file mode 100644 index 000000000..d79fdaa4e --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/dark/reload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/backend/resources/dark/set-target.svg b/projects/cloud-foundry-tools/packages/backend/resources/dark/set-target.svg new file mode 100644 index 000000000..7265ba2d2 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/dark/set-target.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/dark/target-a.svg b/projects/cloud-foundry-tools/packages/backend/resources/dark/target-a.svg new file mode 100644 index 000000000..fc8a755ca --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/dark/target-a.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/dark/target.svg b/projects/cloud-foundry-tools/packages/backend/resources/dark/target.svg new file mode 100644 index 000000000..a8ca9329a --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/dark/target.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/icon.png b/projects/cloud-foundry-tools/packages/backend/resources/icon.png new file mode 100644 index 000000000..0b8ee6692 Binary files /dev/null and b/projects/cloud-foundry-tools/packages/backend/resources/icon.png differ diff --git a/projects/cloud-foundry-tools/packages/backend/resources/light/add.svg b/projects/cloud-foundry-tools/packages/backend/resources/light/add.svg new file mode 100644 index 000000000..4830ea69a --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/light/add.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/light/info.svg b/projects/cloud-foundry-tools/packages/backend/resources/light/info.svg new file mode 100644 index 000000000..54a1b8d7b --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/light/info.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/light/login.svg b/projects/cloud-foundry-tools/packages/backend/resources/light/login.svg new file mode 100644 index 000000000..d9e9408d3 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/light/login.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/light/reload.svg b/projects/cloud-foundry-tools/packages/backend/resources/light/reload.svg new file mode 100644 index 000000000..e03457481 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/light/reload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/backend/resources/light/set-target.svg b/projects/cloud-foundry-tools/packages/backend/resources/light/set-target.svg new file mode 100644 index 000000000..9e0c1cd4c --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/light/set-target.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/light/target-a.svg b/projects/cloud-foundry-tools/packages/backend/resources/light/target-a.svg new file mode 100644 index 000000000..666928674 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/light/target-a.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/resources/light/target.svg b/projects/cloud-foundry-tools/packages/backend/resources/light/target.svg new file mode 100644 index 000000000..a8ca9329a --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/resources/light/target.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/projects/cloud-foundry-tools/packages/backend/src/cfView.ts b/projects/cloud-foundry-tools/packages/backend/src/cfView.ts new file mode 100644 index 000000000..e3599cb93 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/cfView.ts @@ -0,0 +1,260 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import * as _ from "lodash"; +import { + cfGetApps, + cfGetServiceInstancesList, + cfGetTargets, + CFTarget, +} from "@sap/cf-tools"; +let cfView: CFView; + +export type CFTreeChildNode = + | CFMessageNode + | CFService + | CFApplication + | CFFolder + | CFTargetTI; +export class CFTargetTI extends vscode.TreeItem { + public contextValue = `cf-target${ + _.includes(this.target.label, "(no targets)") + ? "-notargets" + : this.target.isCurrent + ? "-active" + : "" + }`; + + constructor(public readonly target: CFTarget) { + super( + target.label, + target.isCurrent + ? vscode.TreeItemCollapsibleState.Expanded + : vscode.TreeItemCollapsibleState.Collapsed + ); + this.tooltip = target.label; + this.iconPath = { + light: path.join( + __dirname, + "..", + "resources", + "light", + `target${this.target.isCurrent ? "-a" : ""}.svg` + ), + dark: path.join( + __dirname, + "..", + "resources", + "dark", + `target${this.target.isCurrent ? "-a" : ""}.svg` + ), + }; + } + + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#properties-overriding-accessors-and-vice-versa-is-an-error + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + get description(): string { + return `${ + _.includes(this.contextValue, "-active") ? "(Active Target)" : "" + }${this.target.isDirty ? "*" : ""}`; + } +} + +export class CFMessageNode extends vscode.TreeItem { + public iconPath = { + light: path.join(__dirname, "..", "resources", "light", "info.svg"), + dark: path.join(__dirname, "..", "resources", "dark", "info.svg"), + }; + constructor(label: string, public readonly parent: CFFolder) { + super(label, vscode.TreeItemCollapsibleState.None); + } +} + +export class CFLoginNode extends CFMessageNode { + public contextValue = "cf-login-required"; + constructor(parent: CFFolder) { + super(`Login required`, parent); + this.tooltip = `Log in to see details`; + } +} + +export class CFTargetNotCurrent extends CFMessageNode { + public contextValue = "cf-target-not-current"; + + constructor(parent: CFFolder, targetName?: string) { + super(`Target needs to be active`, parent); + this.tooltip = `Set the '${targetName}' as the active target to see details.`; + } +} + +export class CFService extends vscode.TreeItem { + public iconPath = vscode.ThemeIcon.File; + public contextValue = "cf-service"; + + constructor( + public readonly label: string, + public readonly type: string, + public readonly parent: CFFolder + ) { + super(label, vscode.TreeItemCollapsibleState.None); + } +} + +export class CFApplication extends vscode.TreeItem { + public iconPath = vscode.ThemeIcon.File; + public contextValue = "cf-application"; + + constructor( + public readonly label: string, + public readonly state: string, + public readonly parent: CFFolder + ) { + super(label, vscode.TreeItemCollapsibleState.None); + } +} + +export class CFFolder extends vscode.TreeItem { + public iconPath = vscode.ThemeIcon.Folder; + + constructor( + public readonly label: string, + public readonly parent: CFTargetTI + ) { + super(label, vscode.TreeItemCollapsibleState.Collapsed); + this.tooltip = `${label}`; + } +} + +export class CFServicesFolder extends CFFolder { + constructor(public readonly label: string, parent: CFTargetTI) { + super(label, parent); + this.contextValue = `services${parent.target.isCurrent ? "-active" : ""}`; + } +} + +export class CFAppsFolder extends CFFolder { + constructor(public readonly label: string, parent: CFTargetTI) { + super(label, parent); + this.contextValue = `apps${parent.target.isCurrent ? "-active" : ""}`; + } +} + +export class CFView implements vscode.TreeDataProvider { + public static get(): CFView { + return cfView; + } + + public readonly privateOnDidChangeTreeData: vscode.EventEmitter< + vscode.TreeItem | undefined + > = new vscode.EventEmitter(); + public readonly onDidChangeTreeData: vscode.Event< + vscode.TreeItem | undefined + > = this.privateOnDidChangeTreeData.event; + public targets?: CFTargetTI[]; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(context: vscode.ExtensionContext, configFilePath: string) { + // this.subscribeOnFileEventsAndRefresh(context, configFilePath); + this.refresh(); + cfView = this; + } + + public refresh(): void { + this.privateOnDidChangeTreeData.fire(undefined); + } + + public getParent( + element: vscode.TreeItem + ): vscode.ProviderResult { + return _.get(element, "parent") as vscode.TreeItem; + } + + public getTreeItem(element: vscode.TreeItem): Promise { + if (/^cf-(service|application)$/.test(element.contextValue || "")) { + const item = _.cloneDeep(element); + item.label = + element.contextValue === "cf-application" + ? `${element.label?.toString()} (${(element as CFApplication).state})` + : `${element.label?.toString()} (${(element as CFService).type})`; + return Promise.resolve(item); + } + return Promise.resolve(element); + } + + public async getChildren( + parent: vscode.TreeItem + ): Promise { + if (parent) { + if (parent instanceof CFTargetTI) { + return [ + new CFServicesFolder("Services", parent), + new CFAppsFolder("Applications", parent), + ]; + } else if (/^(services|apps)/.test(parent.contextValue || "")) { + if (/-active$/.test(parent.contextValue || "")) { + try { + return parent instanceof CFAppsFolder + ? /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + _.map( + await cfGetApps(), + (app) => + new CFApplication(app.name, _.get(app, "state"), parent) + ) + : _.map( + await cfGetServiceInstancesList(), + (service) => + new CFService( + service.label, + service.serviceName, + parent as CFFolder + ) + ); + } catch (e) { + return [new CFLoginNode(parent as CFFolder)]; + } + } else { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + return [ + new CFTargetNotCurrent( + parent as CFFolder, + _.get(parent, "parent.target.label") + ), + ]; + } + } + return []; + } + // return list of targets + this.targets = _.map( + await cfGetTargets(), + (target) => new CFTargetTI(target) + ); + return this.targets; + } + + public getCurrentTarget(): CFTarget | undefined { + return _.get(_.find(this.targets, "target.isCurrent"), "target"); + } + + public getTargets(): CFTargetTI[] | undefined { + return this.targets; + } + + // keep temporary as a reference for future usage + // private subscribeOnFileEventsAndRefresh(context: vscode.ExtensionContext, configFilePath: string) { + // if (_.size(configFilePath) > 0) { + // const fsw = vscode.workspace.createFileSystemWatcher(configFilePath); + // context.subscriptions.push(fsw.onDidChange(() => this.refresh())); + // context.subscriptions.push(fsw.onDidCreate(() => this.refresh())); + // context.subscriptions.push(fsw.onDidDelete(() => this.refresh())); + // } + // } +} + +export function getTargetRoot(node: CFTreeChildNode | undefined): CFTargetTI { + let item = node; + while (_.get(item, "parent")) { + item = _.get(item, "parent") as CFTreeChildNode; // walk up until target folder found + } + return item as CFTargetTI; +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/cfViewCommands.ts b/projects/cloud-foundry-tools/packages/backend/src/cfViewCommands.ts new file mode 100644 index 000000000..1532aaeed --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/cfViewCommands.ts @@ -0,0 +1,756 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import { + CFView, + CFService, + CFTargetTI, + CFTargetNotCurrent, + getTargetRoot, +} from "./cfView"; +import { messages } from "./messages"; +import * as https from "https"; +import { + updateGitIgnoreList, + isWindows, + toText, + UpsServiceQueryOprions, + ServiceQueryOptions, + resolveFilterValue, +} from "./utils"; +import { + DEFAULT_TARGET, + ServiceInstanceInfo, + IServiceQuery, + eFilters, + Cli, + cfGetConfigFileField, + cfBindLocalServices, + ServiceTypeInfo, + cfBindLocalUps, + cfGetInstanceMetadata, + cfGetAuthToken, + padQuery, + eServiceTypes, + CliResult, +} from "@sap/cf-tools"; +import * as _ from "lodash"; +import { + getAvailableServices, + updateServicesOnCFPageSize, + isServiceTypeInfoInArray, + updateInstanceNameAndTags, + getInstanceName, + fetchServicePlanList, + CMD_CREATE_SERVICE, + verifyLoginRetry, + getUserProvidedServiceInstances, + getServiceInstances, + CMD_BIND_TO_DEFAULT_SERVICE, +} from "./commands"; +import { stringify } from "comment-json"; +import { checkAndCreateChiselTask, deleteChiselParamsFromFile } from "./chisel"; +import { getModuleLogger } from "./logger/logger-wrapper"; + +const YES = "Yes"; +const NO = "No"; +const CANCEL = "Cancel"; +const LOGGER_MODULE = "cfViewCommands"; + +interface BindDetails { + instances: ServiceInstanceInfo[]; + tags?: string[]; + keyNames?: string[]; + serviceKeyParams?: unknown[]; +} + +interface BindLocalData { + instanceName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + chiselTask?: any; +} + +export interface CmdOptions { + silent?: boolean; + "skip-reload"?: boolean; + "quote-vcap"?: boolean; +} + +export function cmdReloadTargets(): Promise { + return Promise.resolve(CFView.get().refresh()); +} + +async function execHttp(options: https.RequestOptions): Promise { + return new Promise((resolve, reject) => { + https + .get(options, (resp) => { + let data = ""; + // A chunk of data has been recieved. + resp.on("data", (chunk) => { + data += chunk; + }); + // The whole response has been received. Print out the result. + resp.on("end", () => { + resolve(data); + }); + }) + .on("error", (err) => { + getModuleLogger(LOGGER_MODULE).error( + "execHttp: get failed", + { error: toText(err) }, + { host: options.host } + ); + reject(err.message); + }); + }); +} + +async function askUserForPath(): Promise { + return vscode.window.showOpenDialog({ + openLabel: "Select folder for .env file", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: vscode.workspace.workspaceFolders + ? vscode.workspace.workspaceFolders[0].uri + : undefined, + }); +} + +async function getToBindInstances( + serviceInfos: ServiceTypeInfo[], + availableServices: ServiceInstanceInfo[], + instanceNames: string[], + tags: string[] +) { + for (const serviceTypeInfo of serviceInfos) { + await updateInstanceNameAndTags( + availableServices, + serviceTypeInfo, + instanceNames, + tags + ); + } +} + +type TEnvPath = { + path: vscode.Uri; + ignore?: boolean; +}; + +type BindArgs = { + instances: ServiceInstanceInfo[]; + envPath: TEnvPath; + tags?: string[]; + serviceKeyNames?: string[]; + serviceKeyParams?: unknown[]; + options?: CmdOptions; +}; + +async function doBind(opts: BindArgs) { + async function withProgress(cb: () => Thenable, services: string[]) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }, + cb + ); + const serviceNames = _.join(services, ","); + if (!opts.options?.silent) { + void vscode.window.showInformationMessage( + messages.service_bound_successful(serviceNames) + ); + } + getModuleLogger(LOGGER_MODULE).info( + "The service %s has been bound.", + serviceNames + ); + } + + const ups = _.filter(opts.instances, [ + "serviceName", + eServiceTypes.user_provided, + ]); + const services = _.difference(opts.instances, ups); + if (_.size(services)) { + const labels = _.map(services, "label"); + const cb = () => + cfBindLocalServices( + opts.envPath.path.fsPath, + labels, + opts.tags, + opts.serviceKeyNames, + opts.serviceKeyParams, + opts.options?.["quote-vcap"] + ); + await withProgress(cb, labels); + } + if (_.size(ups)) { + const labels = _.map(ups, "label"); + const cb = () => + cfBindLocalUps( + opts.envPath.path.fsPath, + labels, + opts.tags, + opts.options?.["quote-vcap"] + ); + await withProgress(cb, labels); + } + if (!opts.envPath.ignore) { + void updateGitIgnoreList(opts.envPath.path.fsPath); + } +} + +async function getServiceInstanceInfo( + instanceName: string +): Promise { + const data = await cfGetInstanceMetadata(instanceName); + return { + label: _.get(data, "serviceName"), + serviceName: _.get(data, "service"), + plan_guid: _.get(data, "plan_guid"), + plan: _.get(data, "plan"), + }; +} + +export async function cfDeployServiceAPI(urlPath: string): Promise { + const cfApi: string = await cfGetConfigFileField("Target"); + if (_.size(cfApi) > 0) { + // https://api.cf. + const deployServiceUrl = cfApi.replace("api.cf", "deploy-service.cfapps"); + + const accessToken = (await cfGetAuthToken()) + .replace("\n", "") + .replace("\n", ""); // lgtm [js/incomplete-sanitization] + + const urlObj = new URL(deployServiceUrl); + + const options = { + protocol: urlObj.protocol, + host: urlObj.host, + port: urlObj.port ? urlObj.port : urlObj.protocol === "https:" ? 443 : 80, + method: "get", + path: urlPath ? urlPath : urlObj.pathname, + headers: { Authorization: accessToken }, + }; + + return execHttp(options); + } + + getModuleLogger(LOGGER_MODULE).error("No CF API endpoint", { path: urlPath }); + throw new Error(messages.no_cf_api_endpoint); +} + +export async function cmdDeployServiceAPI( + servicePath: string, + message: string +): Promise { + if (await verifyLoginRetry()) { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: message ? message : messages.load_data_from_deploy, + cancellable: false, + }, + () => cfDeployServiceAPI(servicePath) + ); + } +} + +export async function execSetTarget( + item: CFTargetTI, + options?: CmdOptions +): Promise { + const response: CliResult = await Cli.execute([ + "set-target", + "-f", + item.target.label, + ]); + if (response.exitCode !== 0) { + if (!options?.silent) { + void vscode.window.showErrorMessage(response.stdout); + } + getModuleLogger(LOGGER_MODULE).error( + `execSetTarget:: run 'set-target -f' with lable ${item.target.label} failed`, + { + output: response.stdout, + } + ); + } else { + if (!options?.["skip-reload"]) { + await cmdReloadTargets(); + } + } +} + +export async function execSaveTarget( + item?: CFTargetTI, + options?: CmdOptions +): Promise { + if (item?.contextValue !== "cf-target-notargets") { + const response: CliResult = await Cli.execute( + _.concat( + ["save-target"], + item?.target.label ? ["-f", item.target.label] : [] + ) + ); + if (response.exitCode !== 0) { + if (!options?.silent) { + void vscode.window.showErrorMessage(response.stdout); + } + getModuleLogger(LOGGER_MODULE).error( + `execSaveTarget:: run 'save-target -f' with lable ${item?.target.label} failed`, + { + output: response.stdout, + } + ); + } + } +} + +export async function cmdSetCurrentTarget( + newTarget: CFTargetTI | CFTargetNotCurrent | undefined +): Promise { + const item = getTargetRoot(newTarget); + if (false === item?.target?.isCurrent) { + let answer = YES; + try { + const currTarget = CFView.get().getCurrentTarget(); + if (currTarget?.isDirty) { + answer = await vscode.window + .showWarningMessage( + messages.target_dirty_save(currTarget.label), + YES, + NO, + CANCEL + ) + .then((selection) => { + if (selection === YES) { + return execSaveTarget().then(() => selection); + } else if (selection) { + return NO; + } + // if ESC was pressend and selection is undefined + return CANCEL; + }); + } + + if (answer === CANCEL) { + return; + } + + return execSetTarget(item); + } catch (e) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + void vscode.window.showErrorMessage(toText(e)); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(LOGGER_MODULE).error( + `cmdSetCurrentTargetCommand with new target ${stringify( + newTarget + )} exception thrown`, + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + { error: toText(e) } + ); + } + } +} + +export async function cmdDeleteTarget( + item: CFTargetTI, + options?: CmdOptions +): Promise { + const targetLabel = item.target.label; + if (targetLabel === DEFAULT_TARGET) { + return; + } + const cliResult = await Cli.execute(["delete-target", targetLabel]); + if (cliResult.exitCode === 0) { + if (!options?.["skip-reload"]) { + await cmdReloadTargets(); + } + if (!options?.silent) { + void vscode.window.showInformationMessage( + messages.target_deleted(targetLabel) + ); + } + getModuleLogger(LOGGER_MODULE).debug( + `cmdDeleteTarget:: command "delete-target" of ${targetLabel} succeeded.` + ); + } else { + void vscode.window.showErrorMessage(cliResult.stdout); + getModuleLogger(LOGGER_MODULE).error( + `cmdSetCurrentTargetCommand:: run 'delete-target of ${targetLabel} failed`, + { + output: cliResult.stdout, + } + ); + } +} + +export async function cmdGetSpaceServices( + query?: IServiceQuery, + progressTitle?: string +): Promise { + _.each(query?.filters, (filter) => { + filter.value = encodeURIComponent(filter.value); + }); + return getAvailableServices({ query, ups: { isShow: true } }, progressTitle); +} + +async function composeQueryToObtainInstances( + serviceInfos: ServiceTypeInfo[] +): Promise { + let query; + if (_.get(serviceInfos, ["0", "plan"])) { + query = padQuery(query as unknown as IServiceQuery, [ + { + key: eFilters.service_plan_names, + value: resolveFilterValue(serviceInfos[0].plan), + }, + ]); + } + if (/*!_.size(plans) &&*/ _.get(serviceInfos, "[0].name")) { + if (eServiceTypes.user_provided === serviceInfos[0].name) { + query = padQuery(query as unknown as IServiceQuery, [ + { key: eFilters.service_plan_names, value: "nothing-to-show" }, + ]); // tricky - to show the only user-provided instances + } else { + const guids = _.map( + await fetchServicePlanList({ + filters: [ + { + key: eFilters.service_offering_names, + value: resolveFilterValue(serviceInfos[0].name), + }, + ], + }), + "guid" + ); + query = padQuery(query as unknown as IServiceQuery, [ + { key: eFilters.service_plan_guids, value: _.join(guids) }, + ]); + } + } + return query; +} + +async function collectBindDetails( + service: CFService | ServiceTypeInfo[], + requstedInstance?: string +): Promise { + let details; + + if ( + _.get(service, "contextValue") === "cf-service" && + _.get(service, "label") + ) { + details = { + instances: [ + { + label: (service as CFService).label, + serviceName: (service as CFService).type || "unknown", + }, + ], + }; + } else { + // service is ServiceTypeInfo + const query = await composeQueryToObtainInstances( + service as ServiceTypeInfo[] + ); + let availableServices = await getAvailableServices({ + query, + ups: _.get(service, ["0", "ups"]), + }); + if (_.isEmpty(availableServices)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + getModuleLogger(LOGGER_MODULE).debug( + `No services found for plan ${_.get(service, "[0].plan")}` + ); + if (!_.find(service as ServiceTypeInfo[], "allowCreate")) { + void vscode.window.showInformationMessage( + messages.no_services_instances_found + ); + return details; + } + } + + updateServicesOnCFPageSize(availableServices); + + if (isServiceTypeInfoInArray(service)) { + const instanceNames: string[] = []; + const tags: string[] = []; + const serviceTypeInfos = service as ServiceTypeInfo[]; + if (!requstedInstance) { + for (const serviceTypeInfo of serviceTypeInfos) { + if (serviceTypeInfo.allowCreate) { + // add 'create service' menu item + availableServices = _.concat( + [{ label: CMD_CREATE_SERVICE, serviceName: "" }], + availableServices + ); + if ( + _.size(availableServices) && + !_.isEmpty(serviceTypeInfo.allowCreate.name) + ) { + // add 'Bind to the default service instance' menu item + availableServices = _.concat( + [ + { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + label: `${CMD_BIND_TO_DEFAULT_SERVICE}${serviceTypeInfo.allowCreate.name}`, + plan: serviceTypeInfo.allowCreate.plan, + serviceName: serviceTypeInfo.allowCreate.serviceName ?? "", + }, + ], + availableServices + ); + } + } + await updateInstanceNameAndTags( + availableServices, + serviceTypeInfo, + instanceNames, + tags + ); + } + } else { + let foundInstance = _.find(availableServices, [ + "label", + requstedInstance, + ]); + if (!foundInstance) { + foundInstance = await getServiceInstanceInfo(requstedInstance).catch( + () => undefined + ); + if (!foundInstance) { + throw new Error( + messages.no_services_instance_byname_found(requstedInstance) + ); + } + availableServices.push(foundInstance); + } + instanceNames.push(foundInstance.label); + tags.push(serviceTypeInfos[0].tag); + } + + if (_.size(instanceNames) > 0) { + const serviceKeyNames: string[] = _.compact( + _.flatMap( + serviceTypeInfos, + (serviceTypeInfo) => serviceTypeInfo.serviceKeyName + ) + ); + const serviceKeyParams: unknown[] = _.compact( + _.map(serviceTypeInfos, "serviceKeyParam") + ); + const instances = []; + for (const name of instanceNames) { + let serviceInstance = _.find(availableServices, ["label", name]); + // seems like it just created service and it is needed to fetch it's info + if (!serviceInstance) { + serviceInstance = await getServiceInstanceInfo(name); + } + instances.push(serviceInstance); + } + details = { + instances: _.compact(instances), + tags: _.compact(tags), + keyNames: serviceKeyNames, + serviceKeyParams, + }; + } + } else { + const instanceName = await getInstanceName(availableServices); + if (instanceName) { + details = { + instances: [{ label: instanceName, serviceName: "unknown" }], + }; + } + } + } + return details; +} + +class EnvPathHelper { + static getPath(env: vscode.Uri | TEnvPath): vscode.Uri { + return _.get(env, "fsPath") + ? (env as vscode.Uri) + : _.get(env, "path") + ? (env.path as vscode.Uri) + : vscode.Uri.file(""); + } + static getIgnore(env: vscode.Uri | TEnvPath): boolean { + return (_.get(env, "ignore") as boolean) || false; + } + static isPathEmpty(value: vscode.Uri): boolean { + return value.scheme === "file" && value.fsPath === (isWindows ? "\\" : "/"); + } +} + +export async function cmdBindLocal( + service: CFService | ServiceTypeInfo[], + envPath: vscode.Uri | TEnvPath, + instanceName?: string, + opts?: CmdOptions +): Promise { + // Handle .env path + let filePath = EnvPathHelper.getPath(envPath); + if (EnvPathHelper.isPathEmpty(filePath)) { + const uriArray = await askUserForPath(); + if (_.size(uriArray) === 0) { + // aborted + return; + } + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + filePath = vscode.Uri.file( + path.join(_.get(uriArray, "[0].fsPath"), ".env") + ); + } + try { + const bindDetails = await collectBindDetails(service, instanceName); + if (bindDetails) { + await doBind({ + instances: bindDetails.instances, + envPath: { path: filePath, ignore: EnvPathHelper.getIgnore(envPath) }, + tags: bindDetails.tags, + serviceKeyNames: bindDetails.keyNames, + serviceKeyParams: bindDetails.serviceKeyParams, + options: opts, + }); + const instanceName = _.get(_.head(bindDetails.instances), "label"); + if (instanceName) { + const chiselTask = await checkAndCreateChiselTask( + filePath.fsPath, + instanceName + ); + if (chiselTask) { + await deleteChiselParamsFromFile(filePath.fsPath); + } + return chiselTask ? { instanceName, chiselTask } : { instanceName }; + } + } + } catch (e) { + if (e) { + // login to cf canceled by user + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + void vscode.window.showErrorMessage(toText(e)); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(LOGGER_MODULE).error( + `cmdBindLocal exception thrown`, + { error: toText(new Error(e?.message as string)) }, + { service: service }, + { envPath: filePath }, + { instanceName: instanceName } + ); + } + } + return; +} + +export async function bindLocalService( + serviceInfos: ServiceTypeInfo[], + envPath: vscode.Uri | TEnvPath, + opts?: CmdOptions +): Promise { + try { + const filePath = EnvPathHelper.getPath(envPath); + if (EnvPathHelper.isPathEmpty(filePath)) { + return []; + } + const query = await composeQueryToObtainInstances(serviceInfos); + const availableServices: ServiceInstanceInfo[] = await getAvailableServices( + { query } + ); + if (_.isEmpty(availableServices)) { + if (!_.isEmpty(_.get(serviceInfos, ["0", "plan"]))) { + getModuleLogger(LOGGER_MODULE).debug( + `No service found for the plan ${serviceInfos[0].plan}` + ); + } + if (!_.find(serviceInfos, "allowCreate")) { + void vscode.window.showInformationMessage( + messages.no_services_instances_found + ); + } + return []; + } + + updateServicesOnCFPageSize(availableServices); + + if (isServiceTypeInfoInArray(serviceInfos)) { + const instanceNames: string[] = []; + const tags: string[] = []; + await getToBindInstances( + serviceInfos, + availableServices, + instanceNames, + tags + ); + const instances = _.reduce( + instanceNames, + (result, name) => { + result.push(_.find(availableServices, ["label", name])); + return result; + }, + <(ServiceInstanceInfo | undefined)[]>[] + ); + if (_.size(instanceNames) > 0) { + await doBind({ + instances: _.compact(instances), + envPath: { path: filePath, ignore: EnvPathHelper.getIgnore(envPath) }, + tags: _.compact(tags), + options: opts, + }); + return instanceNames; + } + } else { + const instanceName = await getInstanceName(availableServices); + if (instanceName) { + const service = _.find(availableServices, ["label", instanceName]); + await doBind({ + instances: service ? [service] : [], + envPath: { path: filePath, ignore: EnvPathHelper.getIgnore(envPath) }, + options: opts, + }); + return [instanceName]; + } + } + } catch (e) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + void vscode.window.showErrorMessage(toText(e)); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(LOGGER_MODULE).error( + `bindLocalService exception thrown`, + { error: toText(new Error(e?.message as string)) }, + { serviceInfos: serviceInfos }, + { envPath: EnvPathHelper.getPath(envPath) } + ); + } +} + +export async function cmdGetUpsServiceInstances( + options?: UpsServiceQueryOprions, + progressTitle?: string +): Promise { + return getUserProvidedServiceInstances(options, progressTitle); +} + +export async function cmdGetServiceInstances( + serviceQueryOptions?: ServiceQueryOptions, + progressTitle?: string +): Promise { + let query: IServiceQuery | undefined; + if (serviceQueryOptions) { + // prepare a query to filter the service list by service plan (e.g.: hdi-shared) + // filter the list by service type name (e.g.: hana, hanatrial) + const serviceInfo = { + name: serviceQueryOptions.name || "", + plan: serviceQueryOptions.plan || "", + tag: serviceQueryOptions.tag || "", + prompt: "", + }; + query = await composeQueryToObtainInstances([serviceInfo]); + } + return getServiceInstances(query, progressTitle); +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/chisel.ts b/projects/cloud-foundry-tools/packages/backend/src/chisel.ts new file mode 100644 index 000000000..303c0b6eb --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/chisel.ts @@ -0,0 +1,112 @@ +import * as _ from "lodash"; +import { getModuleLogger } from "./logger/logger-wrapper"; +import { toText, writeProperties } from "./utils"; +import * as PropertiesReader from "properties-reader"; + +const LOGGER_MODULE = "chisel"; + +//Chisel fields array +enum ChiselKeys { + CHISEL_URL = "CHISEL_URL", + CHISEL_USER = "CHISEL_USER", + CHISEL_PASSWORD = "CHISEL_PASSWORD", + TUNNEL_PARAM = "TUNNEL_PARAM", +} + +function initReader(filePath: string): PropertiesReader.Reader | undefined { + try { + return PropertiesReader(filePath); + } catch (e) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(LOGGER_MODULE).error( + "checkAndCreateChiselTask: propertiesReader : environment file is broken or not exists", + { filePath: filePath }, + { exception: toText(new Error(e?.message as string)) } + ); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function checkAndCreateChiselTask( + filePath: string, + name: string +): any | undefined { + const envProperties = initReader(filePath); + if (envProperties) { + const chiselUrl = envProperties.get(ChiselKeys.CHISEL_URL); + if (_.isEmpty(chiselUrl)) { + getModuleLogger(LOGGER_MODULE).debug( + "checkAndCreateChiselTask: empty chisel_url", + { filePath: filePath } + ); + return undefined; + } + getModuleLogger(LOGGER_MODULE).debug( + "checkAndCreateChiselTask: chisel task <%s> composed", + `openChiselTunnerFor-${name}`, + { filePath: filePath } + ); + return { + label: `openChiselTunnerFor-${name}`, + type: "shell", + command: "chisel", + isBackground: true, + problemMatcher: "$chisel-client", + args: [ + "client", + "--auth", + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${envProperties.get(ChiselKeys.CHISEL_USER)}:${envProperties.get( + ChiselKeys.CHISEL_PASSWORD + )}`, + chiselUrl, + envProperties.get(ChiselKeys.TUNNEL_PARAM), + ], + }; + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function dropChiselProperties(propObj: any): Record { + const properties: Record = {}; + _.each(_.keys(propObj), (key) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!(Object as any).values(ChiselKeys).includes(key)) { + const value = propObj[key]; + if (!_.isEmpty(value)) { + properties[key] = value; + } + } + }); + return properties; +} + +export async function deleteChiselParamsFromFile( + filePath: string +): Promise { + const envProperties = initReader(filePath); + if (!envProperties) { + return false; + } + const chiselUrl = envProperties.get(ChiselKeys.CHISEL_URL); + if (_.isEmpty(chiselUrl)) { + return false; + } + try { + await writeProperties( + filePath, + dropChiselProperties(envProperties.getAllProperties()) + ); + getModuleLogger(LOGGER_MODULE).debug( + `deleteChiselParamsFromFile: override the paramters to ${filePath} file without chisel parameters` + ); + return true; + } catch (err) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(LOGGER_MODULE).error( + `deleteChiselParamsFromFile: failed to override the paramters to ${filePath} file without chisel parameters`, + { exception: toText(new Error(err?.message as string)) } + ); + return false; + } +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/commands.ts b/projects/cloud-foundry-tools/packages/backend/src/commands.ts new file mode 100644 index 000000000..b086f8fcd --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/commands.ts @@ -0,0 +1,935 @@ +import * as vscode from "vscode"; +import { + ServiceInfo, + PlanInfo, + ServiceInstanceInfo, + CF_PAGE_SIZE, + IServiceQuery, + Cli, + CliResult, + cfGetServices, + cfCreateService, + cfGetServicePlansList, + ServiceTypeInfo, + cfGetTarget, + ITarget, + cfCreateUpsInstance, + cfGetServiceInstances, + eFilters, + eServiceTypes, + cfGetConfigFileJson, + cfSetOrgSpace, +} from "@sap/cf-tools"; +import { messages } from "./messages"; +import { cmdReloadTargets } from "./cfViewCommands"; +import * as _ from "lodash"; +import { + validateParams, + generateParams4Service, + getAllServiceInstances, + DisplayServices, + isRegexExpression, + toText, + UpsServiceQueryOprions, + getUpsServiceInstances, + resolveFilterValue, + notifyWhenServicesInfoResultIncomplete, + examCFTarget, +} from "./utils"; +import { stringify, parse } from "comment-json"; +import { getModuleLogger } from "./logger/logger-wrapper"; +import { CFLoginNode, getTargetRoot } from "./cfView"; +import { openLoginView } from "./loginTargetView/loginTargetView"; + +const OK = "OK"; +const Cancel = "Cancel"; +const MORE_RESULTS = "More results..."; +export const CMD_CREATE_SERVICE = "+ Create a new service instance"; +export const CMD_BIND_TO_DEFAULT_SERVICE = + "Bind to the default service instance: "; +const LOGGER_MODULE = "commands"; + +export function isCFResource(obj: unknown): boolean { + return ( + _.has(obj, "relationships") && _.has(obj, "links") && _.has(obj, "guid") + ); +} + +export function isServiceTypeInfoInArray(obj: unknown): boolean { + return _.isArray(obj) && _.has(obj, "[0].name"); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function invokeLongFunctionWithProgress( + longFunction: any, + progressMessage: string +): Thenable { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: progressMessage, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + }, + () => longFunction() + ); +} + +async function setCfTarget(message: string) { + let commandId: string; + let target; + const args = []; + + try { + target = await cfGetTarget(); + } catch (e) { + // not logged in ... + } + if (!target || !target.user) { + commandId = "cf.login"; + args.push(true); + } else if (!target.org) { + commandId = "cf.set.orgspace"; + } else if (!target.space) { + commandId = "cf.select.space"; + } else { + getModuleLogger(LOGGER_MODULE).error( + "setCfTarget: cfGetTarget failed", + { target: target }, + { output: message } + ); + return Promise.reject(new Error(message)); + } + + const result = await vscode.commands.executeCommand(commandId, ...args); + if (undefined === result) { + return Promise.reject(); // canceled + } + return result; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function runBaseWithProgressAndLoginRetry( + isCancelable: boolean, + titleMessage: string, + longFunction: () => Promise, + onError: (error: Error) => Promise, + args?: any, + errArgs?: any +): Promise { + while (true) { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: titleMessage, + cancellable: isCancelable, + // eslint-disable-next-line prefer-spread + }, + (progress, token) => + longFunction.apply( + null, + <[]>(_.isArray(args) ? _.concat(args, token) : [args, token]) + ) + ); + } catch (error) { + // eslint-disable-next-line prefer-spread + await onError.apply(null, <[Error]>_.concat([error], errArgs || [])); + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function onErrorSetCfTarget(error: Error): Promise { + const errorText = toText(error); + if ( + errorText.includes(messages.cf_setting_not_set) || + errorText.includes("login") || + errorText.includes("re-authenticate") + ) { + await setCfTarget(errorText); + } else { + return Promise.reject(error); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function runWithProgressAndLoginRetry( + isCancelable: boolean, + titleMessage: string, + longFunction: () => Promise, + args?: any +): Promise { + return runBaseWithProgressAndLoginRetry( + isCancelable, + titleMessage, + longFunction, + onErrorSetCfTarget, + args + ); +} + +export async function verifyLoginRetry(options?: { + weak?: boolean; +}): Promise { + let result: unknown; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result = await runWithProgressAndLoginRetry( + false, + messages.verify_cf_connectivity, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + examCFTarget as () => Promise, + [messages.cf_setting_not_set, ["user", "org", "space"], options?.weak] + ); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + getModuleLogger(LOGGER_MODULE).error( + "verifyLoginRetry failed", + { exception: toText(e) }, + { options: options } + ); + } + return result; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function onErrorCfLogin(error: Error, endPoint?: string): Promise { + const errorText = toText(error); + if (errorText.includes("login") || errorText.includes("re-authenticate")) { + const result = await cmdLogin(false, false, endPoint); + if (_.isUndefined(result)) { + return Promise.reject(""); // canceled + } + } else { + return Promise.reject(error); + } +} + +async function verifyLoginRetryPartial(opts?: { + endPoint?: string; +}): Promise { + let result: unknown; + try { + result = await runBaseWithProgressAndLoginRetry( + false, + messages.verify_cf_connectivity, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + examCFTarget as () => Promise, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onErrorCfLogin as () => Promise, + [messages.login, ["user"], false], + opts?.endPoint ? [opts.endPoint] : [] + ); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + getModuleLogger(LOGGER_MODULE).error("verifyLoginRetryPartial failed", { + exception: toText(e), + }); + } + return result; +} + +export async function cmdCFSetOrgSpace(): Promise { + try { + return await cmdLogin(false, true); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + void vscode.window.showErrorMessage(toText(e)); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + getModuleLogger(LOGGER_MODULE).error("cmdCFSetOrgSpace failed", { + exception: toText(e), + }); + return ""; + } +} + +function pickCfTargetWithProgress(): Thenable { + return vscode.window + .withProgress( + { + location: vscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + }, + () => cfGetTarget() + ) + .then( + (t) => t, + () => undefined + ); +} + +export async function cmdLogin( + weak = false, + target = true, + extEndPoint?: string, + opts?: { isSplit: boolean; isLoginOnly?: boolean } +): Promise { + try { + let endpoint: string | undefined = extEndPoint; + let space: string | undefined = ""; + let org: string | undefined = ""; + + opts = opts ?? { isSplit: true, isLoginOnly: true }; + + let result = weak + ? _.get(await pickCfTargetWithProgress(), "user") + ? OK + : undefined + : undefined; + if (!result) { + const node = weak as unknown; + if (node instanceof CFLoginNode) { + // attempt to login from targets tree + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const configJson = await cfGetConfigFileJson( + _.get(getTargetRoot(node), ["target", "label"]) + ); + if (configJson) { + endpoint = _.get(configJson, "Target"); + space = _.get(configJson, ["SpaceFields", "Name"]); + org = _.get(configJson, ["OrganizationFields", "Name"]); + } + } + + opts.isLoginOnly = true; + result = await openLoginView(opts, endpoint, org, space); + } + + // User is signed in (result = OK) and trying to log in from CF targets view + if (target && OK === result) { + const currentCFTarget = await cfGetTarget(); + endpoint = !!endpoint ? endpoint : currentCFTarget["api endpoint"]; + org = !!org ? org : currentCFTarget?.org; + space = !!space ? space : currentCFTarget?.space; + + // Need only to set active target + if (endpoint && org && space) { + await invokeLongFunctionWithProgress( + cfSetOrgSpace.bind(undefined, org, space), + messages.set_org_space + ); + void vscode.window.showInformationMessage( + messages.success_set_org_space + ); + getModuleLogger(LOGGER_MODULE).debug( + "cmdCFSetOrgSpace: Organization <%s> and space <%s> have been set", + org, + space + ); + result = OK; + } else { + // Needs to complete the target settings (org & space) + result = await openLoginView(opts, endpoint, org, space); + } + } + + return result; + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const errText = toText(e); + void vscode.window.showErrorMessage(errText); + getModuleLogger(LOGGER_MODULE).error( + "cmdLogin failed", + { weak: weak }, + { exception: errText } + ); + return ""; + } +} + +export async function cmdSelectSpace(): Promise { + try { + return await cmdLogin(false, true); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const errText = toText(e); + void vscode.window.showErrorMessage(errText); + getModuleLogger(LOGGER_MODULE).error("cmdSelectSpace failed", { + exception: errText, + }); + return ""; + } +} + +async function cmdCreateTarget(): Promise { + const targetName = await vscode.window.showInputBox({ + prompt: messages.name_for_target, + ignoreFocusOut: true, + }); + if (targetName) { + const cliResult: CliResult = await Cli.execute([ + "save-target", + "-f", + targetName, + ]); + if (cliResult.exitCode !== 0) { + getModuleLogger(LOGGER_MODULE).error( + "cmdCreateTarget : command 'save-target -f <%s>' failed", + targetName, + { + output: cliResult.stdout, + } + ); + return Promise.reject(new Error(cliResult.stdout)); + } + + await cmdReloadTargets(); + void vscode.window.showInformationMessage( + messages.target_created(targetName) + ); + getModuleLogger(LOGGER_MODULE).debug( + "cmdCreateTarget: the <%s> target has been created", + targetName + ); + return targetName; + } +} + +function ask4ArbitraryParams( + serviceInfo: ServiceInfo, + plan: PlanInfo +): Thenable { + const data = generateParams4Service(serviceInfo.label, plan.label); + const value = stringify(data); + const xsappname: string = _.get(data, "xsappname"); + const pos = value.indexOf(xsappname); + return vscode.window.showInputBox({ + ignoreFocusOut: true, + prompt: messages.create_service_enter_params, + value, + valueSelection: pos > 0 ? [pos, pos + _.size(xsappname)] : undefined, + validateInput: validateParams( + _.get(serviceInfo, "label"), + _.get(plan, "label") + ), + }); +} + +async function onGetServicePlansFromCF( + opt: { planName?: string; serviceGuid: string }, + isCancelable = false, + title?: string +): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return runWithProgressAndLoginRetry( + isCancelable, + title || messages.loading_service_plan_list, + cfGetServicePlansList as () => Promise, + _.merge( + { + filters: [ + { key: eFilters.service_offering_guids, value: opt.serviceGuid }, + ], + }, + opt.planName + ? { + filters: [ + { key: eFilters.names, value: resolveFilterValue(opt.planName) }, + ], + } + : {} + ) + ); +} + +export async function onCreateService( + planInfo: PlanInfo, + instanceName: string, + params: unknown, + tags: string[] | undefined, + progress: vscode.Progress<{ message?: string; increment?: number }>, + cancelToken: vscode.CancellationToken +): Promise { + const response = await cfCreateService( + planInfo.guid, + instanceName, + params || {}, + tags || [], + { + progress: progress, + cancelToken: cancelToken, + } + ); + void vscode.window.showInformationMessage( + messages.service_created(response.name) + ); + getModuleLogger(LOGGER_MODULE).debug( + "onCreateService: the service has been created", + { response: response } + ); + return response.name; +} + +async function createUpsInstance( + name: string, + info?: ServiceTypeInfo +): Promise { + let result; + const quietMode = info?.tag && info.allowCreate?.name; + const credentials = quietMode + ? "{}" + : await vscode.window.showInputBox({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }); + if (credentials) { + const tags = quietMode + ? info?.tag + : await vscode.window.showInputBox( + _.merge( + { prompt: messages.enter_tags, ignoreFocusOut: true }, + info?.tag ? { value: info.tag } : {} + ) + ); + if (undefined !== tags) { + const sysLogUrl = quietMode + ? "" + : await vscode.window.showInputBox({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }); + if (undefined !== sysLogUrl) { + const routeUrl = quietMode + ? "" + : await vscode.window.showInputBox({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }); + if (undefined !== routeUrl) { + // canceled + const response = await cfCreateUpsInstance({ + instanceName: name, + syslog_drain_url: sysLogUrl, + route_service_url: routeUrl, + credentials: parse(credentials), + tags: _.compact(_.split(tags, /[,\s]/)), + }); + void vscode.window.showInformationMessage( + isCFResource(response) + ? messages.service_created(response.name) + : _.toString(response) + ); + getModuleLogger(LOGGER_MODULE).debug( + "createUpsInstance: the service has been created", + { + response: response, + } + ); + result = response?.name; + } + } + } + } + return result; +} + +async function createServiceInstance( + name: string, + info?: ServiceTypeInfo +): Promise { + const serviceName = info?.allowCreate?.serviceName || info?.name; + const servicePlan = info?.allowCreate?.plan || info?.plan; + const serviceTag = info?.allowCreate?.tag || info?.tag; + + const servicesInfo: ServiceInfo[] = await runWithProgressAndLoginRetry( + true, + messages.loading_services, + cfGetServices, + { + filters: [ + { key: eFilters.names, value: resolveFilterValue(serviceName) }, + ], + } + ); + if (_.size(servicesInfo)) { + const serviceInfo = + _.size(servicesInfo) === 1 && serviceName + ? servicesInfo[0] + : await vscode.window.showQuickPick(servicesInfo, { + placeHolder: messages.select_services, + ignoreFocusOut: true, + }); + + if (serviceInfo) { + const plansInfo: PlanInfo[] = await onGetServicePlansFromCF({ + serviceGuid: serviceInfo.guid, + planName: servicePlan, + }); + if (_.size(plansInfo)) { + const planInfo = + _.size(plansInfo) === 1 && servicePlan + ? plansInfo[0] + : await vscode.window.showQuickPick(plansInfo, { + placeHolder: messages.select_service_plan, + ignoreFocusOut: true, + }); + + if (planInfo) { + const params = info?.allowCreate?.getParams + ? stringify(await info.allowCreate.getParams()) + : await ask4ArbitraryParams(serviceInfo, planInfo); + if (params) { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: messages.creating_service( + name, + serviceInfo.label, + planInfo.label + ), + cancellable: true, + }, + (progress, cancelToken) => + onCreateService( + planInfo, + name, + parse(params), + serviceTag ? [serviceTag] : undefined, + progress, + cancelToken + ) + ); + } + } + } else { + const message = servicePlan + ? messages.no_service_plan_info_found(servicePlan, serviceInfo.label) + : messages.no_service_plans_found(serviceInfo.label); + throw new Error(message); + } + } + } else { + throw new Error( + serviceName && !isRegexExpression(serviceName) + ? messages.no_services_instances_found_for_type(serviceName) + : messages.no_services_instances_found + ); + } +} + +async function createService( + isUps: boolean, + info?: ServiceTypeInfo +): Promise { + // first ask for service-name + const options = _.merge( + { + prompt: info?.allowCreate?.namePrompt + ? `${info.allowCreate.namePrompt}` + : messages.enter_service_name, + }, + info?.allowCreate?.name ? { value: `${info.allowCreate.name}` } : {}, + { ignoreFocusOut: true } + ); + // Show instance name input box + const instanceName = await vscode.window.showInputBox(options); + if (instanceName) { + try { + return await (isUps + ? createUpsInstance(instanceName, info) + : createServiceInstance(instanceName, info)); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const errText = toText(error); + void vscode.window.showErrorMessage(errText); + getModuleLogger(LOGGER_MODULE).error( + "createService: create instance <%s> failed", + instanceName, + { ups: isUps }, + { info: info }, + { exception: errText } + ); + } + } +} + +export async function cmdCreateService( + info?: ServiceTypeInfo +): Promise { + if (await verifyLoginRetry()) { + return createService(false, info); + } +} + +export async function cmdCreateUps( + info?: ServiceTypeInfo +): Promise { + if (await verifyLoginRetry()) { + return createService(true, info); + } +} + +export async function fetchServicePlanList( + query?: IServiceQuery +): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return runWithProgressAndLoginRetry( + false, + messages.loading_service_plan_list, + cfGetServicePlansList, + query + ); +} + +export function getAvailableServices( + opts?: DisplayServices, + progressTitle?: string +): Promise { + return notifyWhenServicesInfoResultIncomplete( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + runWithProgressAndLoginRetry( + true, + progressTitle || messages.loading_services, + getAllServiceInstances, + opts + ) + ); +} + +export async function getServiceInstances( + query?: IServiceQuery | undefined, + progressTitle?: string +): Promise { + return notifyWhenServicesInfoResultIncomplete( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + runWithProgressAndLoginRetry( + true, + progressTitle || messages.loading_services, + cfGetServiceInstances, + query + ) + ); +} + +export async function getUserProvidedServiceInstances( + options?: UpsServiceQueryOprions, + progressTitle?: string +): Promise { + return notifyWhenServicesInfoResultIncomplete( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + runWithProgressAndLoginRetry( + true, + progressTitle || messages.loading_ups_services, + getUpsServiceInstances, + options + ) + ); +} + +async function askUserForServiceInstanceName( + availableServices: ServiceInstanceInfo[], + serviceType?: ServiceTypeInfo +): Promise { + const serviceName = _.get(serviceType, "name"); + const availableServicesToShow = availableServices; + + if (!_.size(availableServicesToShow)) { + void vscode.window.showErrorMessage( + serviceName && !isRegexExpression(serviceName) + ? messages.no_services_instances_found_for_type(serviceName) + : messages.no_services_instances_found + ); + getModuleLogger(LOGGER_MODULE).debug( + "askUserForServiceInstanceName: no service instances found for <%s> type", + serviceName + ); + return; + } + + const pickItems = _.map(availableServicesToShow, (s) => { + return { + label: s.label, + description: s.serviceName + (s.plan ? ` (${s.plan})` : ""), + }; + }); + + return vscode.window + .showQuickPick(pickItems, { + placeHolder: serviceType ? serviceType.prompt : messages.select_service, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .then((pickedItem) => { + return _.get(pickedItem, "label"); + }); +} + +async function checkForMoreServices( + instanceName: string | undefined, + availableServices: ServiceInstanceInfo[], + serviceType?: ServiceTypeInfo +): Promise { + if (instanceName === MORE_RESULTS && _.size(availableServices) > 0) { + const lastElem = availableServices.pop(); + if (lastElem && !lastElem.serviceName.startsWith("more:")) { + // unexpected + availableServices.push(lastElem); + return false; + } + // it is more: + const nextPage = Number.parseInt( + _.get(lastElem, 'serviceName.split(":")[1]') as string, + 10 + ); + + const nextAvailableServices = await getAvailableServices({ + query: { page: nextPage }, + ups: { tag: serviceType?.ups?.tag }, + }); + if (_.size(nextAvailableServices)) { + _.forEach(nextAvailableServices, (nas) => availableServices.push(nas)); + if (_.size(nextAvailableServices) === CF_PAGE_SIZE) { + const siInfo: ServiceInstanceInfo = { + label: MORE_RESULTS, + serviceName: `more:${nextPage + 1}`, + alwaysShow: true, + }; + availableServices.push(siInfo); + } + return true; + } + } + return false; +} + +export async function getInstanceName( + availableServices: ServiceInstanceInfo[], + serviceType?: ServiceTypeInfo +): Promise { + let instanceName = await askUserForServiceInstanceName( + availableServices, + serviceType + ); + while ( + await checkForMoreServices(instanceName, availableServices, serviceType) + ) { + instanceName = await askUserForServiceInstanceName( + availableServices, + serviceType + ); + } + return instanceName; +} + +export async function updateInstanceNameAndTags( + availableServices: ServiceInstanceInfo[], + serviceTypeInfo: ServiceTypeInfo | undefined, + instanceNames: string[], + tags: string[] +): Promise { + let instanceName = await getInstanceName(availableServices, serviceTypeInfo); + if (instanceName === CMD_CREATE_SERVICE) { + instanceName = await (serviceTypeInfo?.name === eServiceTypes.user_provided + ? cmdCreateUps(serviceTypeInfo) + : cmdCreateService(serviceTypeInfo)); + } else if ( + instanceName === + `${CMD_BIND_TO_DEFAULT_SERVICE}${serviceTypeInfo?.allowCreate?.name}` + ) { + const defaultInstance = _.find(availableServices, { + label: serviceTypeInfo?.allowCreate?.name, + plan: serviceTypeInfo?.allowCreate?.plan, + serviceName: serviceTypeInfo?.allowCreate?.serviceName, + }); + if (defaultInstance) { + instanceName = defaultInstance.label; + } else { + instanceName = await (serviceTypeInfo?.name === + eServiceTypes.user_provided + ? createUpsInstance( + serviceTypeInfo?.allowCreate?.name || "", + serviceTypeInfo + ) + : createServiceInstance( + serviceTypeInfo?.allowCreate?.name || "", + serviceTypeInfo + )); + } + } + if (_.size(instanceName) > 0) { + instanceNames.push(instanceName!); + serviceTypeInfo?.tag && tags.push(serviceTypeInfo.tag); + } + return instanceName; +} + +export function updateServicesOnCFPageSize( + availableServices: ServiceInstanceInfo[] +): void { + if (_.size(availableServices) === CF_PAGE_SIZE) { + availableServices.push({ + label: MORE_RESULTS, + serviceName: "more:2", + alwaysShow: true, + }); + } +} + +export async function cmdSelectAndSaveTarget(): Promise { + try { + let label; + if (await verifyLoginRetryPartial()) { + const target = await cfGetTarget(true); + const affairs = [ + { + id: "save", + label: messages.set_targets_save( + target.org || "undefined", + target.space || "undefined" + ), + detail: messages.set_targets_save_details, + }, + { + id: "pick-save", + label: messages.set_targets_pick_save, + detail: messages.set_targets_pick_save_details, + }, + ]; + let result; + const action = await vscode.window.showQuickPick(affairs, { + placeHolder: messages.set_targets_choose_the_operation, + canPickMany: false, + matchOnDetail: true, + }); + if (action?.id === "pick-save") { + result = await cmdLogin(false, false); + } else if (action?.id === "save") { + if (!target.org || !target.space) { + if ( + OK === + (await vscode.window.showWarningMessage( + messages.target_setup_not_completed(target.org, target.space), + OK, + Cancel + )) + ) { + result = OK; + } + } else { + result = OK; + } + } + if (OK === result) { + label = await cmdCreateTarget(); + } + } + return label; + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const errText = toText(e); + void vscode.window.showErrorMessage(errText); + getModuleLogger(LOGGER_MODULE).error( + `cmdSelectAndSaveTarget exception thrown`, + { error: errText } + ); + } +} + +// for testing purpose only +export const internal = { + onErrorCfLogin, +}; diff --git a/projects/cloud-foundry-tools/packages/backend/src/extension.ts b/projects/cloud-foundry-tools/packages/backend/src/extension.ts new file mode 100644 index 000000000..763e9a4df --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/extension.ts @@ -0,0 +1,340 @@ +import { + ExtensionContext, + window, + StatusBarItem, + EventEmitter, + workspace, + commands, + StatusBarAlignment, + extensions, + ConfigurationChangeEvent, +} from "vscode"; +import * as _ from "lodash"; +import { CFTargetNotCurrent, CFTargetTI, CFView } from "./cfView"; +import { messages } from "./messages"; +import { + cmdLogin, + cmdCreateService, + cmdCFSetOrgSpace, + cmdSelectSpace, + cmdCreateUps, + cmdSelectAndSaveTarget, +} from "./commands"; +import { + cmdDeployServiceAPI, + cmdSetCurrentTarget, + cmdDeleteTarget, + cmdBindLocal, + cmdReloadTargets, + cmdGetSpaceServices, + cmdGetUpsServiceInstances, + cmdGetServiceInstances, + execSetTarget, + execSaveTarget, +} from "./cfViewCommands"; + +import { + cfGetConfigFilePath, + cfGetConfigFileField, + ITarget, + cfGetTarget, + OK, +} from "@sap/cf-tools"; +import * as fs from "fs"; +import { DependencyHandler } from "./run-configuration"; +import { IRunConfigRegistry } from "@sap/wing-run-config-types"; +import { toText } from "./utils"; +import { getModuleLogger, initLogger } from "./logger/logger-wrapper"; + +const LOGGER_MODULE = "extension"; +let cfStatusBarItem: StatusBarItem; +const runConfigExtName = "sap.vscode-wing-run-config"; +let treeDataProvider: CFView; +let extPath: string; + +const targetChangedEventEmitter = new EventEmitter(); +const onDidChangeTarget = targetChangedEventEmitter.event; + +async function updateStatusBar(): Promise { + let isUpdated; + const beforeText = _.get(cfStatusBarItem, "text"); + const results = await Promise.all([ + cfGetConfigFileField("OrganizationFields"), + cfGetConfigFileField("SpaceFields"), + ]); + const orgField: string = _.get(results, "[0].Name"); + const spaceField: string = _.get(results, "[1].Name"); + const isNoTarget = _.isEmpty(orgField) && _.isEmpty(spaceField); + const updatedText = `${ + isNoTarget + ? messages.not_targeted + : messages.targeting(orgField, spaceField) + }`; + + if (beforeText !== updatedText) { + _.set(cfStatusBarItem, "text", updatedText); + if (beforeText) { + // eliminate firing event on initialization + const target = async () => { + try { + return await cfGetTarget(); + } catch (e) { + return; + } + }; + targetChangedEventEmitter.fire(isNoTarget ? undefined : await target()); + isUpdated = true; + } + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return isUpdated; +} + +export function onCFConfigFileChange(): void { + void updateStatusBar().then((isUpdated) => { + if (isUpdated) { + getModuleLogger(LOGGER_MODULE).debug( + "onCFConfigFileChange: the Cloud Foundry 'config' file has changed. The status bar will be updated" + ); + treeDataProvider.refresh(); + } + }); +} + +function displayTargetWhenAllowed(): void { + if ( + workspace.getConfiguration().get("CloudFoundryTools.showTargetInformation") + ) { + cfStatusBarItem.show(); + } else { + cfStatusBarItem.hide(); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function callbackOnDidChangeConfiguration( + e: ConfigurationChangeEvent, + context: ExtensionContext +): void { + if (e.affectsConfiguration("CloudFoundryTools.showTargetInformation")) { + displayTargetWhenAllowed(); + } +} + +async function init( + context: ExtensionContext, + cfConfigFilePath: string +): Promise { + cfStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 100); + context.subscriptions.push(cfStatusBarItem); + + // !!!! does not work on theia 1.5.0 + // cfStatusBarItem.command = {command: loginCmdId, arguments: [true], title: ""}; + // start workarround --> remove following workarround in future theia releases + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + context.subscriptions.push( + commands.registerCommand("cf.login.weak", cmdLogin.bind(null, true)) + ); + cfStatusBarItem.command = "cf.login"; + // end workarround + + void updateStatusBar(); + + if (!_.isEmpty(cfConfigFilePath)) { + fs.watchFile(cfConfigFilePath, onCFConfigFileChange); + } + + displayTargetWhenAllowed(); + + let platformExtension = + extensions.getExtension(runConfigExtName); + if (platformExtension) { + if (!platformExtension.isActive) { + try { + await platformExtension.activate(); + } catch (e) { + platformExtension = undefined; + getModuleLogger(LOGGER_MODULE).error( + "activate <%s> extension fails", + runConfigExtName, + { + exception: toText(new Error(e?.message as string)), + } + ); + } + } + if (platformExtension) { + const genericDependencyHandler = new DependencyHandler( + "cf-tools-rsource-dependency" + ); + platformExtension.exports.registerDependency(genericDependencyHandler); + // eslint-disable-next-line @typescript-eslint/unbound-method + context.subscriptions.push( + commands.registerCommand( + "cf.services.bind", + genericDependencyHandler.bind + ) + ); + // eslint-disable-next-line @typescript-eslint/unbound-method + context.subscriptions.push( + commands.registerCommand( + "cf.services.unbind", + genericDependencyHandler.unbind + ) + ); + // eslint-disable-next-line @typescript-eslint/unbound-method + context.subscriptions.push( + commands.registerCommand( + "cf.services.binding.state", + genericDependencyHandler.getBindState + ) + ); + } + } else { + getModuleLogger(LOGGER_MODULE).error( + "activate: the <%s> extension has not been set", + runConfigExtName + ); + } +} + +export async function activate(context: ExtensionContext): Promise { + await initLogger(context); + extPath = context.extensionPath; + + const cfConfigFilePath: string = cfGetConfigFilePath(); + treeDataProvider = new CFView(context, cfConfigFilePath); + const view = window.createTreeView("cfView", { + treeDataProvider, + showCollapseAll: true, + }); + + context.subscriptions.push( + commands.registerCommand( + "cf.login", + (weak: boolean, target: boolean, extEndPoint: string | undefined) => { + return cmdLogin(weak, target, extEndPoint, { isSplit: !!weak }).then( + (result) => { + if (OK === result) { + const active = _.find( + treeDataProvider.getTargets(), + "target.isCurrent" + ); + if (active) { + // after re-login the target data become invalid -> perform re-create target + void cmdDeleteTarget(active, { + "skip-reload": true, + silent: true, + }).then(() => { + void execSaveTarget(active, { + "skip-reload": true, + silent: true, + }).then(() => { + void execSetTarget(active, { silent: true }); + }); + }); + } + } + return result; + } + ); + } + ) + ); + + context.subscriptions.push( + workspace.onDidChangeConfiguration((e) => + callbackOnDidChangeConfiguration(e, context) + ) + ); + + function revealTargetItem(label?: string) { + setTimeout(() => { + const treeItem = _.find( + treeDataProvider.getTargets(), + label ? ["label", label] : "target.isCurrent" + ); + if (treeItem) { + void view.reveal(treeItem, { select: true, focus: true, expand: true }); + } + }, 400); + } + + context.subscriptions.push( + commands.registerCommand( + "cf.target.set", + async (item: CFTargetTI | CFTargetNotCurrent) => { + await cmdSetCurrentTarget(item); + revealTargetItem(); + } + ) + ); + context.subscriptions.push( + commands.registerCommand("cf.services.create", cmdCreateService) + ); + context.subscriptions.push( + commands.registerCommand("cf.ups.create", cmdCreateUps) + ); + context.subscriptions.push( + commands.registerCommand("cf.services.bind.local", cmdBindLocal) + ); + context.subscriptions.push( + commands.registerCommand("cf.set.orgspace", cmdCFSetOrgSpace) + ); + context.subscriptions.push( + commands.registerCommand("cf.targets.create", async () => { + const label = await cmdSelectAndSaveTarget(); + if (label) { + revealTargetItem(label); + } + }) + ); + context.subscriptions.push( + commands.registerCommand("cf.target.delete", cmdDeleteTarget) + ); + context.subscriptions.push( + commands.registerCommand("cf.targets.reload", cmdReloadTargets) + ); + context.subscriptions.push( + commands.registerCommand("cf.select.space", cmdSelectSpace) + ); + context.subscriptions.push( + commands.registerCommand("cf.deploy-service.api", cmdDeployServiceAPI) + ); + context.subscriptions.push( + commands.registerCommand( + "cf.services.get-space-services", + cmdGetSpaceServices + ) + ); + context.subscriptions.push( + commands.registerCommand( + "cf.services.get-ups-services", + cmdGetUpsServiceInstances + ) + ); + context.subscriptions.push( + commands.registerCommand("cf.services.get-services", cmdGetServiceInstances) + ); + + // postpone the rest of initialization after the extension is activated + setTimeout(() => { + void init(context, cfConfigFilePath); + }, 10); + + return { + // allows to external extensions to listen onDidChangeTarget event + onDidChangeTarget, + }; +} + +export function getPath() { + return extPath; +} + +export function deactivate(): void { + const cfConfigFilePath = cfGetConfigFilePath(); + if (cfConfigFilePath) { + fs.unwatchFile(cfConfigFilePath); + } +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/logger/logger-wrapper.ts b/projects/cloud-foundry-tools/packages/backend/src/logger/logger-wrapper.ts new file mode 100644 index 000000000..aa913ce4e --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/logger/logger-wrapper.ts @@ -0,0 +1,52 @@ +import { ExtensionContext, window } from "vscode"; +import { readFile as readFileCallback } from "fs"; +import { resolve } from "path"; +import { configureLogger, NOOP_LOGGER } from "@vscode-logging/wrapper"; +import { IChildLogger, IVSCodeExtLogger } from "@vscode-logging/types"; +import { promisify } from "util"; +/** + * Note that the values of these configuration properties must match those defined in the package.json + */ +const LOGGING_LEVEL_CONFIG_PROP = "CloudFoundryTools.logging.loggingLevel"; +const SOURCE_TRACKING_CONFIG_PROP = + "CloudFoundryTools.logging.sourceLocationTracking"; + +const readFile = promisify(readFileCallback); +// On file load we initialize our logger to `NOOP_LOGGER` +// this is done because the "real" logger cannot be initialized during file load. +// only once the `activate` function has been called in extension.ts +// as the `ExtensionContext` argument to `activate` contains the required `logPath` +let loggerImpel: IVSCodeExtLogger = NOOP_LOGGER; + +export function getLogger(): IChildLogger { + return loggerImpel; +} + +function setLogger(newLogger: IVSCodeExtLogger): void { + loggerImpel = newLogger; +} + +export function getModuleLogger(name: string): IChildLogger { + return getLogger().getChildLogger({ label: name }); +} + +export async function initLogger(context: ExtensionContext): Promise { + const meta = JSON.parse( + await readFile(resolve(context.extensionPath, "package.json"), "utf8") + ) as { + displayName: string; + }; + + const extLogger = configureLogger({ + extName: meta.displayName, + logPath: context.logUri.fsPath, + logOutputChannel: window.createOutputChannel(meta.displayName), + // set to `true` if you also want your VSCode extension to log to the console. + logConsole: false, + loggingLevelProp: LOGGING_LEVEL_CONFIG_PROP, + sourceLocationProp: SOURCE_TRACKING_CONFIG_PROP, + subscriptions: context.subscriptions, + }); + + setLogger(extLogger); +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/loginTargetView/loginTargetView.ts b/projects/cloud-foundry-tools/packages/backend/src/loginTargetView/loginTargetView.ts new file mode 100644 index 000000000..029fb3aba --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/loginTargetView/loginTargetView.ts @@ -0,0 +1,308 @@ +import * as vscode from "vscode"; +import * as extension from "../extension"; +import { join, sep } from "path"; +import * as fs from "fs"; +import { RpcExtension } from "@sap-devx/webview-rpc/out.ext/rpc-extension"; +import { getModuleLogger } from "../logger/logger-wrapper"; +import { + cfGetAvailableOrgs, + cfGetAvailableSpaces, + cfGetConfigFileField, + cfGetTarget, + cfLogin, + cfLogout, + cfSetOrgSpace, + CredentialsLoginOptions, + ITarget, + OK, + SSOLoginOptions, + Organization, + Space, +} from "@sap/cf-tools"; +import { messages } from "../messages"; +import { cfendpoint } from "@sap/bas-sdk"; +import { toText } from "../utils"; +import * as cheerio from "cheerio"; + +let loginTarget: LoginTarget | undefined; +let initTarget: { + endpoint: string | undefined; + org?: string | undefined; + space?: string | undefined; +}; +let panel: vscode.WebviewPanel | undefined; +let isLoginOnly: boolean | undefined; +let cmdLoginResult: string | undefined; + +const LOGGER_MODULE = "loginTargetView"; + +export function openLoginView( + opts: { isSplit: boolean; isLoginOnly?: boolean }, + endpoint?: string, + org?: string, + space?: string +): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + initTarget = { + endpoint: endpoint, + org: org, + space: space, + }; + isLoginOnly = opts.isLoginOnly; + // Every time the view is opened need to recalculate the login result + cmdLoginResult = undefined; + + return new Promise((resolve, reject) => { + try { + const split = opts.isSplit + ? vscode.ViewColumn.Beside + : vscode.ViewColumn.One; + panel = panel + ? panel + : vscode.window.createWebviewPanel( + "cfLogin", + "Cloud Foundry Sign In", + split, + { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.file(join(extension.getPath(), "dist", "media")), + ], + } + ); + panel.reveal(); + panel.onDidDispose(() => { + resolve(cmdLoginResult); + panel = undefined; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loginTarget = undefined; + }); + + const extensionPath = extension.getPath(); + const mediaPath = join(extensionPath, "dist", "media"); + const htmlFileName = "index.html"; + + let indexHtml = fs.readFileSync(join(mediaPath, htmlFileName), "utf8"); + if (indexHtml) { + // Local path to main script run in the webview + const scriptPathOnDisk = vscode.Uri.file(join(mediaPath, sep)); + const scriptUri = panel.webview.asWebviewUri(scriptPathOnDisk); + const baseUrl = scriptUri.toString(); + const $ = cheerio.load(indexHtml); + + function replaceAttributePaths(elements: any, attributeName: string) { + elements.each((index: number, element: cheerio.Element) => { + const currentAttr = $(element).attr(attributeName); + if (currentAttr) { + $(element).attr(attributeName, `${baseUrl}/${currentAttr}`); + } + }); + } + + replaceAttributePaths($("[src]"), "src"); + replaceAttributePaths($("[href]"), "href"); + + indexHtml = $.html(); + } + + panel.webview.html = indexHtml; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loginTarget = new LoginTarget(panel); + } catch (e) { + reject(e.toString()); + } + }); +} + +class LoginTarget { + private rpc: RpcExtension; + private currentTarget: ITarget | undefined; + + constructor(private readonly panel: vscode.WebviewPanel) { + this.rpc = new RpcExtension(panel.webview); + this.rpc.registerMethod({ func: this.init, thisArg: this }); + this.rpc.registerMethod({ func: this.loginClick, thisArg: this }); + this.rpc.registerMethod({ func: this.logoutClick, thisArg: this }); + this.rpc.registerMethod({ func: this.getSelectedTarget, thisArg: this }); + this.rpc.registerMethod({ func: this.getOrgs, thisArg: this }); + this.rpc.registerMethod({ func: this.getSpaces, thisArg: this }); + this.rpc.registerMethod({ func: this.applyTarget, thisArg: this }); + this.rpc.registerMethod({ func: this.openPasscodeLink, thisArg: this }); + } + + private async getTarget(): Promise { + // TODO: need to pass the endpoint to check if logged-in to correct endpoint. + try { + return (await this.invokeLongFunctionWithProgressForm( + cfGetTarget, + false + )) as ITarget; + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const errText = toText(e); + getModuleLogger(LOGGER_MODULE).error("getTarget failed", { + exception: errText, + }); + return undefined; + } + } + + private getCFDefaultLandscape(): Promise { + return cfGetConfigFileField("Target").then((cfgEndpoint: string) => { + return cfgEndpoint || cfendpoint.getCFEndpoint(); + }); + } + + private calculatePasscodeUrl(endpoint: string): string { + return endpoint.replace(/api./g, "login.") + "/passcode"; + } + + async init(): Promise { + this.currentTarget = await this.getTarget(); + if (!initTarget) { + initTarget = { endpoint: undefined, org: undefined, space: undefined }; + } + + initTarget.endpoint = initTarget?.endpoint + ? initTarget?.endpoint + : this.currentTarget && this.currentTarget["api endpoint"] + ? this.currentTarget["api endpoint"] + : await this.getCFDefaultLandscape(); + + return { + defaultEndpoint: initTarget.endpoint, + isLoggedIn: this.currentTarget !== undefined, + passcodeUrl: this.calculatePasscodeUrl(initTarget.endpoint), + currentOrg: this.currentTarget?.org, + currentSpace: this.currentTarget?.space, + }; + } + + async loginClick(payload: SSOLoginOptions | CredentialsLoginOptions) { + try { + cmdLoginResult = await this.invokeLongFunctionWithProgressForm( + cfLogin, + payload + ); + if (OK !== cmdLoginResult) { + void vscode.window.showErrorMessage(messages.login_failed); + return false; + } + void vscode.window.showInformationMessage(messages.login_success); + getModuleLogger(LOGGER_MODULE).debug("executeLogin: login succeeded"); + + if (isLoginOnly) { + setTimeout(() => { + panel?.dispose(); + }); + } + return true; + } catch (error) { + getModuleLogger(LOGGER_MODULE).error("executelogin: login error", error); + return false; + } + } + + async logoutClick() { + try { + await this.invokeLongFunctionWithProgressForm(cfLogout); + this.currentTarget = undefined; + void vscode.window.showInformationMessage(messages.logout_success); + getModuleLogger(LOGGER_MODULE).debug("executeLogout: logout succeeded"); + return true; + } catch (error) { + getModuleLogger(LOGGER_MODULE).error( + "executeLogout: logout error", + error + ); + return false; + } + } + + getSelectedTarget() { + return { + endpoint: this.currentTarget?.["api endpoint"], + org: this.currentTarget?.org, + space: this.currentTarget?.space, + }; + } + + getOrgs(): Promise { + return cfGetAvailableOrgs() + .then((orgs: Organization[]) => { + getModuleLogger(LOGGER_MODULE).debug( + "executeGetAvaliableOrgs: get avaliable orgs succeeded" + ); + return orgs; + }) + .catch((error) => { + getModuleLogger(LOGGER_MODULE).error( + "executeGetAvaliableOrgs: get avaliable orgs failed", + error + ); + return []; + }); + } + + getSpaces(org: string): Promise { + return cfGetAvailableSpaces(org) + .then((spaces: Space[]) => { + getModuleLogger(LOGGER_MODULE).debug( + "executeGetAvaliableSpaces: get avaliable spaces succeeded" + ); + return spaces; + }) + .catch((error) => { + getModuleLogger(LOGGER_MODULE).error( + "executeGetAvaliableSpaces: get avaliable spaces failed", + error + ); + return []; + }); + } + + async applyTarget(org: string, space: string) { + try { + await cfSetOrgSpace(org, space); + cmdLoginResult = OK; + void vscode.window.showInformationMessage(messages.success_set_org_space); + getModuleLogger(LOGGER_MODULE).debug( + "executeSetOrgSpace: set org & spaces succeeded" + ); + panel?.dispose(); + return cmdLoginResult; + } catch (error) { + cmdLoginResult = undefined; + getModuleLogger(LOGGER_MODULE).error( + "executeSetOrgSpace: set org & spaces failed", + error + ); + return "Error"; + } + } + + openPasscodeLink(endpoint: string) { + void vscode.commands.executeCommand( + "vscode.open", + vscode.Uri.parse(this.calculatePasscodeUrl(endpoint)) + ); + } + + async invokeLongFunctionWithProgressForm( + longFunction: Function, + ...args: any + ): Promise { + try { + await this.rpc.invoke("setBusyIndicator", [true]); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + return await longFunction(...args); + } finally { + await this.rpc.invoke("setBusyIndicator", [false]); + } + } +} + +// for testing purpose only +export const internal = { + LoginTarget, +}; diff --git a/projects/cloud-foundry-tools/packages/backend/src/messages.ts b/projects/cloud-foundry-tools/packages/backend/src/messages.ts new file mode 100644 index 000000000..e3e4f7654 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/messages.ts @@ -0,0 +1,114 @@ +import * as _ from "lodash"; + +export const messages = { + enter_cf_endpoint: "Enter Cloud Foundry endpoint", + enter_user_email: "Enter e-mail address", + label_enter_password: `Enter ${_.reverse([ + "d", + "r", + "o", + "w", + "s", + "s", + "a", + "p", + ]).join("")}`, + login: "You need to login to Cloud Foundry to continue.", + loggin_in: "Logging in...", + logout: "Logging out...", + login_success: "You have been signed in to Cloud Foundry.", + logout_success: "You have been signed out.", + getting_orgs: "Getting Organizations...", + getting_spaces: "Getting Spaces...", + select_org: "Select Organization", + select_space: "Select Space", + set_org_space: "Setting Organization and Space...", + success_set_org_space: "The Organization and Space have been set.", + no_available_orgs: "There are no available organizations.", + no_available_spaces: "There are no available spaces.", + name_for_target: "Enter a name for the target", + not_targeted: "Login to CF", + space_not_set: "Space not set", + cf_setting_not_set: + "Could not find the Cloud Foundry settings. Make sure you have assigned an org and space in Cloud Foundry.", + enter_service_name: "Enter a name for the service instance", + loading_services: "Loading Cloud Foundry services...", + loading_ups_services: "Loading Cloud Foundry user provided services...", + loading_service_plan_list: "Loading Cloud Foundry service plans...", + binding_service_to_file: + "Binding the service instance to a local '.env' file...", + select_service: "Select a Cloud Foundry service instance", + select_services: "Select a Cloud Foundry service", + select_service_plan: "Select a Cloud Foundry service plan", + no_services_instances_found: `No service instances found`, + create_service_enter_params: `Enter arbitrary parameters to be passed along to the service broker`, + enter_sys_log_url: `Enter a system logs drain URL`, + enter_route_service_url: `Enter a route service URL`, + enter_credentials: `Enter credentials`, + enter_tags: `Enter a tag for the service instance`, + load_data_from_deploy: `Loading data from Deploy Service...`, + no_cf_api_endpoint: `No CF API endpoint`, + verify_cf_connectivity: `Verifying Cloud Foundry connectivity details...`, + set_targets_save_details: `Save the current selected target`, + set_targets_pick_save: `Create new ...`, + set_targets_pick_save_details: `Create and save a new target`, + set_targets_choose_the_operation: `Save your current target or specify a new one`, + no_services_instance_byname_found: (name: string): string => + `Could not find the ${name} service instance`, + login_failed: `Authentication failed.`, + authentication_failed: (result: string): string => + `Authentication failed: ${result}`, + targeting: (orgField: string, spaceField: string): string => + `Targeting CF ${orgField}/${spaceField}`, + service_created: (instanceName: string): string => + `The ${instanceName} service has been created.`, + target_created: (targetName: string): string => + `The '${targetName}' target has been created.`, + creating_service: ( + instanceName: string, + servieInfoLabel: string, + planInfoLabel: string + ): string => + `Creating the ${instanceName}(${servieInfoLabel} - ${planInfoLabel}) service...`, + target_deleted: (targetLabel: string): string => + `The '${targetLabel}' target has been deleted.`, + activation_extension: (extname: string): string => + `Activating the "${extname}" extension`, + no_services_found: (plan: string): string => + `No services were found for the '${plan}' service_plan`, + no_services_instances_found_for_type: (name: string): string => + `No service instances found for the ${name} type`, + extension_active: (extname: string): string => + `The "${extname}" extension is now active.`, + error_service_params_mandatory: (name: string): string => + `You must provide a value for the ${name}`, + error_service_params_max_length: (name: string, length: number): string => + `The ${name} may be composed of up to ${length} characters`, + error_service_params_value_not_allowed: (name: string): string => + `Invalid value for the ${name}`, + service_bound_successful: (service: string): string => + `The ${service} service has been bound.`, + service_unbound_successful: (service: string): string => + `The ${service} service has been unbound.`, + no_service_plan_info_found: (plan: string, service: string): string => + `Could not find the ${plan} service plan info for the ${service} service`, + no_service_plans_found: (service: string): string => + `Could not find a service plan for the ${service} service`, + target_dirty_save: (name: string): string => + `The ${name} target has been modified. Would you like to save the changes?`, + service_instances_list_incomplete: (list: string[]): string => + `Could not get the details of ${ + _.size(list) > 3 + ? "some service instances" + : "the following service instances: '" + list.join() + "'" + }. Try to verify the ${ + _.size(list) > 3 ? "" : "specified " + }service instances, then log in again to Cloud Foundry.`, + target_setup_not_completed: ( + org: string | undefined, + space: string | undefined + ): string => + `The CF target definition is incomplete - org='${org}', space='${space}'. Do you want to continue?`, + set_targets_save: (org: string, space: string): string => + `Save current target (${org}/${space})`, +}; diff --git a/projects/cloud-foundry-tools/packages/backend/src/run-configuration-example.ts b/projects/cloud-foundry-tools/packages/backend/src/run-configuration-example.ts new file mode 100644 index 000000000..cd4f526c9 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/run-configuration-example.ts @@ -0,0 +1,98 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import { + IRunnable, + IRunConfigContext, + IDependencyContext, + ConfigurationTarget, + IRunConfigHandler, + IConfigurationData, + ConfigObject, +} from "@sap/wing-run-config-types"; + +class Runnable implements IRunnable { + public displayName = "CF Tools Example"; + public displayType = "CF Tools Type"; + public path = "path"; + constructor(public id: string) {} +} + +class DependencyContext implements IDependencyContext { + public displayName = "cf-tools-rsource-dependency"; + public bindable = true; + public displayType = "cf-tools-rsource-dependency"; + public type = "hana"; + public dependencyHandlerId = "cf-tools-rsource-dependency"; +} + +export class RunConfigContext implements IRunConfigContext { + public iconBuffer: Buffer; + public displayName = "CF Tools Example"; + public groupName = this.appId; + public envPath: vscode.Uri = vscode.Uri.file( + path.join( + "C:", + "Users", + "your-i-number", + "devx-wing", + "test-run-config", + "env", + ".env" + ) + ); + public path: vscode.Uri; + public dependencies = [new DependencyContext()]; + + constructor(private readonly id: string, private readonly appId: string) { + this.groupName = appId; + this.path = vscode.Uri.file(""); + this.iconBuffer = Buffer.alloc(1); + } +} + +export class RunConfigHandler implements IRunConfigHandler { + constructor(private readonly id: string) {} + + public getRunnables(): Promise { + return Promise.resolve([ + new Runnable(this.id), + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + new Runnable(this.id + 1), + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + new Runnable(this.id + 2), + ]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public getRunConfigContext( + runnableId: string, + confData: IConfigurationData + ): Promise { + return Promise.resolve(new RunConfigContext(this.id, "Cf App")); + } + + public createConfiguration( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + runnableId: string + ): Promise { + return Promise.resolve({ + config: new ConfigObject( + { + type: "node", + request: "launch", + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + name: "cf run " + Date.now(), + env: { + Saggi: 1, + "run.config": "sage", + }, + }, + ConfigurationTarget.launch + ), + }); + } + + public getId(): string { + return "cf_run_id_" + this.id; + } +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/run-configuration.ts b/projects/cloud-foundry-tools/packages/backend/src/run-configuration.ts new file mode 100644 index 000000000..62dc5c6fe --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/run-configuration.ts @@ -0,0 +1,178 @@ +import * as types from "@sap/wing-run-config-types"; +import * as cfViewCommands from "./cfViewCommands"; +import { getEnvResources, removeResourceFromEnv, toText } from "./utils"; +import { checkAndCreateChiselTask } from "./chisel"; +import * as _ from "lodash"; +import * as vscode from "vscode"; +import * as cfLocal from "@sap/cf-tools"; +import { messages } from "./messages"; +import { getModuleLogger } from "./logger/logger-wrapper"; + +export class DependencyHandler implements types.IDependencyHandler { + private readonly id: string; + private static readonly MODULE_NAME = "DependencyHandler"; // workarround in minimized mode + + constructor(id: string) { + this.id = id; + } + + public async getBindState( + bindContext: types.IBindContext + ): Promise { + let bindState = types.BindState.notbound; + + try { + if ( + _.get( + (await getEnvResources(_.get(bindContext, ["envPath", "fsPath"]))) + .vcapObject, + _.get(bindContext, ["depContext", "type"]) + ) + ) { + bindState = types.BindState.cloud; + } + } catch (e) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + void vscode.window.showErrorMessage(toText(e)); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(DependencyHandler.MODULE_NAME).error( + "getBindState: processing failed", + { exception: toText(e) } + ); + } + return bindState; + } + + public async bind( + bindContext: types.IBindContext, + options?: cfViewCommands.CmdOptions + ): Promise { + const resourceTag: string = _.get( + bindContext, + "depContext.data.resourceTag" + ); + const serviceType: cfLocal.ServiceTypeInfo[] = [ + { + name: _.get(bindContext.depContext, ["type"], ""), + plan: _.get(bindContext.depContext, ["data", "plan"], ""), + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + tag: resourceTag + ? resourceTag + _.get(bindContext, "depContext.data.resourceName") + : "", + prompt: "", + }, + ]; + try { + const instanceNames = await cfViewCommands.bindLocalService( + serviceType, + bindContext.envPath, + options + ); + let chiselTask; + if (_.size(instanceNames)) { + // Get metadata of service instance by service name + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const instanceType: string = ( + await cfLocal.cfGetInstanceMetadata(_.get(instanceNames, "[0]")) + ).service; + + // Create chisel task if neccessary + if ( + _.get(bindContext, "depContext.data.isCreateChiselTask") || + /^hana(trial)?$/.test(instanceType) + ) { + // Create it in dependent task + const chiselTaskNameSuffix = instanceNames?.join("&") || ""; + chiselTask = await checkAndCreateChiselTask( + bindContext.envPath?.fsPath, + chiselTaskNameSuffix + ); + if (chiselTask) { + if (!options?.silent) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + void vscode.window.showInformationMessage( + `A task for opening the VPN tunnel to the Cloud Foundry space has been created. Name: '${chiselTask.label}'` + ); + } + getModuleLogger(DependencyHandler.MODULE_NAME).info( + "bind: <%s> task for opening the VPN tunnel to the Cloud Foundry space has been created", + chiselTask.label + ); + + if (_.isNil(bindContext.configData.dependentTasks)) { + // eslint-disable-next-line require-atomic-updates + bindContext.configData.dependentTasks = []; + } + bindContext.configData.dependentTasks.push(chiselTask); + } + } + return _.merge( + { + configData: bindContext.configData, + resource: { + name: _.get(instanceNames, "[0]"), + type: instanceType || "", + }, + }, + chiselTask ? { resource: { data: { chiselTask } } } : {} + ); + } + } catch (e) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + void vscode.window.showErrorMessage(toText(e)); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(DependencyHandler.MODULE_NAME).error( + "bind: processing failed", + { exception: toText(e) } + ); + } + } + + public async unbind( + bindContext: types.IBindContext, + options?: cfViewCommands.CmdOptions + ): Promise { + const configObject = new types.ConfigObject( + {}, + types.ConfigurationTarget.launch + ); + const configurationData: types.IConfigurationData = { + config: configObject, + dependentTasks: [], + }; + try { + const removedResourceDetails = await removeResourceFromEnv(bindContext); + if (!options?.silent) { + void vscode.window.showInformationMessage( + messages.service_unbound_successful( + _.get(removedResourceDetails, "resourceName") + ) + ); + } + getModuleLogger(DependencyHandler.MODULE_NAME).info( + "unbind: the <%s> service has been unbound", + _.get(removedResourceDetails, "resourceName") + ); + return Promise.resolve({ + configData: configurationData, + resource: { + name: _.get(removedResourceDetails, "resourceName") || "", + type: _.get(removedResourceDetails, "resourceData.label") || "", + data: _.get(removedResourceDetails, "resourceData") || {}, + }, + }); + } catch (e) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + void vscode.window.showErrorMessage(toText(e)); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + getModuleLogger(DependencyHandler.MODULE_NAME).error( + "unbind: processing failed", + { exception: toText(e) } + ); + } + } + + public getId(): string { + return this.id; + } +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/sql-tools.ts b/projects/cloud-foundry-tools/packages/backend/src/sql-tools.ts new file mode 100644 index 000000000..47060c096 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/sql-tools.ts @@ -0,0 +1,56 @@ +import * as vscode from "vscode"; +import { dataContentAsObject } from "@sap/cf-tools"; +import * as _ from "lodash"; +import { parse } from "comment-json"; + +const CONNECTIONS = "connections"; + +function saveSettings(filePath: string, configuration: any) { + const sqltoolsSettings = vscode.workspace.getConfiguration( + "sqltools", + vscode.Uri.file(filePath) + ); + const connections: any[] = sqltoolsSettings.get(CONNECTIONS) || []; + + const existingConnIndex = connections.findIndex( + (conn: any) => conn.name === configuration.name + ); + if (existingConnIndex >= 0) { + connections[existingConnIndex] = configuration; + } else { + connections.push(configuration); + } + void sqltoolsSettings.update(CONNECTIONS, connections); +} + +export async function checkAndCreateSQLToolsSettings( + filePath: string +): Promise { + const envFileContent = await dataContentAsObject(filePath); + const vcapServicesStr = _.get(envFileContent, "VCAP_SERVICES"); + if (vcapServicesStr) { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + const vcapServices = parse(vcapServicesStr); + const hanaServices = _.get(vcapServices, "hana"); + if (hanaServices) { + const serviceNames = _.map(hanaServices, (srv) => { + const conn = { + name: _.get(srv, "name"), + dialect: "SAPHana", + database: _.get(srv, "credentials.schema"), + server: _.get(srv, "credentials.host"), + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + port: Number.parseInt(_.get(srv, "credentials.port"), 10), + username: _.get(srv, "credentials.user"), + password: _.get(srv, "credentials.password"), + connectionTimeout: 30, + }; + saveSettings(filePath, conn); + return srv.name as string; + }); + return serviceNames.join(","); + } + } + + return ""; +} diff --git a/projects/cloud-foundry-tools/packages/backend/src/utils.ts b/projects/cloud-foundry-tools/packages/backend/src/utils.ts new file mode 100644 index 000000000..00209d31d --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/src/utils.ts @@ -0,0 +1,539 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import * as fs from "fs"; +import { messages } from "./messages"; +import * as types from "@sap/wing-run-config-types"; +import * as _ from "lodash"; +import * as PropertiesReader from "properties-reader"; +import { parse } from "comment-json"; +import { EOL, platform } from "os"; +import { + IServiceQuery, + ServiceInstanceInfo, + cfGetUpsInstances, + eFilters, + ServiceTypeInfo, + cfGetManagedServiceInstances, + cfGetTarget, +} from "@sap/cf-tools"; +import { getModuleLogger } from "./logger/logger-wrapper"; +import { existsSync } from "fs"; + +export const isWindows = platform().includes("win"); +type TypeValidationResult = string | undefined | null; + +const LOGGER_MODULE = "utils"; + +export interface DisplayServices { + query?: IServiceQuery; + ups?: ServiceTypeInfo["ups"]; +} + +export interface BaseServiceQueryOptions { + tag?: string; // service tags + credentials?: { + tag?: string; + }; +} + +export type UpsServiceQueryOprions = BaseServiceQueryOptions; + +export interface ServiceQueryOptions extends BaseServiceQueryOptions { + name?: string; //service type name + plan?: string; //Service plan +} + +function spotRedirectUri() { + // expected host pattern is 'DOMAIN.PLATFORM.REG...APPNAME.cloud.sap' + const urlInput: string = + _.get(process, "env.WS_BASE_URL") || + "https://wingtestsubacc-workspaces-ws-gwzd6.staging-01.dev10.int.webide.cloud.sap"; + const host = new URL(urlInput).hostname; + // "https://*.cry10.int.applicationstudio.cloud.sap/**" + return `https://*.${_.join(_.drop(_.split(host, ".")), ".")}/**`; +} + +export const ENV_VCAP_RESOURCES = "VCAP_SERVICES"; + +export function toText(e: Error): string { + return _.get(e, "message") || _.get(e, "name", _.toString(e)); +} + +export function getEnvResources( + envFilePath: string +): Promise<{ vcapObject: any; isQuotedVcap: boolean }> { + try { + let isQuotedVcap = false; + if (existsSync(envFilePath)) { + const envProperties = PropertiesReader(envFilePath); + let vcapProperty = envProperties.getRaw(ENV_VCAP_RESOURCES); + if (vcapProperty) { + // In case VCAP_SERVICES is wrapped with single quotes - unwrap it + if (_.startsWith(vcapProperty, "'") && _.endsWith(vcapProperty, "'")) { + vcapProperty = vcapProperty.substring(1, vcapProperty.length - 1); + isQuotedVcap = true; + } + return Promise.resolve({ + vcapObject: JSON.parse(vcapProperty), + isQuotedVcap, + }); + } else { + getModuleLogger(LOGGER_MODULE).debug( + "getEnvResources: the '.env' file is missing a key <%s>", + ENV_VCAP_RESOURCES, + { filePath: envFilePath } + ); + return Promise.resolve({ vcapObject: null, isQuotedVcap }); + } + } else { + getModuleLogger(LOGGER_MODULE).debug( + "getEnvResources: the '.env' file does not exist", + { + filePath: envFilePath, + } + ); + return Promise.resolve({ vcapObject: null, isQuotedVcap }); + } + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + getModuleLogger(LOGGER_MODULE).error( + "getEnvResources: could not get the '.env' file resources", + { exception: toText(new Error(error?.message as string)) }, + { filePath: envFilePath } + ); + throw error; + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function findServiceByResourceNameTag( + vcapServices: any, + yamlResourceName: string, + resourceTag: string +): any { + for (const key in vcapServices) { + for (const service of vcapServices[key]) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if ( + service.tags && + service.tags.find( + (t: string) => + t.startsWith(resourceTag) && + t.substr(resourceTag.length) === yamlResourceName + ) + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return [key, service]; + } + } + } + return undefined; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function removeResourceFromEnv( + bindContext: types.IBindContext +): Promise<{ resourceName: string; envPath: string; resourceData: any }> { + let instanceData; + const envFilePath: string = bindContext.envPath.fsPath; + const envResources = await getEnvResources(envFilePath); + const vcapServicesObj = envResources.vcapObject; + const bindResourceType = + bindContext.depContext.type === types.DependencyType.runtimeservice + ? bindContext.depContext.displayType + : bindContext.depContext.type; + let instanceName = ""; + // If this is a tagged resource - remove by tag + const resourceTag: string = _.get(bindContext, "depContext.data.resourceTag"); + const resourceName: string = _.get( + bindContext, + "depContext.data.resourceName" + ); + if (!_.isEmpty(resourceTag)) { + const keyAndService = findServiceByResourceNameTag( + vcapServicesObj, + resourceName, + resourceTag + ); + if (!_.isEmpty(keyAndService)) { + const resourceTypeKey = keyAndService[0]; + instanceData = keyAndService[1]; + instanceName = instanceData.instance_name; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + _.remove(vcapServicesObj[resourceTypeKey], (hanaService: any) => { + return hanaService.instance_name === instanceName; + }); + // If after removal no service instances left - remove this resource type completely to prevent something like: VCAP_SERVICES={"hana":[]} + if (_.isEmpty(vcapServicesObj[resourceTypeKey])) { + delete vcapServicesObj[resourceTypeKey]; + } + } else { + getModuleLogger(LOGGER_MODULE).debug( + "removeResourceFromEnv: the <%s> tagged <%s> resource was not found in the '.env' file", + resourceTag, + resourceName, + { filePath: envFilePath } + ); + } + } else { + // Remove by resource dependency type only + const serviceToDelete = _.get(vcapServicesObj, bindResourceType); + if (!_.isEmpty(serviceToDelete)) { + instanceData = serviceToDelete[0]; + instanceName = instanceData.instance_name; + delete vcapServicesObj[bindResourceType]; + } + } + + // Update VCAP_SERVICES in the .env file + const envProperties = PropertiesReader(envFilePath); + + // If VCAP_SERVICES was already wrapped with single quotes - maintain them when writing back + const quote = envResources.isQuotedVcap ? "'" : ""; + envProperties.set( + ENV_VCAP_RESOURCES, + quote + JSON.stringify(vcapServicesObj) + quote + ); + + await envProperties.save(envFilePath); + return { + resourceName: instanceName, + envPath: envFilePath, + resourceData: instanceData, + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function generateParams4Service( + serviceLabel: string, + plan: string +): any { + if ("xsuaa" === serviceLabel && "application" === plan) { + return { + xsappname: `xsuaa_${_.now()}`, + "tenant-mode": "dedicated", + description: "Security profile of called application", + scopes: [ + { + name: "uaa.user", + description: "UAA", + }, + ], + "role-templates": [ + { + name: "Token_Exchange", + description: "UAA", + "scope-references": ["uaa.user"], + }, + ], + "oauth2-configuration": { + "redirect-uris": [`${spotRedirectUri()}`], + }, + }; + } + return {}; +} + +function validateXsuaaTenantMode(value: string) { + if ( + !_.isUndefined(value) && + !["shared", "dedicated", "external"].includes(_.trim(value)) + ) { + throw new Error( + messages.error_service_params_value_not_allowed("tenant-mode") + ); + } +} + +function validateXsuaaOauth2Configuration(value: unknown) { + for (const uri of _.get(value, "redirect-uris", [])) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + if (!_.startsWith(uri, "http") && !_.startsWith(uri, "localhost")) { + throw new Error( + messages.error_service_params_value_not_allowed("redirect-uris") + ); + } + } +} + +function validateXsuaaName(value: string) { + value = _.trim(value); + if (!value) { + throw new Error(messages.error_service_params_mandatory("xsappname")); + } + if (_.size(value) >= 100) { + throw new Error(messages.error_service_params_max_length("xsappname", 100)); + } + if (/[^a-zA-Z0-9_\-"]/gm.test(value)) { + throw new Error( + messages.error_service_params_value_not_allowed("xsappname") + ); + } +} + +function validateParamsXsuaa(value: string): TypeValidationResult { + let result; + try { + const data = parse(_.trim(value)); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + validateXsuaaName(_.get(data, "xsappname")); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + validateXsuaaTenantMode(_.get(data, "tenant-mode")); + validateXsuaaOauth2Configuration(_.get(data, "oauth2-configuration")); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + result = toText(e); + } + return result; +} + +function validateParamsJson(value: string): TypeValidationResult { + let result; + try { + parse(_.trim(value)); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + result = toText(e); + } + return result; +} + +export function validateParams( + serviceLabel: string, + plan?: string +): (value: string) => TypeValidationResult { + return "xsuaa" === serviceLabel && "application" === plan + ? validateParamsXsuaa + : validateParamsJson; +} + +export function isRegexExpression(statement: string): boolean { + return /^\/\S+\/$/g.test(statement); +} + +export function composeFilterPattern(value: string): string { + return value + ? isRegexExpression(value) + ? _.trim(value, "/") + : `^${value}$` + : value; +} + +async function doGetUpsServiceInstances( + query?: IServiceQuery, + filterCredTag?: string +): Promise { + const upsServices = await cfGetUpsInstances(query); + const pattern = + _.size(upsServices) && filterCredTag + ? composeFilterPattern(filterCredTag) + : undefined; + const ups2Show = pattern + ? upsServices.filter((service) => { + // cretentials.tags can be 'string', 'string array' or 'array of objects' only + const tags = _.isString(service.credentials?.tags) + ? [service.credentials.tags] + : service.credentials?.tags; + return _.find(tags, (tag: string) => new RegExp(pattern).test(tag)); + }) + : upsServices; + return ups2Show; +} + +export async function getUpsServiceInstances( + options?: UpsServiceQueryOprions +): Promise { + return doGetUpsServiceInstances(undefined, options?.credentials?.tag); +} + +export async function getAllServiceInstances( + opts?: DisplayServices +): Promise { + let ups2Show: ServiceInstanceInfo[] = []; + if (opts?.ups?.isShow || opts?.ups?.tag) { + const copyQuery = _.cloneDeep(opts.query); + _.remove(copyQuery?.filters || [], (item) => { + return !_.includes( + [ + eFilters.names, + eFilters.space_guids, + eFilters.organization_guids, + eFilters.oder_by, + eFilters.page, + eFilters.per_page, + eFilters.updated_ats, + eFilters.created_ats, + ], + item.key + ); + }); + ups2Show = await doGetUpsServiceInstances(copyQuery, opts.ups.tag); + } + return _.concat(await cfGetManagedServiceInstances(opts?.query), ups2Show); +} + +const UTF8 = "utf8"; +const GITIGNORE = ".gitignore"; + +export async function updateGitIgnoreList(envPath: string): Promise { + const project = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(envPath)); + if (project) { + const isNotEmptyPattern = (value: string): boolean => { + return _.size(value) > 0 && !/^\s*#/.test(value); + }; + const isPatternFound = async (patterns: string[]): Promise => { + for (const pattern of patterns) { + if ( + isNotEmptyPattern(pattern) && + _.includes( + _.map( + await vscode.workspace.findFiles( + new vscode.RelativePattern(project, pattern) + ), + "fsPath" + ), + envPath + ) + ) { + return true; + } + } + return false; + }; + const ignoreFiles = await vscode.workspace.findFiles( + new vscode.RelativePattern(project, GITIGNORE) + ); + if (!_.size(ignoreFiles)) { + try { + // .gitignore file not exists -> create one + const filePath = path.normalize( + path.join(project.uri.fsPath, GITIGNORE) + ); + await fs.promises.open(filePath, "w"); + ignoreFiles.push(vscode.Uri.file(filePath)); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + getModuleLogger(LOGGER_MODULE).error( + "updateGitIgnoreList: creation .gitignore file failed", + { + exception: toText(new Error(e?.message as string)), + } + ); + } + } + for (const file of ignoreFiles) { + try { + const patterns = _.split( + await fs.promises.readFile(file.fsPath, { encoding: UTF8 }), + EOL + ); + if (!(await isPatternFound(patterns))) { + await fs.promises.writeFile( + file.fsPath, + _.join( + _.concat(patterns, [ + `# auto generated wildcard`, + _.replace( + path.relative(project.uri.fsPath, envPath), + /\\/g, + "/" + ), + ]), + EOL + ), + { encoding: UTF8 } + ); + } + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + getModuleLogger(LOGGER_MODULE).error( + "updateGitIgnoreList: update .gitignore file failed", + { exception: toText(new Error(e?.message as string)) }, + { filePath: file } + ); + } + } + } +} + +export async function writeProperties( + filePath: string, + properties: Record +): Promise { + let text = ""; + Object.keys(properties).forEach((key) => { + const value = properties[key]; + if (value) { + text += `${key}=${value.trim()}\n`; + } + }); + await fs.promises.writeFile(filePath, text); +} + +export function resolveFilterValue(value: string | undefined): string { + // allowed patterns: 'hana'|' "hana ", " xsuaa"'|'["xsuaa", "hana"]' + // eslint-disable-next-line @typescript-eslint/unbound-method + return _.join(_.map(_.isString(value) ? _.split(value, ",") : value, _.trim)); +} + +export function notifyWhenServicesInfoResultIncomplete( + list: Promise +): Promise { + const isUnknown = (value: string | undefined) => "unknown" === value; + return list.then((infos) => { + const results = _.reduce( + infos, + (res, info) => { + if (isUnknown(info.serviceName) || isUnknown(info.plan)) { + res.push(info.label); + } + return res; + }, + [] + ); + if (_.size(results)) { + void vscode.window.showWarningMessage( + messages.service_instances_list_incomplete(results) + ); + } + return infos; + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function examCFTarget( + errorMessage: string, + keys: string[], + weak: boolean +): Promise { + const target = await cfGetTarget(); + if (weak) { + return target; + } + return _.reduce( + keys, + (cond, key) => { + cond = cond && _.get(target, key); + return cond; + }, + true + ) + ? target + : Promise.reject(new Error(errorMessage)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function invokeLongFunctionWithProgress( + longFunction: any, + progressMessage: string +): Thenable { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: progressMessage, + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + }, + () => longFunction() + ); +} diff --git a/projects/cloud-foundry-tools/packages/backend/tests/cfView.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/cfView.spec.ts new file mode 100644 index 000000000..9f2ffe560 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/cfView.spec.ts @@ -0,0 +1,399 @@ +import { expect } from "chai"; +import * as _ from "lodash"; +import * as path from "path"; +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode } from "./ext/mockUtil"; + +mockVscode(nsVsMock.testVscode, "src/cfView.ts"); + +import * as cfView from "../src/cfView"; +import * as cfLocal from "@sap/cf-tools/out/src/cf-local"; +import { createSandbox, SinonMock, SinonSandbox } from "sinon"; +import type { TreeItem } from "vscode"; + +describe("cfView tests", () => { + let sandbox: SinonSandbox; + let cfLocalMock: SinonMock; + let cfViewMock: SinonMock; + + before(() => { + sandbox = createSandbox(); + }); + + after(() => { + sandbox.restore(); + }); + + beforeEach(() => { + cfViewMock = sandbox.mock(cfView.CFView); + cfLocalMock = sandbox.mock(cfLocal); + }); + + afterEach(() => { + cfLocalMock.verify(); + cfViewMock.verify(); + }); + + describe("CFTargetTI", () => { + const activePattern = "(Active Target)"; + const contextPattern = "cf-target"; + it("ok:: get description - active, not dirty", () => { + const item = new cfView.CFTargetTI({ + label: "test", + isCurrent: true, + isDirty: false, + }); + expect(item.description).to.be.equal(activePattern); + expect(item.contextValue).to.be.equal(`${contextPattern}-active`); + const parts = _.split(_.get(item, "iconPath.dark") as string, path.sep); + expect(_.includes(parts, "target-a.svg")).to.be.true; + }); + + it("ok:: get description - not active, is dirty", () => { + const item = new cfView.CFTargetTI({ + label: "test", + isCurrent: false, + isDirty: true, + }); + expect(item.description).to.be.equal("*"); + expect(item.contextValue).to.be.equal(`${contextPattern}`); + const parts = _.split(_.get(item, "iconPath.light") as string, path.sep); + expect(_.includes(parts, "target.svg")).to.be.true; + }); + + it("ok:: get description - (no targets), is active, is dirty", () => { + const item = new cfView.CFTargetTI({ + label: "(no targets)", + isCurrent: true, + isDirty: true, + }); + expect(item.description).to.be.equal(`*`); + expect(item.contextValue).to.be.equal(`${contextPattern}-notargets`); + expect(item.tooltip).to.be.equal(item.label); + const parts = _.split(_.get(item, "iconPath.dark") as string, path.sep); + expect(_.includes(parts, "target-a.svg")).to.be.true; + }); + }); + + describe("Tree Nodes", () => { + const root = new cfView.CFTargetTI({ + label: "(no targets)", + isCurrent: true, + isDirty: true, + }); + const parent = new cfView.CFFolder("folder", root); + + it("ok:: CFLoginNode properties verifying", () => { + const item = new cfView.CFLoginNode(parent); + expect(item.contextValue).to.be.equal(`cf-login-required`); + expect(item.label).to.be.equal(`Login required`); + expect(item.tooltip).to.be.equal(`Log in to see details`); + const parts = _.split(_.get(item, "iconPath.dark") as string, path.sep); + expect(_.includes(parts, "info.svg")).to.be.true; + expect(item.parent).to.be.equal(parent); + }); + + it("ok:: CFTargetNotCurrent properties verifying", () => { + const target = "my-target"; + const item = new cfView.CFTargetNotCurrent(parent, target); + expect(item.contextValue).to.be.equal(`cf-target-not-current`); + expect(item.label).to.be.equal(`Target needs to be active`); + expect(item.tooltip).to.be.equal( + `Set the '${target}' as the active target to see details.` + ); + const parts = _.split(_.get(item, "iconPath.light") as string, path.sep); + expect(_.includes(parts, "info.svg")).to.be.true; + expect(item.parent).to.be.equal(parent); + }); + + it("ok:: CFService properties verifying", () => { + const name = "test-service"; + const type = "xsuaa"; + const item = new cfView.CFService(name, type, parent); + expect(item.contextValue).to.be.equal(`cf-service`); + expect(item.label).to.be.equal(name); + expect(item.type).to.be.equal(type); + expect(item.parent).to.be.equal(parent); + }); + + it("ok:: CFApplication properties verifying", () => { + const name = "test-app"; + const state = "STOPPED"; + const item = new cfView.CFApplication(name, state, parent); + expect(item.contextValue).to.be.equal(`cf-application`); + expect(item.label).to.be.equal(name); + expect(item.state).to.be.equal(state); + expect(item.parent).to.be.equal(parent); + }); + + it("ok:: CFServicesFolder properties verifying", () => { + const name = "services"; + const parent = new cfView.CFTargetTI({ + label: "parent", + isCurrent: false, + isDirty: true, + }); + const item = new cfView.CFServicesFolder(name, parent); + expect(item.contextValue).to.be.equal(`services`); + expect(item.label).to.be.equal(name); + expect(item.tooltip).to.be.equal(item.label); + }); + + it("ok:: CFAppsFolder properties verifying", () => { + const name = "apps"; + const parent = new cfView.CFTargetTI({ + label: "parent", + isCurrent: false, + isDirty: true, + }); + const item = new cfView.CFAppsFolder(name, parent); + expect(item.contextValue).to.be.equal(`apps`); + expect(item.label).to.be.equal(name); + expect(item.tooltip).to.be.equal(item.label); + }); + }); + + describe("CFView ", () => { + const root = new cfView.CFTargetTI({ + label: "(no targets)", + isCurrent: true, + isDirty: true, + }); + const folder = new cfView.CFFolder("folder", root); + + let instance: cfView.CFView; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const testContext: any = { + subscriptions: [], + logUri: { fsPath: path.resolve(__dirname) }, + }; + beforeEach(() => { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + instance = new cfView.CFView(testContext, ""); + }); + + it("ok:: verify getParent of CFAppsFolder", () => { + const parent = new cfView.CFTargetTI({ + label: "parent", + isCurrent: false, + isDirty: true, + }); + const item = new cfView.CFAppsFolder("name", parent); + expect(instance.getParent(item)).to.be.equal(parent); + }); + + it("ok:: verify getParent of CFServicesFolder", () => { + const parent = new cfView.CFTargetTI({ + label: "parent", + isCurrent: false, + isDirty: true, + }); + const item = new cfView.CFServicesFolder("name", parent); + expect(instance.getParent(item)).to.be.equal(parent); + }); + + it("ok:: verify getParent of CFApplication", () => { + expect( + instance.getParent(new cfView.CFApplication("name", "state", folder)) + ).to.be.equal(folder); + }); + + it("ok:: verify getTreeItem of CFAppsFolder", async () => { + const parent = new cfView.CFTargetTI({ + label: "parent", + isCurrent: false, + isDirty: true, + }); + const item = new cfView.CFAppsFolder("name", parent); + expect(await instance.getTreeItem(item)).to.be.equal(item); + }); + + it("ok:: verify getTreeItem of CFApplication", async () => { + const name = "test-app"; + const state = "STOPPED"; + const item = new cfView.CFApplication(name, state, folder); + const viewItem = await instance.getTreeItem(item); + expect(viewItem.label).to.be.equal(`${name} (${state})`); + expect(instance.getParent(item)).to.be.equal(folder); + }); + + it("ok:: verify getTreeItem of CFService", async () => { + const name = "test-app"; + const type = "type"; + const item = new cfView.CFService(name, type, folder); + const viewItem = await instance.getTreeItem(item); + expect(viewItem.label).to.be.equal(`${name} (${type})`); + expect(instance.getParent(item)).to.be.equal(folder); + }); + + it("ok:: verify CFFolder contextValue is not defined", async () => { + const name = "test-app"; + const item = new cfView.CFFolder(name, root); + const viewItem = await instance.getTreeItem(item); + expect(viewItem.label).to.be.equal(`${name}`); + expect(viewItem.tooltip).to.be.equal(`${name}`); + expect(instance.getParent(item)).to.be.equal(root); + }); + + it("ok:: verify getChildren - root", async () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + cfLocalMock.expects("cfGetTargets").resolves(targets); + expect( + await instance.getChildren(undefined as unknown as TreeItem) + ).to.deep.equal( + _.map(targets, (target) => new cfView.CFTargetTI(target)) + ); + }); + + it("ok:: verify getChildren - target, not active", async () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + const expected = [ + new cfView.CFServicesFolder("Services", instance.targets[0]), + new cfView.CFAppsFolder("Applications", instance.targets[0]), + ]; + expect(await instance.getChildren(instance.targets[0])).to.deep.equal( + expected + ); + }); + + it("ok:: verify getChildren - target/services, not active", async () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + const parent = new cfView.CFServicesFolder( + "Services", + instance.targets[0] + ); + const expected = [ + new cfView.CFTargetNotCurrent( + parent, + _.get(parent, "parent.target.label") as string + ), + ]; + expect(await instance.getChildren(parent)).to.deep.equal(expected); + }); + + it("ok:: verify getChildren - target/services, is active", async () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + const parent = new cfView.CFServicesFolder( + "Services", + instance.targets[1] + ); + const services = [ + { label: "s-1", serviceName: "name-1" }, + { label: "s-2", serviceName: "name-2" }, + ]; + cfLocalMock.expects("cfGetServiceInstancesList").resolves(services); + const expected = _.map( + services, + (service) => + new cfView.CFService(service.label, service.serviceName, parent) + ); + expect(await instance.getChildren(parent)).to.deep.equal(expected); + }); + + it("ok:: verify getChildren - target/applications, is active", async () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + const parent = new cfView.CFAppsFolder("Apps", instance.targets[1]); + const apps = [ + { name: "a-1", state: "STARTED" }, + { name: "a-2", state: "STOPPED" }, + ]; + cfLocalMock.expects("cfGetApps").resolves(apps); + const expected = _.map( + apps, + (app) => new cfView.CFApplication(app.name, app.state, parent) + ); + expect(await instance.getChildren(parent)).to.deep.equal(expected); + }); + + it("ok:: verify getChildren - target/applications, log in required", async () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + const parent = new cfView.CFAppsFolder("Apps", instance.targets[1]); + cfLocalMock.expects("cfGetApps").rejects(new Error("error")); + expect(await instance.getChildren(parent)).to.deep.equal([ + new cfView.CFLoginNode(parent), + ]); + }); + + it("ok:: verify getChildren - target/applications, incorrect hierachy", async () => { + expect( + await instance.getChildren(new nsVsMock.testVscode.TreeItem("Apps")) + ).to.deep.equal([]); + }); + + it("ok:: verify getCurrentTarget, active is defined", () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + expect(instance.getCurrentTarget()).to.be.equal( + instance.targets[1].target + ); + }); + + it("ok:: verify getCurrentTarget, active is not defined", () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: false, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + expect(instance.getCurrentTarget()).to.be.equal(undefined); + }); + + it("ok:: verify getTargets", () => { + const targets = [ + { label: "t-1", isCurrent: false, isDirty: false }, + { label: "t-2", isCurrent: true, isDirty: false }, + ]; + instance.targets = _.map( + targets, + (target) => new cfView.CFTargetTI(target) + ); + expect(instance.getTargets()).to.deep.equal(instance.targets); + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/cfViewCommands.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/cfViewCommands.spec.ts new file mode 100644 index 000000000..12d347f96 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/cfViewCommands.spec.ts @@ -0,0 +1,1653 @@ +import { expect, assert } from "chai"; +import * as _ from "lodash"; +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode } from "./ext/mockUtil"; + +nsVsMock.setTestRoots(["/", "/some_path"]); + +mockVscode(nsVsMock.testVscode, "src/cfViewCommands.ts"); +mockVscode(nsVsMock.testVscode, "src/logger/logger-wrapper.ts"); + +import * as cfViewCommands from "../src/cfViewCommands"; +import * as commands from "../src/commands"; +import * as cfLocal from "@sap/cf-tools/out/src/cf-local"; +import * as cfLocalUtils from "@sap/cf-tools/out/src/utils"; +import { + ServiceTypeInfo, + ServiceInstanceInfo, + Cli, + DEFAULT_TARGET, + CFTarget, + IServiceQuery, + eFilters, + PlanInfo, + CF_PAGE_SIZE, + eServiceTypes, +} from "@sap/cf-tools"; +import * as cfView from "../src/cfView"; +import * as https from "https"; +import * as fspath from "path"; +import { messages } from "../src/messages"; +import { fail } from "assert"; +import { + DisplayServices, + ServiceQueryOptions, + UpsServiceQueryOprions, +} from "../src/utils"; +import * as chisel from "../src/chisel"; +import { createSandbox, SinonMock, SinonSandbox } from "sinon"; + +describe("cfViewCommands tests", () => { + let sandbox: SinonSandbox; + let vscodeWindowMock: SinonMock; + let vscodeWorkspaceMock: SinonMock; + let cfViewCommandsMock: SinonMock; + let commandsMock: SinonMock; + let cliMock: SinonMock; + let httpsMock: SinonMock; + let cfLocalMock: SinonMock; + let cfLocalUtilsMock: SinonMock; + let cfViewMock: SinonMock; + let chiselMock: SinonMock; + const cliResult = { exitCode: 0, stdout: "", stderr: "" }; + + before(() => { + sandbox = createSandbox(); + }); + + beforeEach(() => { + cfViewMock = sandbox.mock(cfView.CFView); + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + vscodeWorkspaceMock = sandbox.mock(nsVsMock.testVscode.workspace); + cfViewCommandsMock = sandbox.mock(cfViewCommands); + cfLocalMock = sandbox.mock(cfLocal); + cfLocalUtilsMock = sandbox.mock(cfLocalUtils); + cliMock = sandbox.mock(Cli); + commandsMock = sandbox.mock(commands); + httpsMock = sandbox.mock(https); + chiselMock = sandbox.mock(chisel); + }); + + afterEach(() => { + sandbox.restore(); + cfLocalMock.verify(); + cfLocalUtilsMock.verify(); + vscodeWindowMock.verify(); + vscodeWorkspaceMock.verify(); + cfViewCommandsMock.verify(); + cliMock.verify(); + cfViewMock.verify(); + httpsMock.verify(); + commandsMock.verify(); + chiselMock.verify(); + }); + + it("cmdReloadTargets", async () => { + cfViewMock.expects("get").returns({ + refresh: () => { + return; + }, + }); + await cfViewCommands.cmdReloadTargets(); + }); + + describe("cfDeployServiceAPI", () => { + const testToken = " \ntestToken\n"; + const errorText = "my error"; + const result = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + on: (type: string, callback: any) => callback(new Error(errorText)), + }; + const urlPath = "url/path"; + + it("exception:: https get on - error", async () => { + const ap = "https://api.cf.sap.hana.ondemand.com"; + const target = new URL(_.replace(ap, "api.cf", "deploy-service.cfapps")); + const options = { + protocol: target.protocol, + host: target.host, + port: 443, + method: "get", + path: target.pathname, + headers: { + Authorization: testToken.replace("\n", "").replace("\n", ""), + }, + }; + cfLocalUtilsMock + .expects("cfGetConfigFileField") + .withExactArgs("Target") + .resolves(ap); + httpsMock.expects("get").withArgs(options).returns(result); + cfLocalMock.expects("cfGetAuthToken").resolves(testToken); + try { + await cfViewCommands.cfDeployServiceAPI(""); + fail("should not reach here"); + } catch (e) { + expect(e).to.equal(errorText); + } + }); + + it("exception:: https get on - error, url parse data", async () => { + const ap = "https://api.cf.sap.hana.ondemand.com:8090"; + const target = new URL(_.replace(ap, "api.cf", "deploy-service.cfapps")); + const options = { + protocol: target.protocol, + host: target.host, + port: target.port, + method: "get", + path: urlPath, + headers: { + Authorization: testToken.replace("\n", "").replace("\n", ""), + }, + }; + cfLocalUtilsMock + .expects("cfGetConfigFileField") + .withExactArgs("Target") + .resolves(ap); + httpsMock.expects("get").withArgs(options).returns(result); + cfLocalMock.expects("cfGetAuthToken").resolves(testToken); + try { + await cfViewCommands.cfDeployServiceAPI(urlPath); + fail("should not reach here"); + } catch (e) { + expect(e).to.equal(errorText); + } + }); + + it("exception:: https get on - error, url parse data, default port", async () => { + const ap = "http://api.cf.sap.hana.ondemand.com"; + const target = new URL(_.replace(ap, "api.cf", "deploy-service.cfapps")); + const options = { + protocol: target.protocol, + host: target.host, + port: 80, + method: "get", + path: urlPath, + headers: { + Authorization: testToken.replace("\n", "").replace("\n", ""), + }, + }; + cfLocalUtilsMock + .expects("cfGetConfigFileField") + .withExactArgs("Target") + .resolves(ap); + httpsMock.expects("get").withArgs(options).returns(result); + cfLocalMock.expects("cfGetAuthToken").resolves(testToken); + try { + await cfViewCommands.cfDeployServiceAPI(urlPath); + fail("should not reach here"); + } catch (e) { + expect(e).to.equal(errorText); + } + }); + + it("exception:: no target", async () => { + cfLocalUtilsMock + .expects("cfGetConfigFileField") + .withExactArgs("Target") + .resolves([]); + try { + await cfViewCommands.cfDeployServiceAPI(""); + fail("should not reach here"); + } catch (e) { + expect(e.message).to.equal(messages.no_cf_api_endpoint); + } + }); + }); + + describe("cmdDeployServiceAPI", () => { + it("ok:: empty args", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({ data: {} }); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.load_data_from_deploy, + cancellable: false, + }) + .resolves(); + await cfViewCommands.cmdDeployServiceAPI("", ""); + }); + + it("ok:: empty args, connectivity canceled", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.load_data_from_deploy, + cancellable: false, + }) + .never(); + await cfViewCommands.cmdDeployServiceAPI("", ""); + }); + + it("ok:: with specified message", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({ data: {} }); + const message = "myMessage"; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: message, + cancellable: false, + }) + .resolves(); + await cfViewCommands.cmdDeployServiceAPI("", message); + }); + }); + + describe("cmdSetCurrentTarget scope", () => { + const currentTarget: CFTarget = { + label: "current Target", + isDirty: false, + isCurrent: true, + }; + const localView = { + getCurrentTarget: () => currentTarget, + refresh: () => "", + }; + + it("ok:: target is current already, nothing done", async () => { + cliMock.expects("execute").never(); + cfViewMock.expects("get").never(); + await cfViewCommands.cmdSetCurrentTarget( + new cfView.CFTargetTI({ + label: "my Target", + isDirty: false, + isCurrent: true, + }) + ); + }); + + it("ok:: target is set, reload called", async () => { + const target = { label: "my Target", isDirty: false, isCurrent: false }; + cfViewMock.expects("get").twice().returns(localView); + cliMock + .expects("execute") + .withExactArgs(["set-target", "-f", target.label]) + .resolves(cliResult); + await cfViewCommands.cmdSetCurrentTarget(new cfView.CFTargetTI(target)); + }); + + it("ok:: target is current, is dirty, not save - No", async () => { + const target = { label: "my Target", isDirty: false, isCurrent: false }; + currentTarget.isCurrent = true; + currentTarget.isDirty = true; + cfViewMock.expects("get").twice().returns(localView); + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.target_dirty_save(currentTarget.label), + "Yes", + "No", + "Cancel" + ) + .resolves("No"); + cliMock + .expects("execute") + .withExactArgs(["set-target", "-f", target.label]) + .resolves(cliResult); + await cfViewCommands.cmdSetCurrentTarget(new cfView.CFTargetTI(target)); + }); + + it("ok:: target is current, is dirty, not save - Cancel", async () => { + const target = { label: "my Target", isDirty: false, isCurrent: false }; + currentTarget.isCurrent = true; + currentTarget.isDirty = true; + cfViewMock.expects("get").returns(localView); + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.target_dirty_save(currentTarget.label), + "Yes", + "No", + "Cancel" + ) + .resolves(undefined); + cliMock + .expects("execute") + .withExactArgs(["set-target", "-f", target.label]) + .never(); + await cfViewCommands.cmdSetCurrentTarget(new cfView.CFTargetTI(target)); + }); + + it("ok:: target is current, is dirty, save - Yes", async () => { + const target = { label: "my Target", isDirty: false, isCurrent: false }; + currentTarget.isCurrent = true; + currentTarget.isDirty = true; + cfViewMock.expects("get").twice().returns(localView); + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.target_dirty_save(currentTarget.label), + "Yes", + "No", + "Cancel" + ) + .resolves("Yes"); + cliMock + .expects("execute") + .withExactArgs(["set-target", "-f", target.label]) + .resolves(cliResult); + cliMock + .expects("execute") + .withExactArgs(["save-target"]) + .resolves(cliResult); + await cfViewCommands.cmdSetCurrentTarget(new cfView.CFTargetTI(target)); + }); + + it("exception:: target is current, exception thrown", async () => { + currentTarget.isCurrent = true; + currentTarget.isDirty = true; + cfViewMock.expects("get").returns(localView); + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.target_dirty_save(currentTarget.label), + "Yes", + "No", + "Cancel" + ) + .resolves("Yes"); + const error = new Error("my error"); + cliMock.expects("execute").withExactArgs(["save-target"]).throws(error); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(error.message) + .resolves(); + await cfViewCommands.cmdSetCurrentTarget( + new cfView.CFTargetTI({ + label: "my Target", + isDirty: false, + isCurrent: false, + }) + ); + }); + + it("fail:: target is current, not dirty, execute failed", async () => { + const target = { label: "my Target", isDirty: false, isCurrent: false }; + currentTarget.isCurrent = true; + currentTarget.isDirty = false; + cfViewMock.expects("get").returns(localView); + cliResult.exitCode = 1; + cliResult.stdout = "any error"; + cliMock + .expects("execute") + .withExactArgs(["set-target", "-f", target.label]) + .resolves(cliResult); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(cliResult.stdout) + .resolves(); + await cfViewCommands.cmdSetCurrentTarget(new cfView.CFTargetTI(target)); + }); + + it("ok:: 'CFTargetNotCurrent' item received", async () => { + cliMock.expects("execute").never(); + await cfViewCommands.cmdSetCurrentTarget( + new cfView.CFTargetNotCurrent( + new cfView.CFAppsFolder( + "apps", + new cfView.CFTargetTI({ + label: "my Target", + isDirty: false, + isCurrent: true, + }) + ) + ) + ); + }); + + it("ok:: targets tree broken", async () => { + await cfViewCommands.cmdSetCurrentTarget( + new cfView.CFTargetNotCurrent({ label: "label" } as cfView.CFFolder) + ); + }); + + it("ok:: null received, targets tree broken", async () => { + await cfViewCommands.cmdSetCurrentTarget(undefined); + }); + }); + + describe("execSetTarget scope", () => { + const item = new cfView.CFTargetTI({ + label: "my Target", + isCurrent: false, + isDirty: true, + }); + + it("ok:: silent mode required", async () => { + const cliResult = { exitCode: -1, stdout: "", stderr: "" }; + cliMock + .expects("execute") + .withExactArgs(["set-target", "-f", item.target.label]) + .resolves(cliResult); + vscodeWindowMock.expects("showErrorMessage").never(); + await cfViewCommands.execSetTarget(item, { silent: true }); + }); + + it("ok:: 'skip-reload' mode required", async () => { + const cliResult = { exitCode: 0, stdout: "", stderr: "" }; + cliMock + .expects("execute") + .withExactArgs(["set-target", "-f", item.target.label]) + .resolves(cliResult); + cfViewMock.expects("get").never(); + await cfViewCommands.execSetTarget(item, { + silent: true, + "skip-reload": true, + }); + }); + }); + + describe("execSaveTarget scope", () => { + const item = new cfView.CFTargetTI({ + label: "my Target", + isCurrent: false, + isDirty: true, + }); + + it("ok:: error occured", async () => { + const cliResult = { exitCode: -1, stdout: "", stderr: "" }; + cliMock + .expects("execute") + .withExactArgs(["save-target", "-f", item.target.label]) + .resolves(cliResult); + vscodeWindowMock.expects("showErrorMessage").resolves(); + await cfViewCommands.execSaveTarget(item); + }); + + it("ok:: silent mode required", async () => { + const cliResult = { exitCode: -1, stdout: "", stderr: "" }; + cliMock + .expects("execute") + .withExactArgs(["save-target", "-f", item.target.label]) + .resolves(cliResult); + vscodeWindowMock.expects("showErrorMessage").never(); + await cfViewCommands.execSaveTarget(item, { silent: true }); + }); + + it("ok:: without args", async () => { + const cliResult = { exitCode: 0, stdout: "", stderr: "" }; + cliMock + .expects("execute") + .withExactArgs(["save-target"]) + .resolves(cliResult); + await cfViewCommands.execSaveTarget(); + }); + + it("ok:: 'no-targets' node received", async () => { + cliMock.expects("execute").never(); + await cfViewCommands.execSaveTarget( + new cfView.CFTargetTI({ + label: "my (no targets)", + isCurrent: false, + isDirty: true, + }) + ); + }); + }); + + describe("cmdDeleteTarget scope", () => { + const localView = { + refresh: () => "", + }; + const item = new cfView.CFTargetTI({ + label: "other", + isCurrent: true, + isDirty: false, + }); + + it("ok:: default-target", async () => { + cliMock.expects("execute").withArgs(["delete-target"]).never(); + await cfViewCommands.cmdDeleteTarget( + new cfView.CFTargetTI({ + label: DEFAULT_TARGET, + isCurrent: true, + isDirty: false, + }) + ); + }); + + it("ok:: other deleted", async () => { + cliResult.exitCode = 0; + cliResult.stdout = ""; + cliMock + .expects("execute") + .withArgs(["delete-target", item.target.label]) + .resolves(cliResult); + cfViewMock.expects("get").returns(localView); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.target_deleted(item.target.label)) + .resolves(); + await cfViewCommands.cmdDeleteTarget(item); + }); + + it("ok:: other deleted, silent and 'skip-reload' mode", async () => { + cliResult.exitCode = 0; + cliResult.stdout = ""; + cliMock + .expects("execute") + .withArgs(["delete-target", item.target.label]) + .resolves(cliResult); + cfViewMock.expects("get").never(); + vscodeWindowMock.expects("showInformationMessage").never(); + await cfViewCommands.cmdDeleteTarget(item, { + silent: true, + "skip-reload": true, + }); + }); + + it("ok:: other failure", async () => { + cliResult.stdout = "some error during delete"; + cliResult.exitCode = -1; + cliMock + .expects("execute") + .withArgs(["delete-target", item.target.label]) + .resolves(cliResult); + cfViewMock.expects("get").never(); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(cliResult.stdout) + .resolves(); + await cfViewCommands.cmdDeleteTarget(item); + }); + }); + + describe("cmdGetSpaceServices scope", () => { + it("ok:: verify args", async () => { + const title = "progress title"; + const query: IServiceQuery = { + filters: [ + { key: eFilters.names, value: "@^%$_^%+-_jlkj" }, + { key: eFilters.service_broker_names, value: "+-QWERE_+%?>jlkj" }, + ], + }; + const modified = _.cloneDeep(query); + _.each(modified.filters, (filter) => { + filter.value = encodeURIComponent(filter.value); + }); + const arg = { query: modified, ups: { isShow: true } }; + commandsMock + .expects("getAvailableServices") + .withExactArgs(arg, title) + .resolves(); + await cfViewCommands.cmdGetSpaceServices(query, title); + }); + + it("ok:: undefined query", async () => { + const title = "progress title"; + const arg: unknown = { query: undefined, ups: { isShow: true } }; + commandsMock + .expects("getAvailableServices") + .withExactArgs(arg, title) + .resolves(); + await cfViewCommands.cmdGetSpaceServices(undefined, title); + }); + }); + + describe("cmdGetServiceInstances scope", () => { + const title = "progress title - retrieve hana services"; + + it("ok:: retrieve all services", async () => { + const expServices: ServiceInstanceInfo[] = [ + { label: "s1", serviceName: "hana" }, + { label: "s2", serviceName: "hana" }, + { label: "s2", serviceName: "hanatrial" }, + ]; + commandsMock + .expects("getServiceInstances") + .withExactArgs(undefined, title) + .resolves(expServices); + expect( + await cfViewCommands.cmdGetServiceInstances(undefined, title) + ).to.deep.equal(expServices); + }); + + it("ok:: retrieve 'hana 'services types with 'hdi-shared' plan", async () => { + const rspServices: ServiceInstanceInfo[] = [ + { label: "s1", serviceName: "hana", plan: "hdi-shared" }, + { label: "s2", serviceName: "hana", plan: "hdi-shared" }, + ]; + const serviceQueryOptions: ServiceQueryOptions = { + name: "hana", + plan: "hdi-shared", + }; + const plans: PlanInfo[] = [ + { label: "hdi-shared", guid: "ABCD", description: "" }, + { label: "lite", guid: "EFGH", description: "" }, + ]; + const serviceInstanceQuery = { + filters: [ + { key: eFilters.service_plan_names, value: serviceQueryOptions.plan }, + ], + per_page: CF_PAGE_SIZE, + }; + commandsMock + .expects("fetchServicePlanList") + .withExactArgs({ + filters: [ + { + key: eFilters.service_offering_names, + value: serviceQueryOptions.name, + }, + ], + }) + .resolves(plans); + serviceInstanceQuery.filters = _.concat(serviceInstanceQuery.filters, [ + { + key: eFilters.service_plan_guids, + value: _.join(_.map(plans, "guid")), + }, + ]); + commandsMock + .expects("getServiceInstances") + .withExactArgs(serviceInstanceQuery, title) + .resolves(rspServices); + await cfViewCommands.cmdGetServiceInstances(serviceQueryOptions, title); + }); + + it("ok:: retrieve all services when service types and plan are missing", async () => { + const expServices: ServiceInstanceInfo[] = [ + { label: "s1", serviceName: "hana" }, + { label: "s2", serviceName: "hana" }, + { label: "s2", serviceName: "hanatrial" }, + ]; + const serviceQueryOptions: ServiceQueryOptions = {}; + commandsMock + .expects("getServiceInstances") + .withExactArgs(undefined, title) + .resolves(expServices); + await cfViewCommands.cmdGetServiceInstances(serviceQueryOptions, title); + }); + }); + + describe("cmdGetUpsServiceInstances scope", () => { + const title = "progress title - retrieve all services"; + + it("ok:: verify calling 'getUserProvidedServiceInstances' happens with specified args", async () => { + const options: UpsServiceQueryOprions = { + tag: "tags", + credentials: { tag: "hana" }, + }; + commandsMock + .expects("getUserProvidedServiceInstances") + .withExactArgs(options, title) + .resolves(); + await cfViewCommands.cmdGetUpsServiceInstances(options, title); + }); + }); + + describe("cmdBindLocal scope", () => { + const path = nsVsMock.testVscode.Uri.file("some/path"); + const service: ServiceTypeInfo[] = [ + { name: "", plan: "hdi-shared", prompt: "", tag: "" }, + ]; + let origin: unknown; + const plans: PlanInfo[] = [ + { label: "hdi-shared", guid: "ABCD", description: "" }, + { label: "hana", guid: "EFGH", description: "" }, + ]; + const services: ServiceInstanceInfo[] = [ + { serviceName: "type1", label: "service1" }, + { serviceName: "type2", label: "service2" }, + ]; + const opts: DisplayServices = { + query: { + filters: [{ key: eFilters.service_plan_names, value: service[0].plan }], + per_page: CF_PAGE_SIZE, + }, + ups: _.get(service, ["0", "ups"]), + }; + + before(() => { + origin = commands.updateInstanceNameAndTags; + }); + + beforeEach(() => { + _.set( + commands, + "updateInstanceNameAndTags", + ( + availableServices: ServiceInstanceInfo[], + serviceTypeInfo: ServiceTypeInfo[], + instanceNames: string[] + ) => { + instanceNames.push(services[0].label); + return Promise.resolve(instanceNames[0]); + } + ); + }); + + afterEach(() => { + _.set(commands, "updateInstanceNameAndTags", origin); + }); + + it("ok:: select path canceled", async () => { + const emptyPath = nsVsMock.testVscode.Uri.file(""); + vscodeWindowMock + .expects("showOpenDialog") + .withExactArgs({ + openLabel: "Select folder for .env file", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: nsVsMock.testVscode.workspace.workspaceFolders[0].uri, + }) + .resolves(); + expect(await cfViewCommands.cmdBindLocal(service, emptyPath)).to.be + .undefined; + }); + + it("ok:: path selected, service is CFService type, quote-vcap is false", async () => { + const emptyPath = nsVsMock.testVscode.Uri.file(""); + sandbox + .stub(nsVsMock.testVscode.workspace, "workspaceFolders") + .value(undefined); + vscodeWindowMock + .expects("showOpenDialog") + .withExactArgs({ + openLabel: "Select folder for .env file", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: nsVsMock.testVscode.workspace.workspaceFolders, + }) + .resolves([{ fsPath: `other${fspath.sep}path` }]); + const otherService = { contextValue: "cf-service", label: "my-service" }; + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(otherService.label)) + .resolves(); + const stubWithProgress = sandbox.stub( + nsVsMock.testVscode.window, + "withProgress" + ); + expect( + await cfViewCommands.cmdBindLocal( + otherService as unknown as cfView.CFService, + { + path: emptyPath, + ignore: true, + }, + undefined, + { "quote-vcap": false } + ) + ).deep.equal({ instanceName: otherService.label }); + + cfLocalMock + .expects("cfBindLocalServices") + .withExactArgs( + `other${fspath.sep}path${fspath.sep}.env`, + [otherService.label], + undefined, + undefined, + undefined, + false + ) + .resolves(); + expect(stubWithProgress.calledOnce).to.be.true; + expect(stubWithProgress.args[0].length).to.be.equal(2); + expect(stubWithProgress.args[0][0]).to.be.deep.equal({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + expect(await stubWithProgress.args[0][1]({} as any, {} as any)).to.be + .undefined; + }); + + it("ok:: path selected, service is CFService type", async () => { + const emptyPath = nsVsMock.testVscode.Uri.file(""); + sandbox + .stub(nsVsMock.testVscode.workspace, "workspaceFolders") + .value(undefined); + vscodeWindowMock + .expects("showOpenDialog") + .withExactArgs({ + openLabel: "Select folder for .env file", + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: nsVsMock.testVscode.workspace.workspaceFolders, + }) + .resolves([{ fsPath: `other${fspath.sep}path` }]); + const otherService = { contextValue: "cf-service", label: "my-service" }; + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(otherService.label)) + .resolves(); + const stubWithProgress = sandbox.stub( + nsVsMock.testVscode.window, + "withProgress" + ); + expect( + await cfViewCommands.cmdBindLocal( + otherService as unknown as cfView.CFService, + { + path: emptyPath, + ignore: true, + } + ) + ).deep.equal({ instanceName: otherService.label }); + + cfLocalMock + .expects("cfBindLocalServices") + .withExactArgs( + `other${fspath.sep}path${fspath.sep}.env`, + [otherService.label], + undefined, + undefined, + undefined, + undefined + ) + .resolves(); + expect(stubWithProgress.calledOnce).to.be.true; + expect(stubWithProgress.args[0].length).to.be.equal(2); + expect(stubWithProgress.args[0][0]).to.be.deep.equal({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + expect(await stubWithProgress.args[0][1]({} as any, {} as any)).to.be + .undefined; + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, no services found", async () => { + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.no_services_instances_found) + .resolves(); + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves([]); + expect(await cfViewCommands.cmdBindLocal(service, { path, ignore: true })) + .to.be.undefined; + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, no services found, creation allowed", async () => { + const cloneService = _.merge(_.cloneDeep(service[0]), { + allowCreate: { name: "create" }, + }); + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves([]); + commandsMock + .expects("updateInstanceNameAndTags") + .withExactArgs( + [ + { + label: + commands.CMD_BIND_TO_DEFAULT_SERVICE + + cloneService.allowCreate.name, + plan: cloneService.allowCreate.plan, + serviceName: cloneService.allowCreate.serviceName ?? "", + }, + { + label: commands.CMD_CREATE_SERVICE, + serviceName: "", + }, + ], + cloneService, + [], + [] + ) + .resolves(undefined); + expect( + await cfViewCommands.cmdBindLocal([cloneService], { + path, + ignore: true, + }) + ).to.be.undefined; + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, services found, canceled", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + _.set(commands, "updateInstanceNameAndTags", () => { + return Promise.resolve(); + }); + expect(await cfViewCommands.cmdBindLocal(service, { path, ignore: true })) + .to.be.undefined; + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, services found, type selected", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[0].label)) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal(service, { path, ignore: true }) + ).deep.equal({ + instanceName: services[0].label, + }); + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, services found, required specific service", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + commandsMock.expects("updateInstanceNameAndTags").never(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[0].label)) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal( + service, + { path, ignore: true }, + services[0].label + ) + ).deep.equal({ + instanceName: services[0].label, + }); + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, services found, required service not found", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + commandsMock.expects("updateInstanceNameAndTags").never(); + cfLocalMock + .expects("cfGetInstanceMetadata") + .withExactArgs("not-existed") + .rejects(new Error("not found")); + vscodeWindowMock + .expects("showErrorMessage") + .withArgs(messages.no_services_instance_byname_found("not-existed")) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal( + service, + { path, ignore: true }, + "not-existed" + ) + ).to.be.undefined; + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, service not in list but found via direct lookup (CF eventual consistency)", async () => { + // Simulate CF list API returning stale results: services[0] missing, only services[1] present + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves([services[1]]); + commandsMock.expects("updateInstanceNameAndTags").never(); + cfLocalMock + .expects("cfGetInstanceMetadata") + .withExactArgs(services[0].label) + .resolves({ + serviceName: services[0].label, + service: service[0].plan, + plan_guid: "GUID", + }); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[0].label)) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal( + service, + { path, ignore: true }, + services[0].label + ) + ).deep.equal({ + instanceName: services[0].label, + }); + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no services, services found, ups selected, quote-vcap is true", async () => { + service[0].plan = ""; + service[0].ups = { isShow: true }; + opts.query = undefined; + opts.ups = service[0].ups; + const services: ServiceInstanceInfo[] = [ + { serviceName: eServiceTypes.user_provided, label: "ups1" }, + { serviceName: eServiceTypes.user_provided, label: "ups2" }, + ]; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + _.set( + commands, + "updateInstanceNameAndTags", + ( + availableServices: ServiceInstanceInfo[], + serviceTypeInfo: ServiceTypeInfo[], + instanceNames: string[] + ) => { + instanceNames.push(services[0].label); + return Promise.resolve(instanceNames[0]); + } + ); + const stubWithProgress = sandbox.stub( + nsVsMock.testVscode.window, + "withProgress" + ); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[0].label)) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal( + service, + { path, ignore: true }, + undefined, + { "quote-vcap": true } + ) + ).deep.equal({ + instanceName: services[0].label, + }); + + cfLocalMock + .expects("cfBindLocalUps") + .withExactArgs("some/path", [services[0].label], [], true) + .resolves(); + expect(stubWithProgress.calledOnce).to.be.true; + expect(stubWithProgress.args[0].length).to.be.equal(2); + expect(stubWithProgress.args[0][0]).to.be.deep.equal({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + expect(await stubWithProgress.args[0][1]({} as any, {} as any)).to.be + .undefined; + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no services, services found, ups selected", async () => { + service[0].plan = ""; + service[0].ups = { isShow: true }; + opts.query = undefined; + opts.ups = service[0].ups; + const services: ServiceInstanceInfo[] = [ + { serviceName: eServiceTypes.user_provided, label: "ups1" }, + { serviceName: eServiceTypes.user_provided, label: "ups2" }, + ]; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + _.set( + commands, + "updateInstanceNameAndTags", + ( + availableServices: ServiceInstanceInfo[], + serviceTypeInfo: ServiceTypeInfo[], + instanceNames: string[] + ) => { + instanceNames.push(services[0].label); + return Promise.resolve(instanceNames[0]); + } + ); + const stubWithProgress = sandbox.stub( + nsVsMock.testVscode.window, + "withProgress" + ); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[0].label)) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal(service, { path, ignore: true }) + ).deep.equal({ + instanceName: services[0].label, + }); + + cfLocalMock + .expects("cfBindLocalUps") + .withExactArgs("some/path", [services[0].label], [], undefined) + .resolves(); + expect(stubWithProgress.calledOnce).to.be.true; + expect(stubWithProgress.args[0].length).to.be.equal(2); + expect(stubWithProgress.args[0][0]).to.be.deep.equal({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + expect(await stubWithProgress.args[0][1]({} as any, {} as any)).to.be + .undefined; + }); + + it("ok:: path selected, service is not ServiceTypeInfo type, no ups, services found, type selected", async () => { + service[0].plan = plans[0].label; + delete (service[0] as any).name; + opts.query = { + filters: [{ key: eFilters.service_plan_names, value: service[0].plan }], + per_page: CF_PAGE_SIZE, + }; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + commandsMock + .expects("getInstanceName") + .withExactArgs(services) + .resolves(services[0].label); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[0].label)) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal(service, { path, ignore: true }) + ).deep.equal({ + instanceName: services[0].label, + }); + }); + + it("ok:: path selected, service is not ServiceTypeInfo type, no ups, services found, type selection canceled", async () => { + service[0].plan = plans[0].label; + delete (service[0] as any).name; + opts.query = { + filters: [{ key: eFilters.service_plan_names, value: service[0].plan }], + per_page: CF_PAGE_SIZE, + }; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + commandsMock + .expects("getInstanceName") + .withExactArgs(services) + .resolves(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .never(); + expect(await cfViewCommands.cmdBindLocal(service, { path, ignore: true })) + .to.be.undefined; + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, services found, selected new", async () => { + service[0].plan = plans[0].label; + service[0].name = ""; + opts.query = { + filters: [{ key: eFilters.service_plan_names, value: service[0].plan }], + per_page: CF_PAGE_SIZE, + }; + const newService = "my-service"; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + _.set( + commands, + "updateInstanceNameAndTags", + ( + availableServices: ServiceInstanceInfo[], + serviceTypeInfo: ServiceTypeInfo[], + instanceNames: string[] + ) => { + instanceNames.push(newService); + return Promise.resolve(instanceNames[0]); + } + ); + cfLocalMock + .expects("cfGetInstanceMetadata") + .withExactArgs(newService) + .resolves({ + serviceName: newService, + service: service[0].plan, + plan_guid: "GUID", + }); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(newService)) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal(service, { path, ignore: true }) + ).deep.equal({ + instanceName: newService, + }); + }); + + it("ok:: path selected, service is ServiceTypeInfo type, no ups, services found, create allowed", async () => { + service[0].plan = plans[0].label; + service[0].name = ""; + service[0].allowCreate = {}; + opts.query = { + filters: [{ key: eFilters.service_plan_names, value: service[0].plan }], + per_page: CF_PAGE_SIZE, + }; + let expectedServicesList; + let servicePlans; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + _.set( + commands, + "updateInstanceNameAndTags", + ( + availableServices: ServiceInstanceInfo[], + serviceTypeInfo: ServiceTypeInfo[] + ) => { + expectedServicesList = availableServices; + servicePlans = _.get(serviceTypeInfo, "plans"); + return Promise.resolve(); + } + ); + expect(await cfViewCommands.cmdBindLocal(service, { path, ignore: true })) + .to.be.undefined; + expect( + _.has( + _.find(expectedServicesList, ["label", commands.CMD_CREATE_SERVICE]), + "serviceName" + ) + ).to.be.true; + expect(servicePlans).to.be.undefined; + }); + + it("ok:: return chisel task", async () => { + opts.query = { + filters: [{ key: eFilters.service_plan_names, value: service[0].plan }], + per_page: CF_PAGE_SIZE, + }; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[0].label)) + .resolves(); + const chiselTask = { + label: `openChiselTunnerFor-${services[0].label}`, + type: "shell", + command: "chisel", + isBackground: true, + problemMatcher: "$chisel-client", + args: ["client", "--auth"], + }; + chiselMock + .expects("checkAndCreateChiselTask") + .withExactArgs(path.fsPath, services[0].label) + .resolves(chiselTask); + chiselMock + .expects("deleteChiselParamsFromFile") + .withExactArgs(path.fsPath) + .resolves(); + expect( + await cfViewCommands.cmdBindLocal(service, { path, ignore: true }) + ).deep.equal({ + instanceName: services[0].label, + chiselTask: chiselTask, + }); + }); + }); + + describe("bindLocalService scope", () => { + let origin: unknown; + const services: ServiceInstanceInfo[] = [ + { serviceName: "hdi-shared", label: "label1" }, + { serviceName: "hdi-shared", label: "label2" }, + ]; + const plans: PlanInfo[] = [ + { label: "hdi-shared", guid: "ABCD", description: "" }, + { label: "hana", guid: "EFGH", description: "" }, + ]; + before(() => { + origin = commands.updateInstanceNameAndTags; + }); + + beforeEach(() => { + _.set( + commands, + "updateInstanceNameAndTags", + ( + availableServices: ServiceInstanceInfo[], + serviceTypeInfo: ServiceTypeInfo[], + instanceNames: string[] + ) => { + instanceNames.push(services[1].label); + return Promise.resolve(instanceNames[0]); + } + ); + }); + + afterEach(() => { + _.set(commands, "updateInstanceNameAndTags", origin); + }); + + const service: ServiceTypeInfo[] = [ + { name: "", plan: "hdi-shared", prompt: "", tag: "" }, + ]; + const opts: DisplayServices = { + query: { + filters: [{ key: eFilters.service_plan_names, value: service[0].plan }], + per_page: CF_PAGE_SIZE, + }, + }; + const envPath = nsVsMock.testVscode.Uri.file("root/folder"); + + it("ok:: path is empty", async () => { + const path = nsVsMock.testVscode.Uri.file(""); + expect( + await cfViewCommands.bindLocalService(service, { path, ignore: true }) + ).to.be.empty; + }); + + it("ok:: path defined, plan required, no service selected", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + _.set(commands, "updateInstanceNameAndTags", () => { + return Promise.resolve([]); + }); + expect( + await cfViewCommands.bindLocalService(service, { + path: envPath, + ignore: true, + }) + ).to.be.undefined; + }); + + it("ok:: path defined, plan required, service selected", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[1].label)) + .resolves(); + assert.deepEqual( + await cfViewCommands.bindLocalService(service, { + path: envPath, + ignore: true, + }), + [services[1].label] + ); + }); + + it("ok:: path defined, plan required, service selected, silent mode required", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[1].label)) + .never(); + assert.deepEqual( + await cfViewCommands.bindLocalService( + service, + { path: envPath, ignore: true }, + { silent: true } + ), + [services[1].label] + ); + }); + + it("ok:: serviceInfo is not array, service selected", async () => { + vscodeWorkspaceMock.expects("getWorkspaceFolder").returns(undefined); + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + commandsMock + .expects("getInstanceName") + .withExactArgs(services) + .resolves(services[1].label); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .resolves(); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_bound_successful(services[1].label)) + .resolves(); + delete (service[0] as any).name; + assert.deepEqual( + await cfViewCommands.bindLocalService(service, envPath), + [services[1].label] + ); + }); + + it("ok:: serviceInfo is not array, select service canceled", async () => { + const service: ServiceTypeInfo[] = [ + { name: "", plan: "hdi-shared", prompt: "", tag: "" }, + ]; + delete (service[0] as any).name; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + commandsMock + .expects("getInstanceName") + .withExactArgs(services) + .resolves(); + expect(await cfViewCommands.bindLocalService(service, envPath)).to.be + .undefined; + }); + + it("ok:: serviceInfo is array, no services found", async () => { + service[0].name = ""; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves([]); + vscodeWindowMock.expects("withProgress").never(); + expect( + await cfViewCommands.bindLocalService(service, { + path: envPath, + ignore: true, + }) + ).to.be.empty; + }); + + it("ok:: serviceInfo is array, no services found, plan is not provided", async () => { + const copyService: ServiceTypeInfo[] = [ + { name: "", plan: "", prompt: "", tag: "" }, + ]; + const opts: DisplayServices = { query: undefined }; + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves([]); + expect( + await cfViewCommands.bindLocalService(copyService, { + path: envPath, + ignore: true, + }) + ).to.be.empty; + }); + + it("show error:: serviceInfo is not array, service selected, doBind rejected", async () => { + commandsMock + .expects("getAvailableServices") + .withExactArgs(opts) + .resolves(services); + commandsMock + .expects("getInstanceName") + .withExactArgs(services) + .resolves(services[1].label); + const error = new Error("error"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.binding_service_to_file, + cancellable: false, + }) + .rejects(error); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(error.message) + .resolves(); + delete (service[0] as any).name; + await cfViewCommands.bindLocalService(service, envPath); + }); + + it("ok:: serviceInfo is array, service name required, no services found", async () => { + service[0].plan = ""; + service[0].name = " hana , xsuaa "; + commandsMock + .expects("fetchServicePlanList") + .withExactArgs( + _.cloneDeep({ + filters: [ + { key: eFilters.service_offering_names, value: "hana,xsuaa" }, + ], + }) + ) + .resolves(plans); + const query: IServiceQuery = { + filters: [ + { + key: eFilters.service_plan_guids, + value: _.join(_.map(plans, "guid")), + }, + ], + per_page: CF_PAGE_SIZE, + }; + commandsMock + .expects("getAvailableServices") + .withExactArgs({ query }) + .resolves(services); + assert.deepEqual( + await cfViewCommands.bindLocalService(service, { + path: envPath, + ignore: true, + }), + [services[1].label] + ); + }); + + it("ok:: serviceInfo is array, service name - ups only requested, no services found", async () => { + service[0].plan = ""; + service[0].name = eServiceTypes.user_provided; + const upsServices: ServiceInstanceInfo[] = [ + { serviceName: eServiceTypes.user_provided, label: "label1" }, + { serviceName: eServiceTypes.user_provided, label: "label2" }, + ]; + const query: IServiceQuery = { + filters: [ + { key: eFilters.service_plan_names, value: "nothing-to-show" }, + ], + per_page: CF_PAGE_SIZE, + }; + commandsMock + .expects("getAvailableServices") + .withExactArgs({ query }) + .resolves(upsServices); + assert.deepEqual( + await cfViewCommands.bindLocalService(service, { + path: envPath, + ignore: true, + }), + [services[1].label] + ); + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/chisel.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/chisel.spec.ts new file mode 100644 index 000000000..ab0913de1 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/chisel.spec.ts @@ -0,0 +1,170 @@ +import { expect } from "chai"; +import { join } from "path"; +import * as tmpTestDir from "temp-dir"; +import * as rimraf from "rimraf"; +import * as PropertiesReader from "properties-reader"; +import * as fs from "fs"; +import * as sinon from "sinon"; + +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode } from "./ext/mockUtil"; +mockVscode(nsVsMock.testVscode, "src/chisel.ts"); + +import * as utils from "../src/utils"; +import { + deleteChiselParamsFromFile, + checkAndCreateChiselTask, +} from "../src/chisel"; + +describe("chisel unit tests", () => { + const serviceName = "chisel-service-name"; + let envFilePath: string; + + /* important: + VCAP_SERVICES contains special charecters and also \n that may cause to an issue in the a string + when we read the text from the file the propertyReader add to the string "\" before each "\n" (e.g.: \\n) + therefore we must define the VCAP service with \\n + */ + const VCAP_SERVICE_WITH_ALL_CHARS = + '{"hana":[{"name":"test-service","instance_name":"test-service","label":"hana","tags":["hana","database","relational","mta-resource-name:hdi_hdb","endpoint:https://api.test.com","org:org","space:space"],"plan":"hdi-shared","credentials":{"certificate":"-----BEGIN CERTIFICATE-----\\nXXXXz\\nCAUw7C29=\\n-----END CERTIFICATE-----\\n","driver":"test.Driver","hdi_password":"hdi_password_value","hdi_user":"hdi_user","host":"host_value","password":"password_value","port":"XXXX","schema":"schema_key","service_key_name":"xxxx","url":"jdbc:test://test.ondemand.com:36603?encrypt=true\u0026validateCertificate=true\u0026currentschema=SCHEMA_42","user":"user_value"}}]}'; + const envWithChisel = { + TUNNEL_PARAM: "2020:127.0.0.1:2020", + CHISEL_URL: "https://chise-server.cfapps.test.hana.ondemand.com", + CHISEL_USER: "chisel-user", + CHISEL_PASSWORD: "chisel-password", + VCAP_SERVICES: VCAP_SERVICE_WITH_ALL_CHARS, + }; + + beforeEach(() => { + envFilePath = join(tmpTestDir, ".envtest"); + }); + + afterEach(() => { + rimraf.sync(envFilePath, { glob: false }); + sinon.restore(); + }); + + describe("checkAndCreateChiselTask", () => { + const expectedChiselTask = { + label: `openChiselTunnerFor-${serviceName}`, + type: "shell", + command: "chisel", + isBackground: true, + problemMatcher: "$chisel-client", + args: [ + "client", + "--auth", + "chisel-user:chisel-password", + "https://chise-server.cfapps.test.hana.ondemand.com", + "2020:127.0.0.1:2020", + ], + }; + it("ok:: verify created chisel task structure", async () => { + await utils.writeProperties(envFilePath, envWithChisel); + expect( + await checkAndCreateChiselTask(envFilePath, serviceName) + ).deep.equal(expectedChiselTask); + }); + + it("ok:: create 'undefined' when chisel params not appear in the environment file", async () => { + await utils.writeProperties(envFilePath, {}); + expect(await checkAndCreateChiselTask(envFilePath, serviceName)).to.be + .undefined; + }); + + it("ok:: create 'undefined' chisel URL is empty", async () => { + await utils.writeProperties(envFilePath, { CHISEL_URL: "" }); + expect(await checkAndCreateChiselTask(envFilePath, serviceName)).to.be + .undefined; + }); + + it("ok:: create 'undefined' when .env file does not exists", async () => { + expect(await checkAndCreateChiselTask(envFilePath, serviceName)).to.be + .undefined; + }); + + it("ok:: create chisel task and ignore from invalid format text in the .env file", async () => { + const text = + "invalid format text that ignore by the reader\n" + + "TUNNEL_PARAM= 2020:127.0.0.1:2020\n" + + "CHISEL_URL= https://chise-server.cfapps.test.hana.ondemand.com\n" + + "CHISEL_USER= chisel-user\n" + + "CHISEL_PASSWORD= chisel-password\n" + + "VCAP_SERVICES= VCAP_SERVICES_VALUE"; + + await fs.promises.writeFile(envFilePath, text); + expect( + await checkAndCreateChiselTask(envFilePath, serviceName) + ).deep.equal(expectedChiselTask); + }); + }); + + describe("deleteChiselParamsFromFile", () => { + const expectedProperties = { + VCAP_SERVICES: VCAP_SERVICE_WITH_ALL_CHARS, + }; + it("ok:: return true when delete chisel params from the .env file", async () => { + await utils.writeProperties(envFilePath, envWithChisel); + expect(await deleteChiselParamsFromFile(envFilePath)).to.be.true; + const actualProperties = PropertiesReader(envFilePath); + expect(actualProperties.getAllProperties()).deep.equal( + expectedProperties + ); + }); + + it("ok:: return true and ignored from empty properties when override the .env file", async () => { + const text = + "TUNNEL_PARAM=2020:127.0.0.1:2020\n" + + "CHISEL_URL= https://chise-server.cfapps.test.hana.ondemand.com\n" + + 'CHISEL_USER= "chisel-user"\n' + + "CHISEL_PASSWORD= chisel-password\n" + + "VCAP_SERVICES=" + + VCAP_SERVICE_WITH_ALL_CHARS + + "\n" + + "EMPTY_VALUE="; + await fs.promises.writeFile(envFilePath, text); + expect(await deleteChiselParamsFromFile(envFilePath)).to.be.true; + const actualProperties = PropertiesReader(envFilePath); + expect(actualProperties.getAllProperties()).deep.equal( + expectedProperties + ); + }); + + it("ok:: return false when chisel params does not exists in .env file", async () => { + await utils.writeProperties(envFilePath, expectedProperties); + expect(await deleteChiselParamsFromFile(envFilePath)).to.be.false; + const actualProperties = PropertiesReader(envFilePath); + expect(actualProperties.getAllProperties()).deep.equal( + expectedProperties + ); + }); + + it("ok:: return false when .env file does not exists ", async () => { + expect(await deleteChiselParamsFromFile(envFilePath)).to.be.false; + }); + + it("ok:: return false when write to .env file is failed", async () => { + const envWithChisel = { + TUNNEL_PARAM: "2020:127.0.0.1:2020", + CHISEL_URL: "https://chise-server.cfapps.test.hana.ondemand.com", + CHISEL_USER: "chisel-user", + CHISEL_PASSWORD: "chisel-password", + VCAP_SERVICES: "test", + }; + await utils.writeProperties(envFilePath, envWithChisel); + const expObj = { + VCAP_SERVICES: "test", + }; + sinon + .stub(utils, "writeProperties") + .withArgs(envFilePath, expObj) + .throwsException("mock failed to write to .env file"); + expect(await deleteChiselParamsFromFile(envFilePath)).to.be.false; + }); + + it("ok:: return false when properties does not exists in the .env file", async () => { + //await writeProperties(envFilePath, {}); + expect(await deleteChiselParamsFromFile(envFilePath)).to.be.false; + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/commands-a.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/commands-a.spec.ts new file mode 100644 index 000000000..12fc8c77f --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/commands-a.spec.ts @@ -0,0 +1,622 @@ +import { expect } from "chai"; +import { createSandbox, SinonMock, SinonSandbox } from "sinon"; +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode } from "./ext/mockUtil"; +mockVscode(nsVsMock.testVscode, "src/commands.ts"); +import * as commands from "../src/commands"; +import * as cfLocal from "@sap/cf-tools/out/src/cf-local"; +import { Cli, CliResult, ITarget } from "@sap/cf-tools"; +import { messages } from "../src/messages"; +import * as cfView from "../src/cfView"; +import { toText } from "../src/utils"; +import * as proxyquire from "proxyquire"; + +describe("commands unit tests", () => { + let sandbox: SinonSandbox; + let vscodeWindowMock: SinonMock; + let cliMock: SinonMock; + let cfLocalMock: SinonMock; + let cfViewMock: SinonMock; + let openLoginViewMock: SinonMock; + let internal: any; + + const orgs: any[] = [ + { label: "devx", guid: "1" }, + { label: "devx2", guid: "2" }, + { label: "HRTT", guid: "3" }, + { label: "SAP_CoCo_Messaging", guid: "4" }, + ]; + const spaces: any[] = [{ label: "ArchTeam" }, { label: "platform2" }]; + const openLoginViewProxy = { + openLoginView: () => Promise.reject(new Error("not implemented")), + }; + + before(() => { + internal = proxyquire("../src/commands", { + vscode: { + "@noCallThru": true, + }, + "@noCallThru": true, + "./loginTargetView/loginTargetView": openLoginViewProxy, + }).internal; + sandbox = createSandbox(); + }); + + after(() => { + sandbox.restore(); + }); + + beforeEach(() => { + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + cfViewMock = sandbox.mock(cfView.CFView); + cfLocalMock = sandbox.mock(cfLocal); + openLoginViewMock = sandbox.mock(openLoginViewProxy); + cliMock = sandbox.mock(Cli); + }); + + afterEach(() => { + cfLocalMock.verify(); + vscodeWindowMock.verify(); + cliMock.verify(); + cfViewMock.verify(); + openLoginViewMock.verify(); + sandbox.restore(); + }); + + describe("verifyLoginRetry", () => { + it("fail:: canceled", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .rejects(undefined); + expect(await commands.verifyLoginRetry()).to.be.undefined; + }); + }); + + describe("onErrorCfLogin", () => { + it("should handle login error by calling cmdLogin and retrying empty string", async () => { + const errorMessage = "login"; + openLoginViewMock + .expects("openLoginView") + .withExactArgs( + { isSplit: true, isLoginOnly: true }, + "https://example.com", + "", + "" + ) + .resolves(undefined); + let expErr: any = null; + const error = new Error(errorMessage); + + await internal + .onErrorCfLogin(error, "https://example.com") + .catch((errorMessage: Error) => (expErr = errorMessage)); + expect(expErr).to.be.equal(""); + }); + + it("should handle undefined error gracefully", async () => { + const err = new Error("test"); + let expErr: any = null; + await internal + .onErrorCfLogin(err) + .catch((errorMessage: Error) => (expErr = errorMessage)); + + // Assert that console.error was not called + expect(expErr.message).to.exist; + expect(expErr.message).to.be.equal(err.message); + }); + }); + + describe("cmdSelectAndSaveTarget", () => { + const target: ITarget = { + "api endpoint": "endPoint", + org: "test-org", + space: "test-space", + user: "user", + "api version": "3.100.0", + }; + const cliResult: CliResult = { + stdout: "some text", + stderr: "", + exitCode: 0, + }; + + it("ok:: show selection dialog - canceled", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + cfLocalMock.expects("cfGetTarget").resolves(target); + const org = target.org || ""; + const space = target.space || ""; + const affairs = [ + { + id: "save", + label: messages.set_targets_save(org, space), + detail: messages.set_targets_save_details, + }, + { + id: "pick-save", + label: messages.set_targets_pick_save, + detail: messages.set_targets_pick_save_details, + }, + ]; + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(affairs, { + placeHolder: messages.set_targets_choose_the_operation, + canPickMany: false, + matchOnDetail: true, + }) + .resolves(); + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it("fail:: login wrong", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves(); + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it("fail:: unsupported operation selected", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + cfLocalMock.expects("cfGetTarget").resolves(target); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "wrong" }); + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it("ok:: save current target operation selected", async () => { + const label = "test-Target"; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "save" }); + cfLocalMock.expects("cfGetTarget").resolves(target); + //...cmdCreateTarget + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.name_for_target, + ignoreFocusOut: true, + }) + .resolves(label); + const saveTargetParams = ["save-target", "-f", label]; + cliMock + .expects("execute") + .withExactArgs(saveTargetParams) + .resolves(cliResult); + cfViewMock.expects("get").returns({ + refresh: () => { + return; + }, + }); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.target_created(label)); + // + expect(await commands.cmdSelectAndSaveTarget()).to.be.equals(label); + }); + + it("ok:: save current target operation selected, create target command canceled", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "save" }); + cfLocalMock.expects("cfGetTarget").resolves(target); + //...cmdCreateTarget canceled + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.name_for_target, + ignoreFocusOut: true, + }) + .resolves(); + // + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it("ok:: save current target operation selected, target setup is incompleted, canceled", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "save" }); + const t: ITarget = { + "api endpoint": "endPoint", + user: "user", + "api version": "3.100.0", + }; + cfLocalMock.expects("cfGetTarget").resolves(t); + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.target_setup_not_completed(t.org, t.space), + "OK", + "Cancel" + ) + .resolves("Cancel"); + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it("ok:: save current target operation selected, target setup is incompleted (partial), canceled", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "save" }); + const t: ITarget = { + "api endpoint": "endPoint", + user: "user", + "api version": "3.100.0", + org: "test-org", + }; + cfLocalMock.expects("cfGetTarget").resolves(t); + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.target_setup_not_completed(t.org, t.space), + "OK", + "Cancel" + ) + .resolves("Cancel"); + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it("ok:: save current target operation selected, target setup is incompleted (partial), continued", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "save" }); + const t: ITarget = { + "api endpoint": "endPoint", + user: "user", + "api version": "3.100.0", + space: "test-space", + }; + cfLocalMock.expects("cfGetTarget").resolves(t); + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.target_setup_not_completed(t.org, t.space), + "OK", + "Cancel" + ) + .resolves("OK"); + //...cmdCreateTarget canceled + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.name_for_target, + ignoreFocusOut: true, + }) + .resolves(); + // + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it("ok:: save current target operation selected, create target command throws exception", async () => { + const label = "test-Target"; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "save" }); + cfLocalMock.expects("cfGetTarget").resolves(target); + //...cmdCreateTarget + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.name_for_target, + ignoreFocusOut: true, + }) + .resolves(label); + const saveTargetParams = ["save-target", "-f", label]; + const cli: CliResult = { + exitCode: 1, + stdout: "create target command error", + stderr: "", + }; + cliMock.expects("execute").withExactArgs(saveTargetParams).resolves(cli); + // + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(toText(new Error(cli.stdout))) + .resolves(); + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it.skip("ok:: new target operation selected, other endPoint typed", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "pick-save" }); + cfLocalMock.expects("cfGetTarget").resolves(target); + const selectedEndPoint = "other-end-point"; + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_cf_endpoint, + value: target["api endpoint"], + ignoreFocusOut: true, + }) + .resolves(selectedEndPoint); + cliMock + .expects("execute") + .withExactArgs(["api", selectedEndPoint], undefined, undefined) + .resolves(cliResult); + // ...cmdCFSetOrgSpace + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({ data: {} }); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Window, + title: messages.getting_orgs, + }) + .resolves(orgs); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(orgs, { + placeHolder: messages.select_org, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(orgs[1]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Window, + title: messages.getting_spaces, + }) + .resolves(spaces); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(spaces, { + placeHolder: messages.select_space, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(spaces[0]); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.success_set_org_space) + .resolves(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Window, + title: messages.set_org_space, + }) + .resolves(); + //...cmdCreateTarget canceled + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.name_for_target, + ignoreFocusOut: true, + }) + .resolves(); + // + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it.skip("ok:: new target operation selected, other endPoint typed, cmdCFSetOrgSpace fails", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "pick-save" }); + cfLocalMock.expects("cfGetTarget").resolves(target); + const selectedEndPoint = "other-end-point"; + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_cf_endpoint, + value: target["api endpoint"], + ignoreFocusOut: true, + }) + .resolves(selectedEndPoint); + cliMock + .expects("execute") + .withExactArgs(["api", selectedEndPoint], undefined, undefined) + .resolves(cliResult); + // ...cmdCFSetOrgSpace fails + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({ data: {} }); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Window, + title: messages.getting_orgs, + }) + .resolves(orgs); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(orgs, { + placeHolder: messages.select_org, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(); + // + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it.skip("ok:: new target operation selected, endPoint selection canceled", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "pick-save" }); + cfLocalMock.expects("cfGetTarget").resolves(target); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_cf_endpoint, + value: target["api endpoint"], + ignoreFocusOut: true, + }) + .resolves(); + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + + it.skip("ok:: new target operation selected, same endPoint typed", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({}); + vscodeWindowMock.expects("showQuickPick").resolves({ id: "pick-save" }); + cfLocalMock.expects("cfGetTarget").resolves(target); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_cf_endpoint, + value: target["api endpoint"], + ignoreFocusOut: true, + }) + .resolves(target["api endpoint"]); + // ...cmdCFSetOrgSpace + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({ data: {} }); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Window, + title: messages.getting_orgs, + }) + .resolves(orgs); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(orgs, { + placeHolder: messages.select_org, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(orgs[1]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Window, + title: messages.getting_spaces, + }) + .resolves(spaces); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(spaces, { + placeHolder: messages.select_space, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(spaces[0]); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.success_set_org_space) + .resolves(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Window, + title: messages.set_org_space, + }) + .resolves(); + //...cmdCreateTarget canceled + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.name_for_target, + ignoreFocusOut: true, + }) + .resolves(); + // + expect(await commands.cmdSelectAndSaveTarget()).to.be.undefined; + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/commands.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/commands.spec.ts new file mode 100644 index 000000000..e4ed23a34 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/commands.spec.ts @@ -0,0 +1,2103 @@ +import { expect } from "chai"; +import * as _ from "lodash"; +import { createSandbox, SinonMock, SinonSandbox } from "sinon"; +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode } from "./ext/mockUtil"; + +mockVscode(nsVsMock.testVscode, "src/commands.ts"); +import * as commands from "../src/commands"; +import * as cfLocal from "@sap/cf-tools/out/src/cf-local"; +import { + ServiceTypeInfo, + ServiceInstanceInfo, + Cli, + CF_PAGE_SIZE, + OK, +} from "@sap/cf-tools"; +import { messages } from "../src/messages"; +import * as cfLocalUtils from "@sap/cf-tools/out/src/utils"; +import * as loginTargetView from "../src/loginTargetView/loginTargetView"; +import * as cfView from "../src/cfView"; +import { fail } from "assert"; +import { validateParams } from "../src/utils"; +import { stringify } from "comment-json"; +import type { CancellationToken, Progress } from "vscode"; + +describe("commands unit tests", () => { + let sandbox: SinonSandbox; + let vscodeWindowMock: SinonMock; + let vscodeCommandsMock: SinonMock; + let commandsMock: SinonMock; + let cliMock: SinonMock; + let cfLocalMock: SinonMock; + let cfViewMock: SinonMock; + let cfLocalUtilsMock: SinonMock; + let cfLoginTargetMock: SinonMock; + const testCFDefaultLandscape = `https://my.api.cf.sap.hana.ondemand.com`; + let originalCFDefaultLandscape: string; + + before(() => { + sandbox = createSandbox(); + }); + + after(() => { + sandbox.restore(); + }); + + beforeEach(() => { + sandbox.restore(); + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + vscodeCommandsMock = sandbox.mock(nsVsMock.testVscode.commands); + commandsMock = sandbox.mock(commands); + cfViewMock = sandbox.mock(cfView.CFView); + cfLocalMock = sandbox.mock(cfLocal); + cliMock = sandbox.mock(Cli); + cfLocalUtilsMock = sandbox.mock(cfLocalUtils); + cfLoginTargetMock = sandbox.mock(loginTargetView); + originalCFDefaultLandscape = _.get(process, "env.CF_API_ENDPOINT"); + _.set(process, "env.CF_API_ENDPOINT", testCFDefaultLandscape); + }); + + afterEach(() => { + cfLocalMock.verify(); + vscodeWindowMock.verify(); + vscodeCommandsMock.verify(); + commandsMock.verify(); + cliMock.verify(); + cfLocalUtilsMock.verify(); + cfLoginTargetMock.verify(); + cfViewMock.verify(); + _.set(process, "env.CF_API_ENDPOINT", originalCFDefaultLandscape); + }); + + describe("cmdLogin", () => { + const target = new cfView.CFTargetTI({ + label: "target", + isCurrent: true, + isDirty: false, + }); + const parent = new cfView.CFFolder("parent", target); + const node = new cfView.CFLoginNode(parent); + + it("ok:: triggered from targets tree", async () => { + const json = { + Target: "my-endPoint", + SpaceFields: { + Name: "space", + }, + OrganizationFields: { + Name: "org", + }, + }; + cfLocalUtilsMock.expects("cfGetConfigFileJson").resolves(json); + const ap = "https://api.sap.test.ondemand.com"; + cfLoginTargetMock.expects("openLoginView").resolves(ap); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + const result = await commands.cmdLogin(node as any); + expect(result).to.exist; + expect(result).to.be.equal(ap); + }); + + it("ok:: triggered from targets tree, config file not recognized", async () => { + const json = { + Target: "my-endPoint", + SpaceFields: { + Name: "space", + }, + OrganizationFields: { + Name: "org", + }, + }; + cfLocalUtilsMock.expects("cfGetConfigFileJson").resolves(json); + cfLoginTargetMock.expects("openLoginView").resolves("OK"); + cfLocalMock.expects("cfGetTarget").resolves({ user: "bag023" }); + + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + const result = await commands.cmdLogin(node as any); + expect(result).to.exist; + expect(result).to.be.equal("OK"); + }); + + it.skip("'ok:: return an empty string, we caught the error", async () => { + cfLocalUtilsMock.expects("cfGetConfigFileJson").resolves(); + cfLoginTargetMock.expects("openLoginView").resolves("OK"); + + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + const result = await commands.cmdLogin(node as any); + expect(result).to.exist; + expect(result).to.be.equal(""); + }); + + it("'ok:: select space return ok", async () => { + cfLoginTargetMock.expects("openLoginView").resolves("OK"); + cfLoginTargetMock + .expects("openLoginView") + .withArgs({ isLoginOnly: true, isSplit: true }, undefined, undefined) + .resolves("OK"); + cfLocalMock.expects("cfGetTarget").resolves({ user: "bag023" }); + + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + const result = await commands.cmdSelectSpace(); + expect(result).to.exist; + expect(result).to.be.equal("OK"); + }); + }); + + describe("updateServicesOnCFPageSize", () => { + it("ok:: size not equal to cf page size", () => { + const availableServices: ServiceInstanceInfo[] = []; + commands.updateServicesOnCFPageSize(availableServices); + expect(availableServices).to.be.empty; + }); + + it("ok:: size equal to cf page size", () => { + const availableServices: ServiceInstanceInfo[] = new Array(CF_PAGE_SIZE); + commands.updateServicesOnCFPageSize(availableServices); + expect(availableServices).to.have.lengthOf(CF_PAGE_SIZE + 1); + }); + }); + + describe("cmdCreateService", () => { + beforeEach(() => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({ data: {} }); + }); + + it("fail:: cf connectivity canceled", async () => { + vscodeWindowMock.restore(); + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves(); + expect(await commands.cmdCreateService()).to.be.undefined; + }); + + it("fail:: instanceName is not specified", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves(); + expect(await commands.cmdCreateService()).to.be.undefined; + }); + + it("fail:: withProgress is called, servicesInfo is empty", async () => { + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(messages.no_services_instances_found); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([]); + expect(await commands.cmdCreateService()).to.be.undefined; + }); + + it("fail:: withProgress is called, servicesInfo is not empty, planInfo is empty", async () => { + const info = { + service_plans_url: "test_service_plans_url", + label: "testLabel", + ignoreFocusOut: true, + }; + vscodeWindowMock.expects("showQuickPick").resolves(info); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "serviceName" }]); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(messages.no_service_plans_found(info.label)) + .resolves(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([]); + expect(await commands.cmdCreateService()).to.be.undefined; + }); + + it("fail:: withProgress is called, servicesInfo is not empty, select service canceled", async () => { + vscodeWindowMock.expects("showQuickPick").resolves(undefined); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "serviceName" }]); + expect(await commands.cmdCreateService()).to.be.undefined; + }); + + it("fail:: withProgress is called, servicesInfo is not empty, planInfo several", async () => { + vscodeWindowMock.expects("showQuickPick").never(); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "myService" }]); + const plans = [{ label: "plan1" }, { label: "plan2" }]; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([plans[0]]); + vscodeWindowMock + .expects("showInputBox") + .withArgs({ + ignoreFocusOut: true, + prompt: messages.create_service_enter_params, + value: "{}", + valueSelection: undefined, + validateInput: validateParams("testServiceInfoLabel"), + }) + .resolves("{}"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.creating_service( + "instanceName", + "myService", + "plan1" + ), + cancellable: true, + }) + .resolves(); + const service = { name: "myService", plan: "plan1", tag: "", prompt: "" }; + expect(await commands.cmdCreateService(service)).to.be.undefined; + }); + + it("fail:: withProgress filtered is called, servicesInfo is not empty, planInfo is empty", async () => { + vscodeWindowMock.expects("showQuickPick").never(); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "myService" }]); + // const plans = [{ label: 'plan1' }, { label: 'plan2' }]; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves(); + const service = { name: "myService", plan: "wrong", tag: "", prompt: "" }; + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs( + messages.no_service_plan_info_found(service.plan, "myService") + ) + .resolves(); + expect(await commands.cmdCreateService(service)).to.be.undefined; + }); + + it("ok:: withProgress is called, servicesInfo is not empty, planInfo is not empty", async () => { + vscodeWindowMock.expects("showQuickPick").resolves({ + service_plans_url: "test_service_plans_url", + label: "testServiceInfoLabel", + ignoreFocusOut: true, + }); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs([{}], { + placeHolder: messages.select_service_plan, + ignoreFocusOut: true, + }) + .resolves({ label: "testPlanInfoLabel", guid: "plan-guid" }); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("showInputBox") + .withArgs({ + ignoreFocusOut: true, + prompt: messages.create_service_enter_params, + value: "{}", + valueSelection: undefined, + validateInput: validateParams("testServiceInfoLabel"), + }) + .resolves("{}"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([{}]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.creating_service( + "instanceName", + "testServiceInfoLabel", + "testPlanInfoLabel" + ), + cancellable: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "serviceName" }]); + expect(await commands.cmdCreateService()).to.be.equal("instanceName"); + }); + + it("fail:: withProgress is called, servicesInfo is not empty, planInfo is not empty, params canceled", async () => { + vscodeWindowMock.expects("showQuickPick").resolves({ + service_plans_url: "test_service_plans_url", + label: "testServiceInfoLabel", + ignoreFocusOut: true, + }); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs([{}], { + placeHolder: messages.select_service_plan, + ignoreFocusOut: true, + }) + .resolves({ label: "testPlanInfoLabel" }); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("showInputBox") + .withArgs({ + ignoreFocusOut: true, + prompt: messages.create_service_enter_params, + value: "{}", + valueSelection: undefined, + validateInput: validateParams("testServiceInfoLabel"), + }) + .resolves(undefined); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([{}]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "serviceName" }]); + expect(await commands.cmdCreateService()).to.be.undefined; + }); + + it("fail:: withProgress is called, servicesInfo is not empty, select plan canceled", async () => { + vscodeWindowMock.expects("showQuickPick").resolves({ + service_plans_url: "test_service_plans_url", + label: "testServiceInfoLabel", + ignoreFocusOut: true, + }); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs([{}], { + placeHolder: messages.select_service_plan, + ignoreFocusOut: true, + }) + .resolves(undefined); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([{}]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "serviceName" }]); + expect(await commands.cmdCreateService()).to.be.undefined; + }); + + it("fail:: with service metadata info", async () => { + const info: ServiceTypeInfo = { + name: "name", + plan: "", + prompt: "", + tag: "", + allowCreate: { + name: "customizedName", + namePrompt: "namePrompt", + }, + }; + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs( + messages.no_services_instances_found_for_type(info.name) + ); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + value: info.allowCreate?.name, + prompt: info.allowCreate?.namePrompt || messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([]); + await commands.cmdCreateService(info); + }); + + it("fail:: with service metadata info - quiet params", async () => { + vscodeWindowMock.expects("showQuickPick").never(); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: "myService" }]); + const plans = [{ label: "plan1" }, { label: "plan2" }]; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([plans[0]]); + vscodeWindowMock + .expects("showInputBox") + .withArgs({ + ignoreFocusOut: true, + placeHolder: messages.create_service_enter_params, + valueSelection: undefined, + validateInput: validateParams("testServiceInfoLabel"), + }) + .never(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.creating_service( + "instanceName", + "myService", + "plan1" + ), + cancellable: true, + }) + .resolves(); + const service = { + name: "myService", + plan: "plan1", + tag: "", + prompt: "", + allowCreate: { + getParams: () => Promise.resolve({ some: { data: "value" } }), + }, + }; + await commands.cmdCreateService(service); + }); + + it("fail:: withProgress is called but thrown exception", async () => { + const error = new Error("withProgress error"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceName"); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .rejects(error); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(error.message) + .resolves(); + const result = await commands.cmdCreateService(); + expect(result).to.be.undefined; + }); + + async function testAllowCreateParams( + expectedInstanceName: string, + expectedServiceName: string, + expectedPlanName: string, + info: ServiceTypeInfo, + plans: { label: string }[] + ) { + vscodeWindowMock.expects("showQuickPick").never(); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves(expectedInstanceName); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: expectedServiceName }]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves(plans); + vscodeWindowMock + .expects("showInputBox") + .withArgs({ + ignoreFocusOut: true, + placeHolder: messages.create_service_enter_params, + valueSelection: undefined, + validateInput: validateParams("testServiceInfoLabel"), + }) + .never(); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.creating_service( + expectedInstanceName, + expectedServiceName, + expectedPlanName + ), + cancellable: true, + }) + .resolves(expectedServiceName); + const serviceName = await commands.cmdCreateService(info); + expect(serviceName).to.equal(expectedServiceName); + } + + it("ok:: use the info params because serviceName, plan and tag are not defined in allowCreate", async () => { + const expectedServiceName = "nameA"; + const expectedPlanName = "planA"; + const expectedInstanceName = "instanceName"; + const info: ServiceTypeInfo = { + name: expectedServiceName, + plan: expectedPlanName, + prompt: "promptA", + tag: "tagA", + allowCreate: { + getParams: () => Promise.resolve({ some: { data: "value" } }), + }, + }; + const plans = [{ label: expectedPlanName }]; + + await testAllowCreateParams( + expectedInstanceName, + expectedServiceName, + expectedPlanName, + info, + plans + ); + }); + + it("ok:: use allowCreate serviceName, plan and tag because they are defined in allowCreate ", async () => { + const expectedServiceName = "nameB"; + const expectedPlanName = "planB"; + const expectedInstanceName = "instanceName"; + const info: ServiceTypeInfo = { + name: "nameA", + plan: "planA", + prompt: "promptA", + tag: "tagA", + allowCreate: { + serviceName: expectedServiceName, + plan: expectedPlanName, + tag: "tagB", + getParams: () => Promise.resolve({ some: { data: "value" } }), + }, + }; + const plans = [{ label: expectedPlanName }]; + + await testAllowCreateParams( + expectedInstanceName, + expectedServiceName, + expectedPlanName, + info, + plans + ); + }); + }); + + describe("onCreateService - continue", () => { + it("createService throws an error", async () => { + const error = new Error("testError"); + cfLocalMock.expects("cfCreateService").throws(error); + try { + await commands.onCreateService( + { guid: "", label: "", description: "" }, + "testInstance", + undefined, + undefined, + undefined as unknown as Progress<{ + message?: string; + increment?: number; + }>, + undefined as unknown as CancellationToken + ); + fail("test should not reach here"); + } catch (e) { + expect(_.get(e, "message")).to.be.equal(error.message); + } + }); + + it("ok:: createService suceeded and returns cf resource", async () => { + const params = { params: { data: "value" } }; + const tags = ["tag1", "other"]; + cfLocalMock + .expects("cfCreateService") + .withArgs("guid", "testInstance", params, tags) + .resolves({ + guid: "instance-guid", + name: "testInstance", + links: {}, + relationships: { + service_plan: { + data: { + guid: "guid", + }, + }, + }, + metadata: {}, + }); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_created("testInstance")) + .resolves(); + await commands.onCreateService( + { guid: "guid", label: "label", description: "" }, + "testInstance", + params, + tags, + undefined as unknown as Progress<{ + message?: string; + increment?: number; + }>, + undefined as unknown as CancellationToken + ); + }); + }); + + describe("isCFResource", () => { + it("fail:: obj is undefined", () => { + expect(commands.isCFResource(undefined)).to.be.false; + }); + + it("fail:: obj has no links property", () => { + expect( + commands.isCFResource({ + guid: "instance-guid", + name: "testInstance", + relationships: { + service_plan: { + data: { + guid: "guid", + }, + }, + }, + metadata: {}, + }) + ).to.be.false; + }); + + it("fail:: obj has no guid attribute", () => { + expect( + commands.isCFResource({ + name: "testInstance", + links: {}, + relationships: { + service_plan: { + data: { + guid: "guid", + }, + }, + }, + metadata: {}, + }) + ).to.be.false; + }); + + it("fail:: obj has no relationships attribute", () => { + expect( + commands.isCFResource({ + guid: "instance-guid", + links: {}, + }) + ).to.be.false; + }); + + it("ok:: obj is a cf resource", () => { + expect( + commands.isCFResource({ + guid: "instance-guid", + links: {}, + relationships: { + service_plan: { + data: { + guid: "guid", + }, + }, + }, + metadata: {}, + }) + ).to.be.true; + }); + }); + + describe("isServiceTypeInfoInArray", () => { + it("fail:: obj is undefined", () => { + expect(commands.isServiceTypeInfoInArray(undefined)).to.be.false; + }); + + it("fail:: obj is not an array", () => { + expect(commands.isServiceTypeInfoInArray({ metadata: {} })).to.be.false; + }); + + it("fail:: obj has no name property", () => { + expect(commands.isServiceTypeInfoInArray([{ metadata: {} }])).to.be.false; + }); + + it("ok:: obj has name property", () => { + expect(commands.isServiceTypeInfoInArray([{ name: "test" }])).to.be.true; + }); + }); + + describe("getAvailableServices", () => { + let commandId = "cf.set.orgspace"; + + it("fail:: loading services undefined", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves(); + expect(await commands.getAvailableServices()).to.be.undefined; + }); + + it("ok:: success", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{}]); + const result = await commands.getAvailableServices(); + expect(result).to.have.lengthOf(1); + }); + + it("fail:: on any error", async () => { + const errorMessage = "test error"; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .rejects(new Error(errorMessage)); + try { + await commands.getAvailableServices(); + fail("should fail"); + } catch (e) { + expect(e.message).to.be.equal(errorMessage); + } + }); + + it("ok:: logout, on any login ok", async () => { + const errorMessage = messages.cf_setting_not_set; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .rejects(new Error(errorMessage)); + vscodeCommandsMock + .expects("executeCommand") + .withExactArgs(commandId) + .resolves(OK); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{}]); + cfLocalMock.expects("cfGetTarget").resolves({ user: "bag023" }); + expect(await commands.getAvailableServices()).to.have.lengthOf(1); + }); + + it("fail:: on any login error", async () => { + const errorMessage = messages.cf_setting_not_set; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .rejects(new Error(errorMessage)); + cfLocalMock.expects("cfGetTarget").resolves({ user: "bag023" }); + vscodeCommandsMock + .expects("executeCommand") + .withExactArgs(commandId) + .resolves(undefined); // canceled by user + try { + await commands.getAvailableServices(); + fail("should fail"); + } catch (e) { + expect(e).to.be.equal(undefined); + } + }); + it.skip("ok:: on any login ok, org no space", async () => { + commandId = "cf.select.space"; + const errorMessage = messages.cf_setting_not_set; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .rejects(new Error(errorMessage)); + vscodeCommandsMock + .expects("executeCommand") + .withExactArgs(commandId) + .resolves(OK); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{}]); + cfLocalMock + .expects("cfGetTarget") + .resolves({ user: "bag023", org: "org" }); + const result = await commands.getAvailableServices(); + expect(result).to.have.lengthOf(1); + }); + + it("fail:: not logged in, cfGetTarget unknown fail", async () => { + commandId = "cf.select.space"; + const errorMessage = messages.cf_setting_not_set; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .rejects(new Error(errorMessage)); + cfLocalMock + .expects("cfGetTarget") + .resolves({ user: "bag023", org: "org", space: "space" }); + try { + await commands.getAvailableServices(); + fail("should fail"); + } catch (e) { + expect(e.message).to.be.equal(errorMessage); + } + }); + }); + + describe("getServiceInstances", () => { + it("ok:: on success", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{}]); + const result = await commands.getServiceInstances(); + expect(result).to.have.lengthOf(1); + }); + + it("fail:: on any error", async () => { + const errorMessage = "test error"; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .rejects(new Error(errorMessage)); + try { + await commands.getServiceInstances(); + fail("test should fail here"); + } catch (e) { + expect(e.message).to.be.equal(errorMessage); + } + }); + }); + + describe("getUserProvidedServiceInstances", () => { + it("ok:: on success", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_ups_services, + cancellable: true, + }) + .resolves([{}]); + const result = await commands.getUserProvidedServiceInstances(); + expect(result).to.have.lengthOf(1); + }); + + it("fail:: on any error", async () => { + const errorMessage = "test error"; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_ups_services, + cancellable: true, + }) + .rejects(new Error(errorMessage)); + try { + await commands.getUserProvidedServiceInstances(); + fail("test should fail here"); + } catch (e) { + expect(e.message).to.be.equal(errorMessage); + } + }); + }); + + describe("updateInstanceNameAndTags", () => { + it("ok:: availableServices is empty", async () => { + vscodeWindowMock.expects("showQuickPick").never(); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(messages.no_services_instances_found) + .resolves(); + const result = await commands.updateInstanceNameAndTags( + [], + undefined, + [], + [] + ); + expect(result).to.be.undefined; + }); + + it("ok:: availableServices is empty for service type", async () => { + vscodeWindowMock.expects("showQuickPick").never(); + const name = "absent_service"; + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(messages.no_services_instances_found_for_type(name)) + .resolves(); + const result = await commands.updateInstanceNameAndTags( + [], + { name, plan: "", tag: "", prompt: "" }, + [], + [] + ); + expect(result).to.be.undefined; + }); + + it("ok:: availableServices is not empty", async () => { + const plans = [ + { + guid: "plan_guid_1", + label: "uuaa", + description: "plan_description_1", + }, + ]; + const availableServices = [ + { + serviceName: "testServiceName1", + label: "testLabel1", + plan_guid: "plan_guid_1", + plan: plans[0].label, + }, + { serviceName: "testServiceName2", label: "More results..." }, + ]; + const pickItems = [ + { + description: `testServiceName1 (${plans[0].label})`, + label: "testLabel1", + }, + { description: "testServiceName2", label: "More results..." }, + ]; + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(pickItems, { + placeHolder: "testPrompt", + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(pickItems[0]); + const result = await commands.updateInstanceNameAndTags( + availableServices, + { + tag: "testTag", + name: "testServiceName1", + plan: "testPlan", + prompt: "testPrompt", + }, + [], + [] + ); + expect(result).to.be.equal("testLabel1"); + }); + + it("ok:: availableServices have more services", async () => { + const availableServices = [ + { serviceName: "testServiceName1", label: "More results..." }, + ]; + const pickItems = [ + { description: "testServiceName1", label: "More results..." }, + ]; + + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs([pickItems[0]], { + placeHolder: "testPrompt", + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(pickItems[0]); + const result = await commands.updateInstanceNameAndTags( + availableServices, + { + tag: "testTag", + name: "testName", + plan: "testPlan", + prompt: "testPrompt", + }, + [], + [] + ); + expect(result).to.be.equal("More results..."); + }); + + it("ok:: Bind to default service, when default service instance existed.", async () => { + const expectedInstanceName = "defaultService"; + const info: ServiceTypeInfo = { + name: "nameA", + plan: "planA", + prompt: "promptA", + tag: "tagA", + allowCreate: { + serviceName: "name", + plan: "planName", + tag: "tagB", + name: expectedInstanceName, + getParams: () => Promise.resolve({ some: { data: "value" } }), + }, + }; + + const infoName = info?.allowCreate?.name || ""; + const infoServiceName = info?.allowCreate?.serviceName || ""; + const infoPlan = info?.allowCreate?.plan || ""; + + const availableServices = [ + { + label: commands.CMD_BIND_TO_DEFAULT_SERVICE + infoName, + serviceName: infoServiceName, + plan: infoPlan, + }, + { + label: commands.CMD_CREATE_SERVICE, + serviceName: "", + }, + { + serviceName: infoServiceName, + label: expectedInstanceName, + plan: infoPlan, + }, + ]; + const pickItems = [ + { + description: `${infoServiceName} (${infoPlan})`, + label: commands.CMD_BIND_TO_DEFAULT_SERVICE + infoName, + }, + { + description: "", + label: commands.CMD_CREATE_SERVICE, + }, + { + description: `${infoServiceName} (${infoPlan})`, + label: expectedInstanceName, + }, + ]; + + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(pickItems, { + placeHolder: "promptA", + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(pickItems[0]); + const result = await commands.updateInstanceNameAndTags( + availableServices, + info, + [], + [] + ); + expect(result).to.be.equal(infoName); + }); + + it("ok:: Bind to default service,when default service instance doesn't existed (createServiceInstance).", async () => { + const expectedServiceName = "nameB"; + const expectedPlanName = "planB"; + const expectedInstanceName = "instanceName"; + const info: ServiceTypeInfo = { + name: "nameA", + plan: "planA", + prompt: "promptA", + tag: "tagA", + allowCreate: { + serviceName: expectedServiceName, + plan: expectedPlanName, + tag: "tagB", + name: expectedInstanceName, + getParams: () => Promise.resolve({ some: { data: "value" } }), + }, + }; + const plans = [{ label: expectedPlanName }]; + + const infoName = info?.allowCreate?.name || ""; + const availableServices = [ + { + label: commands.CMD_BIND_TO_DEFAULT_SERVICE + infoName, + serviceName: "", + }, + { label: commands.CMD_CREATE_SERVICE, serviceName: "" }, + ]; + const pickItems = [ + { + description: "", + label: commands.CMD_BIND_TO_DEFAULT_SERVICE + infoName, + }, + { description: "", label: commands.CMD_CREATE_SERVICE }, + ]; + + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(pickItems, { + placeHolder: "promptA", + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(pickItems[0]); + + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves([{ label: expectedServiceName }]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves(plans); + + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.creating_service( + expectedInstanceName, + expectedServiceName, + expectedPlanName + ), + cancellable: true, + }) + .resolves(expectedInstanceName); + + const result = await commands.updateInstanceNameAndTags( + availableServices, + info, + [], + [] + ); + expect(result).to.be.equal(infoName); + }); + + it("ok:: Bind to default service,when default service instance doesn't existed (createUpsInstance).", async () => { + const info: ServiceTypeInfo = { + name: "user-provided", + plan: "", + prompt: "", + tag: "mono", + allowCreate: { + name: "silent", + }, + }; + + const infoName = info?.allowCreate?.name || ""; + const availableServices = [ + { + label: commands.CMD_BIND_TO_DEFAULT_SERVICE + infoName, + serviceName: "", + }, + { label: commands.CMD_CREATE_SERVICE, serviceName: "" }, + ]; + const pickItems = [ + { + description: "", + label: commands.CMD_BIND_TO_DEFAULT_SERVICE + infoName, + }, + { description: "", label: commands.CMD_CREATE_SERVICE }, + ]; + + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(pickItems, { + placeHolder: "", + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(pickItems[0]); + + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_created(infoName)) + .resolves(); + cfLocalMock + .expects("cfCreateUpsInstance") + .withExactArgs({ + instanceName: infoName, + credentials: {}, + route_service_url: "", + syslog_drain_url: "", + tags: [info.tag], + }) + .resolves({ + name: infoName, + guid: "instance-guid", + type: "user-provided", + relationships: {}, + links: {}, + }); + + const result = await commands.updateInstanceNameAndTags( + availableServices, + info, + [], + [] + ); + expect(result).to.be.equal(infoName); + }); + }); + + describe("fetchServicePlanList", () => { + const plan = { + label: "test", + guid: "plan-guid", + description: "", + service_offering: { + guid: "service-guid", + name: "service-name", + description: "service-description", + }, + }; + + it("ok:: on success", async () => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([plan]); + vscodeWindowMock.expects("showWarningMessage").never(); + expect(await commands.fetchServicePlanList()).to.deep.equal([plan]); + }); + + it("ok:: not logged in, perform loggin", async () => { + const errorMessage = messages.login; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .rejects(new Error(errorMessage)); + cfLocalMock.expects("cfGetTarget").rejects(); + vscodeCommandsMock + .expects("executeCommand") + .withExactArgs("cf.login", true) + .resolves(OK); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .resolves([plan]); + expect(await commands.fetchServicePlanList()).to.deep.equal([plan]); + }); + + it("fail:: not logged in, login canceled by user", async () => { + const errorMessage = messages.login; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .rejects(new Error(errorMessage)); + cfLocalMock.expects("cfGetTarget").rejects(); + vscodeCommandsMock + .expects("executeCommand") + .withExactArgs("cf.login", true) + .resolves(undefined); + try { + await commands.fetchServicePlanList(); + fail("should fail"); + } catch (e) { + expect(e).to.be.equal(undefined); + } + }); + + it("fail:: not logged in, other error", async () => { + const errorMessage = "FAILED\nNo API endpoint set."; + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_service_plan_list, + cancellable: false, + }) + .rejects(new Error(errorMessage)); + vscodeCommandsMock.expects("executeCommand").never(); + try { + await commands.fetchServicePlanList(); + expect(true).to.be.equal(false); + } catch (e) { + expect(e.message).to.be.equal(errorMessage); + } + }); + }); + + describe("getInstanceName", () => { + const MORE_RESULTS = "More results..."; + const servicesPage2 = [{ serviceName: "serviceName2", label: "label2" }]; + const availableServices: ServiceInstanceInfo[] = [ + { serviceName: "serviceName1", label: "label1" }, + { label: MORE_RESULTS, serviceName: "more:2", alwaysShow: true }, + ]; + const items = [ + { + label: availableServices[0].label, + description: availableServices[0].serviceName, + }, + { + label: availableServices[1].label, + description: availableServices[1].serviceName, + }, + ]; + const allItems = [ + { + label: availableServices[0].label, + description: availableServices[0].serviceName, + }, + { + label: servicesPage2[0].label, + description: servicesPage2[0].serviceName, + }, + ]; + + it("ok:: success", async () => { + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(items, { + placeHolder: messages.select_service, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(items[1]); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.loading_services, + cancellable: true, + }) + .resolves(servicesPage2); + vscodeWindowMock + .expects("showQuickPick") + .withExactArgs(allItems, { + placeHolder: messages.select_service, + canPickMany: false, + matchOnDetail: true, + ignoreFocusOut: true, + }) + .resolves(items[0]); + const result = await commands.getInstanceName(availableServices); + expect(result).to.equal(items[0].label); + }); + + it("fail:: no avialable services", async () => { + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(messages.no_services_instances_found) + .resolves(); + const result = await commands.getInstanceName( + undefined as unknown as ServiceInstanceInfo[] + ); + expect(result).to.undefined; + }); + }); + + describe("cmdCreateUps", () => { + beforeEach(() => { + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves({ data: {} }); + }); + + it("fail:: cf connectivity canceled", async () => { + vscodeWindowMock.restore(); + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + vscodeWindowMock + .expects("withProgress") + .withArgs({ + location: nsVsMock.testVscode.ProgressLocation.Notification, + title: messages.verify_cf_connectivity, + cancellable: false, + }) + .resolves(); + expect(await commands.cmdCreateUps()).to.be.undefined; + }); + + it("ok:: empty info data", async () => { + const instanceUpsName = "myUps"; + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves(instanceUpsName); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves("{}"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_created(instanceUpsName)); + cfLocalMock + .expects("cfCreateUpsInstance") + .withExactArgs({ + instanceName: instanceUpsName, + credentials: {}, + route_service_url: "", + syslog_drain_url: "", + tags: [], + }) + .resolves({ + name: instanceUpsName, + guid: "instance-guid", + type: "user-provided", + relationships: {}, + links: {}, + }); + const info: ServiceTypeInfo = { + name: "", + plan: "", + prompt: "", + tag: "", + }; + expect(await commands.cmdCreateUps(info)).to.be.equal(instanceUpsName); + }); + + it("fail:: no name provided", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves(undefined); + cfLocalMock.expects("cfCreateUpsInstance").never(); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("fail:: enter credentials canceled", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("name"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves(undefined); + cfLocalMock.expects("cfCreateUpsInstance").never(); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("fail:: enter tags canceled", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("name"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves('{"user": "paswrd"}'); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves(undefined); + cfLocalMock.expects("cfCreateUpsInstance").never(); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("fail:: entering enter_sys_log_url canceled", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("name"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves('{"user": "paswrd"}'); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(undefined); + cfLocalMock.expects("cfCreateUpsInstance").never(); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("fail:: entering enter_route_service_url canceled", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("name"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves('{"user": "paswrd"}'); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }) + .resolves(undefined); + cfLocalMock.expects("cfCreateUpsInstance").never(); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("fail:: no info data - unexpected response format", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceUpsName"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves("{}"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }) + .resolves(""); + const response = { label: "name", metadata: {} }; + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(_.toString(response)) + .resolves(); + cfLocalMock.expects("cfCreateUpsInstance").resolves(response); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("exception:: no info data, show error message", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceUpsName"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves("{}"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock.expects("showInformationMessage").never(); + const error = new Error("runtime error"); + vscodeWindowMock.expects("showErrorMessage").withExactArgs(error.message); + cfLocalMock.expects("cfCreateUpsInstance").rejects(error); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("exception:: no info data, exception no error thrown", async () => { + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves("instanceUpsName"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves("{}"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock.expects("showInformationMessage").never(); + vscodeWindowMock.expects("showErrorMessage").once().resolves(); + cfLocalMock.expects("cfCreateUpsInstance").throws(); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + + it("ok:: silent creation", async () => { + const info: ServiceTypeInfo = { + name: "", + plan: "", + prompt: "", + tag: "mono", + allowCreate: { + name: "silent", + }, + }; + const infoName = info?.allowCreate?.name || ""; + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + value: infoName, + }) + .resolves(infoName); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_created(infoName)) + .resolves(); + cfLocalMock + .expects("cfCreateUpsInstance") + .withExactArgs({ + instanceName: infoName, + credentials: {}, + route_service_url: "", + syslog_drain_url: "", + tags: [info.tag], + }) + .resolves({ + name: infoName, + guid: "instance-guid", + type: "user-provided", + relationships: {}, + links: {}, + }); + expect(await commands.cmdCreateUps(info)).to.be.equal(infoName); + }); + + it("ok:: tags provided", async () => { + const info: ServiceTypeInfo = { + name: "", + plan: "", + prompt: "", + tag: "mono", + }; + const instanceUpsName = "myUps"; + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves(instanceUpsName); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves("{}"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_tags, + ignoreFocusOut: true, + value: info.tag, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.service_created(instanceUpsName)) + .resolves(); + cfLocalMock + .expects("cfCreateUpsInstance") + .withExactArgs({ + instanceName: instanceUpsName, + credentials: {}, + route_service_url: "", + syslog_drain_url: "", + tags: [], + }) + .resolves({ + name: instanceUpsName, + guid: "instance-guid", + type: "user-provided", + relationships: {}, + links: {}, + }); + expect(await commands.cmdCreateUps(info)).to.be.equal(instanceUpsName); + }); + + it("fail:: response isn't cf resource", async () => { + const instanceUpsName = "myUps"; + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_service_name, + ignoreFocusOut: true, + }) + .resolves(instanceUpsName); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_credentials, + ignoreFocusOut: true, + value: stringify({}), + validateInput: validateParams(""), + }) + .resolves("{}"); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ prompt: messages.enter_tags, ignoreFocusOut: true }) + .resolves([]); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_sys_log_url, + ignoreFocusOut: true, + }) + .resolves(""); + vscodeWindowMock + .expects("showInputBox") + .withExactArgs({ + prompt: messages.enter_route_service_url, + ignoreFocusOut: true, + }) + .resolves(""); + const response: any = null; + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(_.toString(response)) + .resolves(); + cfLocalMock + .expects("cfCreateUpsInstance") + .withExactArgs({ + instanceName: instanceUpsName, + credentials: {}, + route_service_url: "", + syslog_drain_url: "", + tags: [], + }) + .resolves(response); + expect(await commands.cmdCreateUps()).to.be.equal(undefined); + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/ext/mockUtil.ts b/projects/cloud-foundry-tools/packages/backend/tests/ext/mockUtil.ts new file mode 100644 index 000000000..e35eefdcb --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/ext/mockUtil.ts @@ -0,0 +1,40 @@ +import * as path from "path"; +import { dropRight, join, size, split } from "lodash"; +import { existsSync } from "fs"; + +const Module = require("module"); // eslint-disable-line @typescript-eslint/no-var-requires +const originalRequire = Module.prototype.require; + +export function clearModuleCache(testModulePath?: string): void { + if (testModulePath) { + const key = path.resolve(__dirname, "..", testModulePath); + if (require.cache[key]) { + delete require.cache[key]; + } + } +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function mockVscode(oVscodeMock: any, testModulePath?: string): void { + clearModuleCache(testModulePath); + + Module.prototype.require = function (request: any) { + if (request === "vscode") { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return oVscodeMock; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return originalRequire.apply(this, arguments); + }; +} + +export function recognisePackageJsonPath(cwd?: string): string | undefined { + let parts = split(cwd || __dirname, path.sep); + while (size(parts)) { + if (existsSync(path.resolve(path.join(...parts, "package.json")))) { + return join(parts, path.sep); + } + parts = dropRight(parts); + } +} diff --git a/projects/cloud-foundry-tools/packages/backend/tests/ext/mockVscode.ts b/projects/cloud-foundry-tools/packages/backend/tests/ext/mockVscode.ts new file mode 100644 index 000000000..ac8c0c5ab --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/ext/mockVscode.ts @@ -0,0 +1,288 @@ +import * as _ from "lodash"; +import * as vscode from "vscode"; +import * as path from "path"; +import { platform } from "os"; +export const isWindows = platform().includes("win"); + +const testRegisteredCommands = {}; +export function getTestRegisteredCommands() { + return testRegisteredCommands; +} + +let privateOnDidChangeTreeData: any; +export function getTestPrivateOnDidChangeTreeData() { + return privateOnDidChangeTreeData; +} +let treeProvider: any; +export function getTestTreeProvider() { + return treeProvider; +} +const treeView: any = { + reveal: () => { + return; + }, +}; +export function getTestTreeView() { + return treeView; +} + +let aListeners: any[] = []; +export function getTestListeners() { + return aListeners; +} +export function setTestListeners(data: any[]) { + aListeners = data; +} + +let configs = {}; +export function setTestConfigs(data: any) { + configs = data; +} + +export enum ViewColumn { + Active = -1, + Beside = -2, + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6, + Seven = 7, + Eight = 8, + Nine = 9, +} + +class Uri { + public static file(testpath: string): vscode.Uri { + return new Uri("file", "", testpath || (isWindows ? "\\" : "/"), "", ""); + } + public static parse(testpath: string): vscode.Uri { + return new Uri("http", "", testpath || (isWindows ? "\\" : "/"), "", ""); + } + public readonly path: string; + public readonly scheme: string; + public readonly authority: string; + public readonly query: string; + public readonly fragment: string; + public readonly fsPath: string; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private constructor( + scheme: string, + authority: string, + testpath: string, + query: string, + fragment: string + ) { + this.fsPath = testpath; + this.scheme = scheme; + this.path = ""; + this.authority = ""; + this.query = ""; + this.fragment = ""; + } + public with(change: { + scheme?: string; + authority?: string; + path?: string; + query?: string; + fragment?: string; + }): Uri { + return new Uri("", "", change.path || "", "", ""); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public toString(skipEncoding?: boolean): string { + return this.fsPath; + } + public toJSON(): any { + return {}; + } +} + +class RelativePattern { + base: string; + pattern: string; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(base: any, pattern: string) { + this.base = base; + this.pattern = pattern; + } +} + +function _path2Uri(extPath: string): { uri: Uri; name: string; index: number } { + return _.set( + { uri: Uri.file(extPath), name: extPath, index: 0 }, + ["uri", "fsPath"], + extPath + ); +} +const roots = [ + _path2Uri(path.resolve("workspace")), + _path2Uri(path.resolve("project-test")), +]; + +let selectedQuickPicks: any; +export function getTestSelectedQuickPicks() { + return selectedQuickPicks; +} +export function setTestSelectedQuickPicks(data: any) { + selectedQuickPicks = data; +} + +export const testVscode = { + Uri, + ViewColumn, + RelativePattern, + Progress: { + report: () => { + return; + }, + }, + ProgressLocation: { Window: 10, Notification: 15 }, + debug: { + startDebugging: () => Promise.resolve(), + }, + TreeItemCollapsibleState: { Collapsed: 1, Expanded: 2, None: 0 }, + commands: { + registerCommand: (id: string, cmd: any) => { + _.set(testRegisteredCommands, id, cmd); + return Promise.resolve(testRegisteredCommands); + }, + executeCommand: () => Promise.reject(), + }, + window: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + showTextDocument: (resourcePath: vscode.Uri) => { + return; + }, + showInformationMessage: () => Promise.resolve(), + showInputBox: () => Promise.resolve(), + showErrorMessage: () => Promise.reject(), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + registerTreeDataProvider: (id: string, provider: any) => { + return; + }, + showQuickPick: () => Promise.resolve(selectedQuickPicks), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + showWarningMessage: (message: string) => { + return; + }, + withProgress: ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options: vscode.ProgressOptions, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + task: ( + progress: vscode.Progress<{ message?: string; increment?: number }>, + token: vscode.CancellationToken + ) => Thenable + ) => Promise.resolve(), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + createTreeView: (id: string, options: any) => { + treeProvider = options.treeDataProvider; + privateOnDidChangeTreeData = treeProvider.privateOnDidChangeTreeData; + privateOnDidChangeTreeData.fire = () => { + return; + }; + treeProvider.findTreeItem = () => Promise.resolve(); + treeProvider.matchItems = () => Promise.resolve([]); + return treeView; + }, + showOpenDialog: () => Promise.reject(), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + createOutputChannel: (name: string) => { + return; + }, + createStatusBarItem: () => "", + createWebviewPanel: () => Promise.resolve(), + }, + workspace: { + rootPath: path.resolve("."), + workspaceFolders: roots, + getConfiguration: () => configs, + onDidChangeWorkspaceFolders: (listener: (e: any) => any) => { + aListeners.push(listener); + }, + onDidChangeConfiguration: () => { + return; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onDidChangeTextDocument: (listener: any) => { + return; + }, + getWorkspaceFolder: (folderPath: vscode.Uri) => { + const fsPath = _.get(folderPath, "fsPath"); + return _.set( + { + uri: testVscode.Uri.file(fsPath), + name: _.last(_.split(fsPath, "\\")), + index: 0, + }, + ["uri", "fsPath"], + fsPath + ); + }, + findFiles: () => Promise.reject(), + }, + TreeItem: class MockTreeItem { + public readonly label?: string; + public contextValue?: string; + constructor(label?: string) { + if (label) { + this.label = label; + } + } + }, + Position: class MockPossition { + public readonly line: number; + public readonly character: number; + constructor(line: number, character: number) { + this.line = line; + this.character = character; + } + }, + Selection: class MockSelection { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(anchor: vscode.Position, active: vscode.Position) { + return; + } + }, + Range: class MockRange { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(begin: number, end: number) { + return; + } + }, + EventEmitter: class MockEventEmitter { + public event: any = { name: "test" }; + public fire() { + return; + } + }, + extensions: { + getExtension: () => { + return; + }, + }, + StatusBarAlignment: { Left: "left" }, + ThemeIcon: class MockThemeIcon { + static readonly File: vscode.ThemeIcon; + static readonly Folder: vscode.ThemeIcon; + readonly id: string; + readonly color?: vscode.ThemeColor; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(id: string, color?: vscode.ThemeColor) { + this.id = id; + } + }, + WebviewPanel: { + webview: { + onDidReceiveMessage: () => {}, + }, + }, +}; + +export function setTestRoots(projects: string[]) { + testVscode.workspace.workspaceFolders = _.map(projects, (project) => { + return _path2Uri(path.resolve(project)); + }); +} diff --git a/projects/cloud-foundry-tools/packages/backend/tests/extension.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/extension.spec.ts new file mode 100644 index 000000000..7e7698525 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/extension.spec.ts @@ -0,0 +1,813 @@ +import * as _ from "lodash"; +import * as fs from "fs"; +import * as path from "path"; +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode, recognisePackageJsonPath } from "./ext/mockUtil"; + +const statusBarItem = { show: () => "", hide: () => "" }; + +mockVscode(nsVsMock.testVscode, "src/extension.ts"); +mockVscode(nsVsMock.testVscode, "src/commands.ts"); +mockVscode(nsVsMock.testVscode, "src/cfViewCommands.ts"); +mockVscode(nsVsMock.testVscode, "src/run-configuration.ts"); +import * as extension from "../src/extension"; +import * as commands from "../src/commands"; +import * as cfViewCommands from "../src/cfViewCommands"; +import * as cflocal from "@sap/cf-tools/out/src/cf-local"; +import * as cflocalUnits from "@sap/cf-tools/out/src/utils"; +import * as loggerWrapper from "../src/logger/logger-wrapper"; +import { cfGetConfigFilePath, ITarget, OK } from "@sap/cf-tools"; +import { expect } from "chai"; +import { createSandbox, SinonMock, SinonSandbox } from "sinon"; +import { CFTargetTI, CFView } from "../src/cfView"; +import type { ExtensionContext } from "vscode"; + +describe("extension unit test", () => { + const runConfigExtName = "sap.vscode-wing-run-config"; + let sandbox: SinonSandbox; + let windowMock: SinonMock; + let workspaceMock: SinonMock; + let viewCommandsMock: SinonMock; + let fsMock: SinonMock; + let mockCfLocal: SinonMock; + let mockCfLocalUnits: SinonMock; + let mockStatusBarItem: SinonMock; + let extensionsMock: SinonMock; + let loggerWrapperMock: SinonMock; + let isShowTarget: boolean; + const config = { + get: (key: string) => { + return key === "CloudFoundryTools.showTargetInformation" + ? isShowTarget + : undefined; + }, + }; + + before(() => { + sandbox = createSandbox(); + }); + + beforeEach(() => { + windowMock = sandbox.mock(nsVsMock.testVscode.window); + workspaceMock = sandbox.mock(nsVsMock.testVscode.workspace); + viewCommandsMock = sandbox.mock(cfViewCommands); + fsMock = sandbox.mock(fs); + mockCfLocal = sandbox.mock(cflocal); + mockCfLocalUnits = sandbox.mock(cflocalUnits); + mockStatusBarItem = sandbox.mock(statusBarItem); + extensionsMock = sandbox.mock(nsVsMock.testVscode.extensions); + loggerWrapperMock = sandbox.mock(loggerWrapper); + }); + + afterEach(() => { + windowMock.verify(); + workspaceMock.verify(); + viewCommandsMock.verify(); + fsMock.verify(); + mockCfLocal.verify(); + mockCfLocalUnits.verify(); + mockStatusBarItem.verify(); + extensionsMock.verify(); + loggerWrapperMock.verify(); + sandbox.restore(); + }); + + describe("deactivate", () => { + it("unwatchFile called", () => { + fsMock.expects("unwatchFile").withArgs(cfGetConfigFilePath()); + extension.deactivate(); + }); + + it("unwatchFile not called", () => { + mockCfLocalUnits.expects("cfGetConfigFilePath").returns(""); + fsMock.expects("unwatchFile").never(); + extension.deactivate(); + }); + }); + + describe("activate- fake createTreeView", () => { + let testContext: any; + const runConfigExtName = "sap.vscode-wing-run-config"; + + beforeEach(() => { + isShowTarget = true; + windowMock.expects("createTreeView").withArgs("cfView"); + testContext = { + subscriptions: [], + logUri: { fsPath: path.resolve(__dirname) }, + }; + loggerWrapperMock + .expects("initLogger") + .withExactArgs(testContext) + .resolves(); + mockStatusBarItem.expects("show"); + windowMock + .expects("createStatusBarItem") + .withExactArgs(nsVsMock.testVscode.StatusBarAlignment.Left, 100) + .returns(statusBarItem); + workspaceMock.expects("getConfiguration").returns(config); + }); + + it("ok:: cfConfigFilePath does not exist", async () => { + extensionsMock + .expects("getExtension") + .withExactArgs(runConfigExtName) + .returns(undefined); + mockCfLocalUnits.expects("cfGetConfigFilePath").returns(""); + fsMock.expects("watchFile").never(); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + }); + + it("ok:: cfConfigFilePath exists", async () => { + extensionsMock + .expects("getExtension") + .withExactArgs(runConfigExtName) + .returns(undefined); + mockCfLocalUnits + .expects("cfGetConfigFilePath") + .returns("testCFConfigFilePath"); + fsMock.expects("watchFile").withArgs("testCFConfigFilePath"); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + }); + + it("ok:: depended platform extension is defined but not activated", async () => { + const extRun = { + isActive: false, + activate: () => Promise.resolve(), + exports: { + registerDependency: () => "", + }, + }; + const mockExt = sandbox.mock(extRun); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .atLeast(1) + .resolves({ Name: "testName1" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .atLeast(1) + .resolves({ Name: "testName2" }); + mockCfLocalUnits.expects("cfGetConfigFilePath").returns(""); + mockExt.expects("activate").resolves(); + extensionsMock + .expects("getExtension") + .withExactArgs(runConfigExtName) + .returns(extRun); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + mockExt.verify(); + }); + + it("ok:: depended platform extension is defined but can not be not activated", async () => { + const extRun = { + isActive: false, + activate: () => Promise.resolve(), + exports: { + registerDependency: () => "", + }, + }; + const mockExt = sandbox.mock(extRun); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .atLeast(1) + .resolves({ Name: "testName1" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .atLeast(1) + .resolves({ Name: "testName2" }); + mockCfLocalUnits.expects("cfGetConfigFilePath").returns(""); + mockExt.expects("activate").rejects(new Error("my error")); + extensionsMock + .expects("getExtension") + .withExactArgs(runConfigExtName) + .returns(extRun); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + mockExt.verify(); + }); + }); + + describe("activate - depended platform extension is defined and activated", () => { + let testContext: any; + let mockCommands: SinonMock; + + beforeEach(async () => { + isShowTarget = false; + workspaceMock.expects("getConfiguration").returns(config); + mockCommands = sandbox.mock(commands); + testContext = { + subscriptions: [], + logUri: { fsPath: path.resolve(__dirname) }, + }; + loggerWrapperMock + .expects("initLogger") + .withExactArgs(testContext) + .resolves(); + windowMock + .expects("createStatusBarItem") + .withExactArgs(nsVsMock.testVscode.StatusBarAlignment.Left, 100) + .returns(statusBarItem); + mockCfLocalUnits + .expects("cfGetConfigFilePath") + .returns("testCFConfigFilePath"); + fsMock.expects("watchFile").withArgs("testCFConfigFilePath"); + mockStatusBarItem.expects("hide"); + extensionsMock + .expects("getExtension") + .withExactArgs(runConfigExtName) + .returns({ + isActive: true, + exports: { + registerDependency: () => "", + }, + }); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for rest of initialization asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + }); + + afterEach(() => { + mockCommands.verify(); + }); + + it("ok:: depended platform extension is defined and activated, verify commands", () => { + _.each( + [ + "cf.login", + "cf.login.weak", + "cf.target.set", + "cf.services.create", + "cf.services.bind.local", + "cf.services.unbind", + "cf.services.bind", + "cf.services.binding.state", + "cf.set.orgspace", + "cf.targets.create", + "cf.target.delete", + "cf.targets.reload", + "cf.select.space", + "cf.deploy-service.api", + "cf.services.get-space-services", + "cf.services.get-ups-services", + "cf.services.get-services", + "cf.ups.create", + ], + (cmd) => { + expect(_.get(nsVsMock.getTestRegisteredCommands(), cmd)).to.be.ok; + } + ); + }); + + it("ok:: cmdLogin command triggered", async () => { + const targets = [ + new CFTargetTI({ + label: "test-target", + isCurrent: true, + isDirty: false, + }), + ]; + sandbox + .stub(nsVsMock.getTestTreeProvider(), "getTargets") + .returns(targets); + mockCommands.expects("cmdLogin").resolves(OK); + viewCommandsMock + .expects("cmdDeleteTarget") + .withExactArgs(targets[0], { silent: true, "skip-reload": true }) + .resolves(); + viewCommandsMock + .expects("execSaveTarget") + .withExactArgs(targets[0], { silent: true, "skip-reload": true }) + .resolves(); + viewCommandsMock + .expects("execSetTarget") + .withExactArgs(targets[0], { silent: true }) + .resolves(); + await _.get(nsVsMock.getTestRegisteredCommands(), ["cf", "login"])(); + }); + + it("ok:: cmdLogin command triggered and failed", async () => { + const targets = [ + new CFTargetTI({ + label: "test-target", + isCurrent: true, + isDirty: false, + }), + ]; + sandbox + .stub(nsVsMock.getTestTreeProvider(), "getTargets") + .returns(targets); + mockCommands.expects("cmdLogin").resolves("failed"); + viewCommandsMock.expects("cmdDeleteTarget").never(); + await _.get(nsVsMock.getTestRegisteredCommands(), ["cf", "login"])(); + }); + + it("ok:: CFView created", () => { + expect(CFView.get()).to.be.ok; + }); + + it("ok:: cmdLogin command triggered, canceled", async () => { + mockCommands.expects("cmdLogin").resolves(); + await _.get(nsVsMock.getTestRegisteredCommands(), ["cf", "login"])(); + }); + + it("ok:: cmdLogin command triggered, no active target", async () => { + sandbox + .stub(nsVsMock.getTestTreeProvider(), "getCurrentTarget") + .returns(undefined); + mockCommands.expects("cmdLogin").resolves(OK); + await _.get(nsVsMock.getTestRegisteredCommands(), ["cf", "login"])(); + }); + + it("ok:: 'cf.target.set' command triggered", (done) => { + const targets = [ + new CFTargetTI({ + label: "test-target", + isCurrent: false, + isDirty: false, + }), + new CFTargetTI({ + label: "test-target-active", + isCurrent: true, + isDirty: false, + }), + ]; + const cfViewMock = sandbox.mock(nsVsMock.getTestTreeView()); + sandbox + .stub(nsVsMock.getTestTreeProvider(), "getTargets") + .returns(targets); + cfViewMock + .expects("reveal") + .withExactArgs(targets[1], { select: true, focus: true, expand: true }) + .resolves(); + viewCommandsMock + .expects("cmdSetCurrentTarget") + .withExactArgs(targets[0]) + .resolves(); + _.get(nsVsMock.getTestRegisteredCommands(), ["cf", "target", "set"])( + targets[0] + ); + setTimeout(() => { + cfViewMock.verify(); + done(); + }, 600); + }); + + it("ok:: 'cf.target.set' command triggered, reveal item not found", (done) => { + const targets = [ + new CFTargetTI({ + label: "test-target", + isCurrent: false, + isDirty: false, + }), + new CFTargetTI({ + label: "test-target-active", + isCurrent: false, + isDirty: false, + }), + ]; + const cfViewMock = sandbox.mock(nsVsMock.getTestTreeView()); + sandbox + .stub(nsVsMock.getTestTreeProvider(), "getTargets") + .returns(targets); + cfViewMock.expects("reveal").never(); + viewCommandsMock + .expects("cmdSetCurrentTarget") + .withExactArgs(targets[0]) + .resolves(); + _.get(nsVsMock.getTestRegisteredCommands(), ["cf", "target", "set"])( + targets[0] + ); + setTimeout(() => { + cfViewMock.verify(); + done(); + }, 600); + }); + + it("ok:: 'cf.targets.create' command triggered", (done) => { + const targets = [ + new CFTargetTI({ + label: "test-target", + isCurrent: false, + isDirty: false, + }), + new CFTargetTI({ + label: "test-target-active", + isCurrent: true, + isDirty: false, + }), + ]; + const cfViewMock = sandbox.mock(nsVsMock.getTestTreeView()); + sandbox + .stub(nsVsMock.getTestTreeProvider(), "getTargets") + .returns(targets); + cfViewMock + .expects("reveal") + .withExactArgs(targets[1], { select: true, focus: true, expand: true }) + .resolves(); + mockCommands.expects("cmdSelectAndSaveTarget").resolves(targets[1].label); + _.get(nsVsMock.getTestRegisteredCommands(), [ + "cf", + "targets", + "create", + ])(); + setTimeout(() => { + cfViewMock.verify(); + done(); + }, 600); + }); + + it("ok:: 'cf.targets.create' command triggered, creation canceled", (done) => { + const targets = [ + new CFTargetTI({ + label: "test-target", + isCurrent: false, + isDirty: false, + }), + new CFTargetTI({ + label: "test-target-active", + isCurrent: true, + isDirty: false, + }), + ]; + const cfViewMock = sandbox.mock(nsVsMock.getTestTreeView()); + sandbox + .stub(nsVsMock.getTestTreeProvider(), "getTargets") + .returns(targets); + cfViewMock.expects("reveal").never(); + mockCommands.expects("cmdSelectAndSaveTarget").resolves(); + _.get(nsVsMock.getTestRegisteredCommands(), [ + "cf", + "targets", + "create", + ])(); + setTimeout(() => { + cfViewMock.verify(); + done(); + }, 600); + }); + }); + + describe("onCFConfigFileChange", () => { + let testContext: any; + const target: ITarget = { + "api endpoint": "endpoint", + "api version": "3.2.1", + user: "test", + org: "org", + space: "space", + }; + + beforeEach(() => { + isShowTarget = false; + mockStatusBarItem.expects("hide"); + workspaceMock.expects("getConfiguration").returns(config); + windowMock.expects("createTreeView").withArgs("cfView"); + testContext = { + subscriptions: [], + logUri: { fsPath: path.resolve(__dirname) }, + }; + loggerWrapperMock + .expects("initLogger") + .withExactArgs(testContext) + .resolves(); + extensionsMock + .expects("getExtension") + .withExactArgs(runConfigExtName) + .returns(undefined); + mockCfLocalUnits + .expects("cfGetConfigFilePath") + .returns("testCFConfigFilePath"); + fsMock.expects("watchFile").withArgs("testCFConfigFilePath"); + windowMock + .expects("createStatusBarItem") + .withExactArgs(nsVsMock.testVscode.StatusBarAlignment.Left, 100) + .returns(statusBarItem); + }); + + it("ok:: verify callback trigger, target updated", async () => { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .resolves({ Name: "testName1" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .resolves({ Name: "testName2" }); + mockCfLocal.expects("cfGetTarget").resolves(target); + setTimeout(() => { + extension.onCFConfigFileChange(); + }, 100); + await new Promise((resolve) => setTimeout(resolve, 200)); + }); + + it("ok:: nothing updated", async () => { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + + setTimeout(() => { + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .resolves({ Name: "testName1" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .resolves({ Name: "testName2" }); + mockCfLocal.expects("cfGetTarget").resolves(target); + extension.onCFConfigFileChange(); + }, 100); + + setTimeout(() => { + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .resolves({ Name: "testName1" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .resolves({ Name: "testName2" }); + extension.onCFConfigFileChange(); + }, 300); + await new Promise((resolve) => setTimeout(resolve, 500)); + }); + + it("ok:: not targeted", async () => { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + + setTimeout(() => { + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .resolves({ Name: "" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .resolves({ Name: "" }); + mockCfLocal.expects("cfGetTarget").never(); + extension.onCFConfigFileChange(); + }, 100); + await new Promise((resolve) => setTimeout(resolve, 300)); + }); + + it("ok:: not logged in", async () => { + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .resolves({ Name: "testName1" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .resolves({ Name: "testName3" }); + /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ + await extension.activate(testContext).then(() => { + // wait for the tree view to be created asynchonously + return new Promise((resolve) => setTimeout(resolve, 100)); + }); + + setTimeout(() => { + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("OrganizationFields") + .resolves({ Name: "testName1" }); + mockCfLocalUnits + .expects("cfGetConfigFileField") + .withExactArgs("SpaceFields") + .resolves({ Name: "" }); + mockCfLocal.expects("cfGetTarget").rejects(undefined); + extension.onCFConfigFileChange(); + }, 100); + await new Promise((resolve) => setTimeout(resolve, 300)); + }); + }); + + describe("callbackOnDidChangeConfiguration event", () => { + it("ok:: callbackOnDidChangeConfiguration - other section", () => { + const event = { + affectsConfiguration: (section: string) => { + return section === "RunConfigurations.loggingLevel"; + }, + }; + workspaceMock.expects("getConfiguration").never(); + extension.callbackOnDidChangeConfiguration( + event, + undefined as unknown as ExtensionContext + ); + }); + + it("ok:: callbackOnDidChangeConfiguration - triggered, hide", () => { + const event = { + affectsConfiguration: (section: string) => { + return section === "CloudFoundryTools.showTargetInformation"; + }, + }; + workspaceMock.expects("getConfiguration").returns(config); + mockStatusBarItem.expects("hide"); + extension.callbackOnDidChangeConfiguration( + event, + undefined as unknown as ExtensionContext + ); + }); + + it("ok:: callbackOnDidChangeConfiguration - triggered, show", () => { + const event = { + affectsConfiguration: (section: string) => { + return section === "CloudFoundryTools.showTargetInformation"; + }, + }; + isShowTarget = true; + workspaceMock.expects("getConfiguration").returns(config); + mockStatusBarItem.expects("show"); + extension.callbackOnDidChangeConfiguration( + event, + undefined as unknown as ExtensionContext + ); + }); + }); + + describe("Common extension package definition", () => { + const jsonPath = recognisePackageJsonPath(__dirname); + const jsonPackage = JSON.parse( + fs.readFileSync( + path.resolve(path.join(jsonPath ? jsonPath : "", "package.json")), + { encoding: "utf8" } + ) + ); + + it("ok:: configuration title", () => { + expect( + _.get(jsonPackage, ["contributes", "configuration", "title"]) + ).to.be.equal("CloudFoundryTools"); + }); + + it("ok:: logging.loggingLevel desciption", () => { + expect( + _.get(jsonPackage, [ + "contributes", + "configuration", + "properties", + "CloudFoundryTools.logging.loggingLevel", + "description", + ]) + ).to.be.equal( + "The verbosity of logging. The Order is None < fatal < error < warn < info < debug < trace." + ); + }); + + it("ok:: logging.sourceLocationTracking desciption", () => { + expect( + _.get(jsonPackage, [ + "contributes", + "configuration", + "properties", + "CloudFoundryTools.logging.sourceLocationTracking", + "description", + ]) + ).to.be.equal( + "Should Source Code Location Info be added to log entries, DANGER - May be very slow, only use in debugging scenarios" + ); + }); + + it("ok:: showTargetInformation desciption", () => { + expect( + _.get(jsonPackage, [ + "contributes", + "configuration", + "properties", + "CloudFoundryTools.showTargetInformation", + "description", + ]) + ).to.be.equal( + "Display the current Cloud Foundry target information in the status bar" + ); + }); + + it("ok:: commandPalette content", () => { + const menus: any[] = _.get(jsonPackage, [ + "contributes", + "menus", + "commandPalette", + ]); + for (const menu of menus) { + expect( + [ + "cf.login.weak", + "cf.target.delete", + "cf.targets.create", + "cf.deploy-service.api", + "cf.target.set", + "cf.services.unbind", + "cf.services.bind", + "cf.services.binding.state", + "cf.services.get-space-services", + "cf.services.get-ups-services", + "cf.services.get-services", + ].includes(menu.command as string) + ).to.be.true; + expect(menu.when).to.be.equal("false"); + } + }); + + it("ok:: view/title content", () => { + const commands = _.get(jsonPackage, [ + "contributes", + "menus", + "view/title", + ]); + expect( + _.find(commands, ["command", "cf.targets.create"]) + ).to.be.deep.equal({ + command: "cf.targets.create", + when: "view == cfView", + group: "navigation@1", + }); + expect( + _.find(commands, ["command", "cf.targets.reload"]) + ).to.be.deep.equal({ + command: "cf.targets.reload", + when: "view == cfView", + group: "navigation@2", + }); + }); + + it("ok:: view/item/context content", () => { + const commands = _.get(jsonPackage, [ + "contributes", + "menus", + "view/item/context", + ]); + expect( + _.find(commands, ["command", "cf.services.create"]) + ).to.be.deep.equal({ + command: "cf.services.create", + when: "view == cfView && viewItem =~ /^services-active$/", + group: "inline", + }); + expect(_.find(commands, ["command", "cf.login"])).to.be.deep.equal({ + command: "cf.login", + when: "view == cfView && viewItem == cf-login-required", + group: "inline", + }); + expect( + _.find(commands, ["command", "cf.services.bind.local"]) + ).to.be.deep.equal({ + command: "cf.services.bind.local", + when: "view == cfView && viewItem == cf-service", + }); + let command = _.find(commands, (com) => { + return com.command === "cf.target.set" && com.group === "inline"; + }); + expect(command).to.be.deep.equal({ + command: "cf.target.set", + when: "view == cfView && viewItem =~ /^cf-target$|^cf-target-not-current$/", + group: "inline", + }); + command = _.find(commands, (com) => { + return com.command === "cf.target.set" && com.group === "appearance@1"; + }); + expect(command).to.be.deep.equal({ + command: "cf.target.set", + when: "view == cfView && viewItem =~ /^cf-target$/", + group: "appearance@1", + }); + expect( + _.find(commands, ["command", "cf.target.delete"]) + ).to.be.deep.equal({ + command: "cf.target.delete", + when: "view == cfView && viewItem =~ /^cf-target(-active)?$/", + group: "appearance@2", + }); + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/loginTargetView/loginTargetView.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/loginTargetView/loginTargetView.spec.ts new file mode 100644 index 000000000..17766b0a9 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/loginTargetView/loginTargetView.spec.ts @@ -0,0 +1,556 @@ +import { expect } from "chai"; +import { fail } from "assert"; +import * as proxyquire from "proxyquire"; +import { createSandbox, mock, SinonMock, SinonSandbox } from "sinon"; +import { cfendpoint } from "@sap/bas-sdk"; +import { OK, Organization, Space, SSOLoginOptions } from "@sap/cf-tools"; +import { RpcExtension } from "@sap-devx/webview-rpc/out.ext/rpc-extension"; +import * as nsVsMock from "./../ext/mockVscode"; +import { mockVscode } from "../ext/mockUtil"; +import * as cfLocal from "@sap/cf-tools/out/src/cf-local"; +import { messages } from "../../src/messages"; +import * as path from "path"; +import * as fs from "fs"; + +nsVsMock.setTestRoots(["/", "/some_path"]); + +mockVscode(nsVsMock.testVscode, "src/cfViewCommands.ts"); +mockVscode(nsVsMock.testVscode, "src/logger/logger-wrapper.ts"); +mockVscode(nsVsMock.testVscode, "src/commands.ts"); + +describe("loginTargetView tests", () => { + let sandbox: SinonSandbox; + let cfApiMock: SinonMock; + let cfFsMock: SinonMock; + let cfendpointMock: SinonMock; + let vscodeWindowMock: SinonMock; + let cfLocalMock: SinonMock; + let vscodeCommandsMock: SinonMock; + + const cfApiProxy = { + cfGetConfigFileField: () => Promise.reject(new Error("not implemented")), + cfGetAvailableOrgs: () => Promise.reject(new Error("not implemented")), + cfGetAvailableSpaces: () => Promise.reject(new Error("not implemented")), + cfSetOrgSpace: () => Promise.reject(new Error("not implemented")), + }; + let internal: any; + let loginTargetInstance: typeof internal.LoginTarget; + + const panelObj = { + viewType: "myViewType", + title: "My Webview Panel", + webview: { + onDidReceiveMessage: () => {}, + asWebviewUri: (uri: any) => uri, + onDidDispose: (callback: () => void) => { + // Simulate panel disposal + callback(); + }, + html: "", + }, + options: {}, // Provide appropriate mock data here + viewColumn: nsVsMock.testVscode.ViewColumn.One, + active: true, + visible: true, + onDidDispose: () => {}, + onDidChangeViewState: () => {}, // Provide appropriate mock data here + reveal: () => {}, + dispose: () => {}, + }; + + let internalModule: any; + const extensionPath = "api.sap.test.ondemand.com"; + + before(() => { + sandbox = createSandbox(); + internalModule = proxyquire("../../src/loginTargetView/loginTargetView", { + vscode: { + ...nsVsMock.testVscode, + "@noCallThru": true, + }, + "@sap/cf-tools": cfApiProxy, + "../extension": { + getPath: () => extensionPath, + "@noCallThru": true, + }, + "@noCallThru": true, + }); + internal = internalModule.internal; + }); + + beforeEach(() => { + cfApiMock = mock(cfApiProxy); + cfFsMock = mock(fs); + cfendpointMock = mock(cfendpoint); + cfLocalMock = sandbox.mock(cfLocal); + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + vscodeCommandsMock = sandbox.mock(nsVsMock.testVscode.commands); + }); + + afterEach(() => { + cfApiMock.verify(); + cfFsMock.verify(); + cfendpointMock.verify(); + cfLocalMock.verify(); + vscodeWindowMock.verify(); + vscodeCommandsMock.verify(); + sandbox.restore(); + }); + + const htmlContext = + ""; + + describe("openLoginView", () => { + it("should open login view and handle login", async () => { + const panelMock = sandbox.mock(panelObj); + panelMock.expects("reveal").returns(undefined); + sandbox.stub(panelObj, "onDidDispose").yields(); + vscodeWindowMock + .expects("createWebviewPanel") + .withExactArgs( + "cfLogin", + "Cloud Foundry Sign In", + nsVsMock.testVscode.ViewColumn.Beside, + { + enableScripts: true, + localResourceRoots: [ + nsVsMock.testVscode.Uri.file( + path.join(extensionPath, "dist", "media") + ), + ], + } + ) + .returns(panelObj); + cfFsMock + .expects("readFileSync") + .withExactArgs( + path.join(extensionPath, "dist", "media", "index.html"), + "utf8" + ) + .returns(htmlContext); + + // Stub the rpcInvokeStub for invoking long function with progress form + sandbox.stub(RpcExtension.prototype, "invoke").resolves(OK); + + expect(await internalModule.openLoginView({ isSplit: true })).to.equal( + undefined + ); + }); + }); + + describe("getOrgs", () => { + it("should fetch orgs successfully", async () => { + const orgs: Organization[] = [{ label: "org", guid: "guid" }]; + loginTargetInstance = new internal.LoginTarget(panelObj); + + cfApiMock.expects("cfGetAvailableOrgs").resolves(orgs); + + const result = await loginTargetInstance.getOrgs(); + + expect(result).to.deep.equal(orgs); + }); + + it("should handle error when fetching orgs", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + + cfApiMock + .expects("cfGetAvailableOrgs") + .rejects(new Error("Failed to fetch orgs")); + + const result = await loginTargetInstance.getOrgs(); + + expect(result).to.deep.equal([]); + }); + }); + + describe("getTarget", () => { + it("should fetch target successfully", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + const api = { + user: "user", + space: "space", + }; + + const cfLogoutStub = sandbox.stub().resolves(api); + mockPrivateMethod( + loginTargetInstance, + cfLogoutStub, + "invokeLongFunctionWithProgressForm" + ); + + const result = await loginTargetInstance.getTarget(); + + expect(result.user).to.equal(api.user); + expect(result.space).to.equal(api.space); + }); + + it("should handle error when fetching target", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + const cfLogoutStub = sandbox.stub().rejects(new Error("Logout error")); + mockPrivateMethod( + loginTargetInstance, + cfLogoutStub, + "invokeLongFunctionWithProgressForm" + ); + + const result = await loginTargetInstance.getTarget(); + + expect(result).to.equal(undefined); + }); + }); + + describe("init", () => { + it("should initialize with target when logged in", async () => { + const loginTargetInstance = new internal.LoginTarget(panelObj); + + const cfGetTargetStub = sandbox + .stub(loginTargetInstance, "getTarget") + .resolves({ + "api endpoint": extensionPath, + org: "my-org", + space: "my-space", + }); + mockPrivateMethod(loginTargetInstance, cfGetTargetStub, "getTarget"); + + const cfDefaultLandscape = sandbox.stub().resolves(extensionPath); + mockPrivateMethod( + loginTargetInstance, + cfDefaultLandscape, + "getCFDefaultLandscape" + ); + + const result = await loginTargetInstance.init(); + + expect(result.defaultEndpoint).to.equal(extensionPath); + expect(result.isLoggedIn).to.be.true; + expect(result.passcodeUrl).to.equal( + "login.sap.test.ondemand.com/passcode" + ); + expect(result.currentOrg).to.equal("my-org"); + expect(result.currentSpace).to.equal("my-space"); + }); + }); + + describe("getSpaces", () => { + it("should fetch spaces successfully", async () => { + const org = "org-1"; + const spaces: Space[] = [ + { label: "space", guid: "guid", orgGUID: "orgGUID" }, + ]; + loginTargetInstance = new internal.LoginTarget(panelObj); + + cfApiMock + .expects("cfGetAvailableSpaces") + .withExactArgs(org) + .resolves(spaces); + + const result = await loginTargetInstance.getSpaces(org); + + expect(result).to.deep.equal(spaces); + }); + + it("should handle error when fetching spaces", async () => { + const org = "org-1"; + loginTargetInstance = new internal.LoginTarget(panelObj); + + cfApiMock + .expects("cfGetAvailableSpaces") + .withExactArgs(org) + .rejects(new Error("Failed to fetch spaces")); + + const result = await loginTargetInstance.getSpaces(org); + + expect(result).to.deep.equal([]); + }); + }); + + describe("getSelectedTarget", () => { + it("should return selected target", () => { + const expectedTarget = { + "api endpoint": extensionPath, + org: "my-org", + space: "my-space", + }; + + const loginTargetInstance = new internal.LoginTarget(panelObj); + loginTargetInstance["currentTarget"] = expectedTarget; + + const result = loginTargetInstance.getSelectedTarget(); + + expect(result.org).to.deep.equal(expectedTarget.org); + expect(result.space).to.deep.equal(expectedTarget.space); + expect(result.endpoint).to.deep.equal(expectedTarget["api endpoint"]); + }); + + it("should return undefined when no target is selected", () => { + const loginTargetInstance = new internal.LoginTarget(panelObj); + loginTargetInstance["currentTarget"] = undefined; + + const result = loginTargetInstance.getSelectedTarget(); + + expect(result.org).to.be.undefined; + expect(result.space).to.be.undefined; + expect(result.endpoint).to.be.undefined; + }); + }); + + describe("logoutClick", () => { + it("logoutClick should fail and return false", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + + const cfLogoutStub = sandbox.stub().rejects(new Error("Logout error")); + mockPrivateMethod( + loginTargetInstance, + cfLogoutStub, + "invokeLongFunctionWithProgressForm" + ); + + const result = await loginTargetInstance.logoutClick(); + + expect(result).to.be.false; + // Additional assertions could be made here if desired + }); + + it("should handle successful logout", async () => { + const loginTargetInstance = new internal.LoginTarget(panelObj); + + // Mock the cfLogout function to resolve successfully + const cfLogoutStub = sandbox.stub().resolves(); + loginTargetInstance.invokeLongFunctionWithProgressForm = cfLogoutStub; + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.logout_success) + .resolves(); + + const result = await loginTargetInstance.logoutClick(); + + // Check if cfLogout was called + expect(cfLogoutStub.calledOnce).to.be.true; + expect(result).to.be.true; + }); + }); + + describe("loginClick", () => { + it("should handle successful login", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + + // Mock the long function (cfLogin) to return a success result + const cfLoginStub = sandbox.stub().resolves(OK); + mockPrivateMethod( + loginTargetInstance, + cfLoginStub, + "invokeLongFunctionWithProgressForm" + ); + const showMessageStub = vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.login_success) + .resolves(); + + const result = await loginTargetInstance.loginClick( + {} as SSOLoginOptions + ); + + expect(result).to.be.true; + expect(showMessageStub.calledOnceWith(messages.login_success)).to.be.true; + expect(cfLoginStub.calledOnce).to.be.true; + }); + + it("should handle failed login and show error message", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + + const loginResult = "Failed"; + const showMessageStub = vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(messages.login_failed) + .resolves(); + + // Mock the long function (cfLogin) to return a failure result + const cfLoginStub = sandbox.stub().resolves(loginResult); + mockPrivateMethod( + loginTargetInstance, + cfLoginStub, + "invokeLongFunctionWithProgressForm" + ); + + const result = await loginTargetInstance.loginClick( + {} as SSOLoginOptions + ); + + expect(result).to.be.false; + expect(showMessageStub.calledOnceWith(messages.login_failed)).to.be.true; + expect(cfLoginStub.calledOnce).to.be.true; + }); + + it("should handle login exception and show error message", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + + // Mock the long function (cfLogin) to throw an error + const cfLoginStub = sandbox.stub().rejects(new Error("Login error")); + mockPrivateMethod( + loginTargetInstance, + cfLoginStub, + "invokeLongFunctionWithProgressForm" + ); + + const result = await loginTargetInstance.loginClick( + {} as SSOLoginOptions + ); + + expect(result).to.be.false; + expect(cfLoginStub.calledOnce).to.be.true; + }); + }); + + describe("applyTarget", () => { + it("ok:: applyTarget - successful set org and space", async () => { + const org = "my-org"; + const space = "my-space"; + loginTargetInstance = new internal.LoginTarget(panelObj); + + // Mock the cfSetOrgSpace function to resolve successfully + const cfSetOrgSpaceStub = sandbox.stub().resolves(OK); + cfApiProxy.cfSetOrgSpace = cfSetOrgSpaceStub; + + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs(messages.success_set_org_space) + .resolves(); + + const result = await loginTargetInstance.applyTarget(org, space); + + // Check if cfSetOrgSpace was called with the correct arguments + expect(cfSetOrgSpaceStub.calledOnceWith(org, space)).to.be.true; + expect(result).to.equal(OK); + }); + + it("should handle applyTarget exception and show error message", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + + // Mock the cfSetOrgSpace function to resolve successfully + cfApiProxy.cfSetOrgSpace = sandbox.stub().throws("err"); + + const result = await loginTargetInstance.applyTarget(null, null); + + expect(result).to.equal("Error"); + }); + }); + + describe("openPasscodeLink", () => { + it("should open passcode link in vscode", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + + vscodeCommandsMock.expects("executeCommand").resolves(); + + await loginTargetInstance.openPasscodeLink(extensionPath); + }); + }); + + describe("invokeLongFunctionWithProgressForm", () => { + it("should invoke long function with progress form", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + const longFunctionStub = sandbox.stub().resolves("result"); + const rpcInvokeStub = sandbox.stub(RpcExtension.prototype, "invoke"); + + const result = + await loginTargetInstance.invokeLongFunctionWithProgressForm( + longFunctionStub, + "arg1", + "arg2" + ); + + expect(result).to.equal("result"); + expect(rpcInvokeStub.calledTwice).to.be.true; + expect(rpcInvokeStub.firstCall.calledWith("setBusyIndicator", [true])).to + .be.true; + expect(rpcInvokeStub.secondCall.calledWith("setBusyIndicator", [false])) + .to.be.true; + }); + + it("should handle error when invoking long function", async () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + const longFunctionStub = sandbox + .stub() + .rejects(new Error("Function failed")); + const rpcInvokeStub = sandbox.stub(RpcExtension.prototype, "invoke"); + + try { + await loginTargetInstance.invokeLongFunctionWithProgressForm( + longFunctionStub, + "arg1", + "arg2" + ); + fail("Should have thrown an error"); + } catch (error) { + expect(error.message).to.equal("Function failed"); + expect(rpcInvokeStub.calledTwice).to.be.true; + expect(rpcInvokeStub.firstCall.calledWith("setBusyIndicator", [true])) + .to.be.true; + expect(rpcInvokeStub.secondCall.calledWith("setBusyIndicator", [false])) + .to.be.true; + } + }); + }); + + describe("getCFDefaultLandscape", () => { + const envEndpoint = "https://my.env.endpoint"; + const fileEndpoint = "https://my.test.endpoint"; + + it("ok:: getCFDefaultLandscape - 'Target' is defined in .cf config file - return it first", async () => { + const loginTargetInstance = new internal.LoginTarget(panelObj); + cfApiMock + .expects("cfGetConfigFileField") + .withExactArgs("Target") + .resolves(fileEndpoint); + expect(await loginTargetInstance.getCFDefaultLandscape()).to.be.equal( + fileEndpoint + ); + }); + + it("ok:: getCFDefaultLandscape - 'Target' is undefined in .cf config file - fetch from env", async () => { + const loginTargetInstance = new internal.LoginTarget(panelObj); + cfApiMock + .expects("cfGetConfigFileField") + .withExactArgs("Target") + .resolves(); + cfendpointMock.expects("getCFEndpoint").resolves(envEndpoint); + expect(await loginTargetInstance.getCFDefaultLandscape()).to.be.equal( + envEndpoint + ); + }); + + it("fail:: getCFDefaultLandscape - cfGetConfigFileField is rejected", () => { + const loginTargetInstance = new internal.LoginTarget(panelObj); + const error = new Error("cfGetConfigFileField rejected"); + cfApiMock + .expects("cfGetConfigFileField") + .withExactArgs("Target") + .rejects(error); + return loginTargetInstance + .getCFDefaultLandscape() + .then(() => { + fail("should fail"); + }) + .catch((e: Error) => { + expect(e.message).to.be.equal(error.message); + }); + }); + }); + + describe("calculatePasscodeUrl", () => { + it("ok:: calculatePasscodeUrl", () => { + loginTargetInstance = new internal.LoginTarget(panelObj); + expect( + loginTargetInstance.calculatePasscodeUrl(extensionPath) + ).to.be.equal("login.sap.test.ondemand.com/passcode"); + }); + }); + + function mockPrivateMethod( + loginTargetInstance: typeof internal.LoginTarget, + cfLoginStub: any, + nameofPrivateMethod: string + ) { + // Call the private method through the instance + return (loginTargetInstance[nameofPrivateMethod] = cfLoginStub); + } +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/messages.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/messages.spec.ts new file mode 100644 index 000000000..f60669ba2 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/messages.spec.ts @@ -0,0 +1,29 @@ +import { expect } from "chai"; +import { messages } from "../src/messages"; + +describe("messages unit test", () => { + it("messages", () => { + expect(messages.service_created("test")).to.be.not.undefined; + expect(messages.activation_extension("test")).to.be.not.undefined; + expect(messages.extension_active("test")).to.be.not.undefined; + expect(messages.targeting("testOrg", "testSpace")).to.be.not.undefined; + expect(messages.targeting("testOrg", "testSpace")).to.be.not.undefined; + expect(messages.no_service_plan_info_found("testPlan", "testService")).to.be + .not.undefined; + expect(messages.no_services_instance_byname_found("testService")).to.be.not + .undefined; + expect(messages.no_services_found("my-plan {1}")).to.be.ok; + expect(messages.service_bound_successful("planName")).to.be.ok; + expect( + messages.service_instances_list_incomplete(["s-1", "s-2"]) + ).to.be.equal( + "Could not get the details of the following service instances: 's-1,s-2'. Try to verify the specified service instances, then log in again to Cloud Foundry." + ); + expect( + messages.service_instances_list_incomplete(["s-1", "s-2", "s-3", "s-4"]) + ).to.be.equal( + "Could not get the details of some service instances. Try to verify the service instances, then log in again to Cloud Foundry." + ); + expect(messages.target_created("my-target")).to.be.ok; + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/resources/.envNoVCAP b/projects/cloud-foundry-tools/packages/backend/tests/resources/.envNoVCAP new file mode 100644 index 000000000..9be943f4b --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/resources/.envNoVCAP @@ -0,0 +1 @@ +CHISEL_USER=chisel-devx \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/backend/tests/resources/.envtest b/projects/cloud-foundry-tools/packages/backend/tests/resources/.envtest new file mode 100644 index 000000000..1e3d1795a --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/resources/.envtest @@ -0,0 +1,6 @@ +TUNNEL_PARAM=31111:10.255.008.222:35555 +CHISEL_URL=https://chisel6bb53d2a.test.hana.ondemand.com +CHISEL_USER=chisel-xxx +CHISEL_PASSWORD=6test5 +VCAP_SERVICES={"naha":[{"name":"hdi-test-instance","instance_name":"hdi-test-instance","label":"hana","tags":["database","relational","mta-resource-name:bookshop-hdi-container"],"plan":"hdi-shared"}],"hanatest":[{"name":"hana-1","instance_name":"hana-1","label":"hanatest","tags":["mta-resource-name:hana1-mta"]},{"name":"hana-2","instance_name":"hana-2","label":"hanatest","tags":["mta-resource-name:hana2-mta"]}],"xsuaa":[{"name":"xsuaa-myser","instance_name":"xsuaa-myser","label":"xsuaa","tags":["xsuaa"],"plan":"application"}]} +JBP_CONFIG_RESOURCE_CONFIGURATION=[tomcat/webapps/ROOT/META-INF/context.xml: {"service_test" : "hdi-test-instance"}] \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/backend/tests/resources/.envtest_readonly b/projects/cloud-foundry-tools/packages/backend/tests/resources/.envtest_readonly new file mode 100644 index 000000000..a934934ab --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/resources/.envtest_readonly @@ -0,0 +1,5 @@ +TUNNEL_PARAM=30041:10.253.111.222:30041 +CHISEL_URL=https://chisel6bb53d2a-11111-a998.example.hana.ondemand.com +CHISEL_USER=chisel-user +VCAP_SERVICES={"hana":[{"name":"hdi-test-instance","instance_name":"hdi-test-instance","label":"hana","tags":["hana","database","relational","mta-resource-name:bookshop-hdi-container"],"plan":"hdi-shared"}]} +JBP_CONFIG_RESOURCE_CONFIGURATION=[tomcat/webapps/ROOT/META-INF/context.xml: {"service_name_for_DefaultDB" : "hdi-test-instance"}] \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/backend/tests/resources/.testInValidVCAP b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testInValidVCAP new file mode 100644 index 000000000..639da7f67 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testInValidVCAP @@ -0,0 +1,5 @@ +TUNNEL_PARAM=30041:10.253.222.111:30041 +CHISEL_URL=https://chisel6bb53d2a-6429e0cba998.test.ondemand.com +CHISEL_USER=chisel-user +VCAP_SERVICES={"test":"} +JBP_CONFIG_RESOURCE_CONFIGURATION=[tomcat/webapps/ROOT/META-INF/context.xml: {"service_name_for_DefaultDB" : "hdi-test-instance"}] diff --git a/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnv b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnv new file mode 100644 index 000000000..035561cbf --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnv @@ -0,0 +1,5 @@ +TUNNEL_PARAM=30041:10.253.108.214:30041 +CHISEL_URL=https://chisel6bb53d2a-b9f6-44f8-b70a-6429e0cba998.cfapps.sap.hana.ondemand.com +CHISEL_USER=chisel-user +VCAP_SERVICES={"hana":[{"name":"hdi-test-instance","instance_name":"hdi-test-instance","label":"hana","tags":["hana","database","relational","mta-resource-name:bookshop-hdi-container"],"plan":"hdi-shared"}]} +JBP_CONFIG_RESOURCE_CONFIGURATION=[tomcat/webapps/ROOT/META-INF/context.xml: {"service_name_for_DefaultDB" : "hdi-test-instance"}] diff --git a/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnvVCAPWithQuotes b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnvVCAPWithQuotes new file mode 100644 index 000000000..329794c23 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnvVCAPWithQuotes @@ -0,0 +1,5 @@ +TUNNEL_PARAM=30041:10.253.108.214:30041 +CHISEL_URL=https://chisel6bb53d2a-b9f6-44f8-b70a-6429e0cba998.cfapps.sap.hana.ondemand.com +CHISEL_USER=chisel-user +VCAP_SERVICES='{"hana":[{"name":"hdi-test-instance","instance_name":"hdi-test-instance","label":"hana","tags":["hana","database","relational","mta-resource-name:bookshop-hdi-container"],"plan":"hdi-shared"}]}' +JBP_CONFIG_RESOURCE_CONFIGURATION=[tomcat/webapps/ROOT/META-INF/context.xml: {"service_name_for_DefaultDB" : "hdi-test-instance"}] diff --git a/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnvVCAPWithQuotesAndSpaces b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnvVCAPWithQuotesAndSpaces new file mode 100644 index 000000000..b08f8ca17 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/resources/.testValidEnvVCAPWithQuotesAndSpaces @@ -0,0 +1,5 @@ +TUNNEL_PARAM=30041:10.253.108.214:30041 +CHISEL_URL=https://chisel6bb53d2a-b9f6-44f8-b70a-6429e0cba998.cfapps.sap.hana.ondemand.com +CHISEL_USER=chisel-user +VCAP_SERVICES= '{"hana":[{"name":"hdi-test-instance","instance_name":"hdi-test-instance","label":"hana","tags":["hana","database","relational","mta-resource-name:bookshop-hdi-container"],"plan":"hdi-shared"}]}' +JBP_CONFIG_RESOURCE_CONFIGURATION=[tomcat/webapps/ROOT/META-INF/context.xml: {"service_name_for_DefaultDB" : "hdi-test-instance"}] diff --git a/projects/cloud-foundry-tools/packages/backend/tests/run-configuration.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/run-configuration.spec.ts new file mode 100644 index 000000000..e188c1515 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/run-configuration.spec.ts @@ -0,0 +1,436 @@ +import { expect, assert } from "chai"; +import * as _ from "lodash"; +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode } from "./ext/mockUtil"; + +mockVscode(nsVsMock.testVscode, "src/run-configuration.ts"); +import * as cfViewCommands from "../src/cfViewCommands"; +import * as cfLocal from "@sap/cf-tools/out/src/cf-local"; +import * as chisel from "../src/chisel"; +import { DependencyHandler } from "../src/run-configuration"; +import * as utils from "../src/utils"; +import { + IBindContext, + ConfigurationTarget, + ConfigMetadata, + BindState, +} from "@sap/wing-run-config-types"; +import { messages } from "../src/messages"; +import { createSandbox, SinonMock, SinonSandbox } from "sinon"; + +describe("run-configuration tests package", () => { + let sandbox: SinonSandbox; + let vscodeWindowMock: SinonMock; + + before(() => { + sandbox = createSandbox(); + }); + + beforeEach(() => { + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + }); + + afterEach(() => { + vscodeWindowMock.verify(); + sandbox.restore(); + }); + + describe("DependencyHandler scope", () => { + const instances = ["testInstance"]; + const bindContext: IBindContext = { + runnableId: "", + configData: { + config: { + data: ConfigMetadata.get(ConfigurationTarget.launch), + }, + }, + envPath: nsVsMock.testVscode.Uri.file("envPath"), + depContext: { + type: "hdi_type", + displayName: "", + bindable: true, + displayType: "dysplayType", + dependencyHandlerId: "dependencyHandlerId", + data: { + resourceTag: "res-tag", + resourceName: "res-name", + }, + }, + }; + + it("ok:: constructor", () => { + const id = "test.Handler.id"; + expect(new DependencyHandler(id).getId()).to.be.equal(id); + }); + + it("ok:: 'getBindState' - cloud type", async () => { + const getEnvResourcesResult = { + vcapObject: { + hdi_type: BindState.cloud, + }, + isQuotedVcap: false, + }; + sandbox + .stub(utils, "getEnvResources") + .withArgs(bindContext.envPath.fsPath) + .resolves(getEnvResourcesResult); + expect( + await new DependencyHandler("test.Handler.id").getBindState(bindContext) + ).to.be.equal(BindState.cloud); + }); + + it("ok:: 'getBindState' - not defined", async () => { + const getEnvResourcesResult = { + vcapObject: { + type: BindState.cloud, + }, + isQuotedVcap: false, + }; + sandbox + .stub(utils, "getEnvResources") + .withArgs(bindContext.envPath.fsPath) + .resolves(getEnvResourcesResult); + expect( + await new DependencyHandler("test.Handler.id").getBindState(bindContext) + ).to.be.equal(BindState.notbound); + }); + + it("ok:: 'getBindState' - exception error, resolved to not bound", async () => { + const error = new Error("some error"); + sandbox + .stub(utils, "getEnvResources") + .withArgs(bindContext.envPath.fsPath) + .throws(error); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(error.message) + .resolves(); + expect( + await new DependencyHandler("test.Handler.id").getBindState(bindContext) + ).to.be.equal(BindState.notbound); + }); + + it("ok:: 'bind' - no instance", async () => { + sandbox.stub(cfViewCommands, "bindLocalService").resolves([]); + expect( + await new DependencyHandler("test.Handler.id").bind(bindContext) + ).to.be.equal(undefined); + }); + + it("ok:: 'bind' - exception", async () => { + const error = new Error("my error"); + sandbox.stub(cfViewCommands, "bindLocalService").rejects(error); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(error.message) + .resolves(); + expect( + await new DependencyHandler("test.Handler.id").bind(bindContext) + ).to.be.equal(undefined); + }); + + it("ok:: 'bind' succeedded", async () => { + sandbox + .stub(cfViewCommands, "bindLocalService") + .resolves(["testInstance"]); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "resourceType" }); + + expect( + await new DependencyHandler("test.Handler.id").bind(bindContext) + ).to.be.deep.equal({ + configData: bindContext.configData, + resource: { name: "testInstance", type: "resourceType" }, + }); + }); + + it("ok:: 'bind' - with tag", async () => { + const resourceTag: string = _.get( + bindContext, + "depContext.data.resourceTag" + ); + const resourceName: string = _.get( + bindContext, + "depContext.data.resourceName" + ); + const serviceType = [ + { + name: bindContext.depContext.type, + plan: _.get(bindContext.depContext, ["data", "plan"], ""), + tag: resourceTag ? resourceTag + resourceName : "", + prompt: "", + }, + ]; + sandbox + .stub(cfViewCommands, "bindLocalService") + .withArgs(serviceType, bindContext.envPath) + .resolves(["testInstance"]); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "resourceType" }); + + expect( + await new DependencyHandler("test.Handler.id").bind(bindContext) + ).to.be.deep.equal({ + configData: bindContext.configData, + resource: { name: "testInstance", type: "resourceType" }, + }); + }); + + it("ok:: 'bind' - no tag", async () => { + const serviceType = [ + { + name: bindContext.depContext.type, + plan: _.get(bindContext.depContext, ["data", "plan"], ""), + tag: "", + prompt: "", + }, + ]; + const copyContext = _.cloneDeep(bindContext); + copyContext.depContext.data = {}; + sandbox + .stub(cfViewCommands, "bindLocalService") + .withArgs(serviceType, bindContext.envPath) + .resolves(["testInstance"]); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "resourceType" }); + + expect( + await new DependencyHandler("test.Handler.id").bind(copyContext) + ).to.be.deep.equal({ + configData: copyContext.configData, + resource: { name: "testInstance", type: "resourceType" }, + }); + }); + + it("ok:: 'bind' - create chisel task - return undefined", async () => { + sandbox + .stub(chisel, "checkAndCreateChiselTask") + .withArgs(bindContext.envPath.fsPath, instances.join("&")) + .resolves(undefined); + sandbox.stub(cfViewCommands, "bindLocalService").resolves(instances); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "resourceType" }); + + const copyContext = _.cloneDeep(bindContext); + copyContext.depContext.data = { isCreateChiselTask: true }; + expect( + await new DependencyHandler("test.Handler.id").bind(copyContext) + ).to.be.deep.equal({ + configData: copyContext.configData, + resource: { name: "testInstance", type: "resourceType" }, + }); + }); + + it("ok:: 'bind' - create chisel task - no depended tasks", async () => { + const chiselTask = { label: "chiselLabel", data: { context: "some" } }; + sandbox + .stub(chisel, "checkAndCreateChiselTask") + .withArgs( + _.get(bindContext, "envPath.fsPath") as string, + instances.join("&") + ) + .resolves(chiselTask); + sandbox.stub(cfViewCommands, "bindLocalService").resolves(instances); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "ahana" }); + const copyContext = _.cloneDeep(bindContext); + copyContext.depContext.data = { isCreateChiselTask: true }; + + copyContext.configData.dependentTasks = undefined; + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs( + `A task for opening the VPN tunnel to the Cloud Foundry space has been created. Name: '${chiselTask.label}'` + ) + .resolves(); + expect( + await new DependencyHandler("test.Handler.id").bind(copyContext, { + "skip-reload": false, + }) + ).to.be.deep.equal({ + configData: copyContext.configData, + resource: { + name: "testInstance", + type: "ahana", + data: { chiselTask }, + }, + }); + }); + + it("ok:: 'bind' - create chisel task - no depended tasks, create chisel task is not required explicitly", async () => { + const chiselTask = { label: "chiselLabel", data: { context: "some" } }; + sandbox + .stub(chisel, "checkAndCreateChiselTask") + .withArgs( + _.get(bindContext, "envPath.fsPath") as string, + instances.join("&") + ) + .resolves(chiselTask); + sandbox.stub(cfViewCommands, "bindLocalService").resolves(instances); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "hanatrial" }); + const copyContext = _.cloneDeep(bindContext); + + copyContext.configData.dependentTasks = undefined; + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs( + `A task for opening the VPN tunnel to the Cloud Foundry space has been created. Name: '${chiselTask.label}'` + ) + .resolves(); + expect( + await new DependencyHandler("test.Handler.id").bind(copyContext) + ).to.be.deep.equal({ + configData: copyContext.configData, + resource: { + name: "testInstance", + type: "hanatrial", + data: { chiselTask }, + }, + }); + }); + + it("ok:: 'bind' - create chisel task - no depended tasks, silent mode required", async () => { + const chiselTask = { label: "chiselLabel", data: { context: "some" } }; + sandbox + .stub(chisel, "checkAndCreateChiselTask") + .withArgs( + _.get(bindContext, "envPath.fsPath") as string, + instances.join("&") + ) + .resolves(chiselTask); + sandbox.stub(cfViewCommands, "bindLocalService").resolves(instances); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "resourceType" }); + const copyContext = _.cloneDeep(bindContext); + copyContext.depContext.data = { isCreateChiselTask: true }; + + copyContext.configData.dependentTasks = undefined; + vscodeWindowMock.expects("showInformationMessage").never(); + expect( + await new DependencyHandler("test.Handler.id").bind(copyContext, { + silent: true, + }) + ).to.be.deep.equal({ + configData: copyContext.configData, + resource: { + name: "testInstance", + type: "resourceType", + data: { chiselTask }, + }, + }); + }); + + it("ok:: 'bind' - create chisel task - depended tasks", async () => { + const chiselLabel = "chiselLabel"; + const tsk = { name: "task" }; + const chiselJson = { label: chiselLabel, data: { context: "some" } }; + sandbox + .stub(chisel, "checkAndCreateChiselTask") + .withArgs(bindContext.envPath.fsPath, instances.join("&")) + .resolves(chiselJson); + sandbox.stub(cfViewCommands, "bindLocalService").resolves(instances); + sandbox + .stub(cfLocal, "cfGetInstanceMetadata") + .resolves({ serviceName: "testInstance", service: "resourceType" }); + const copyContext = _.cloneDeep(bindContext); + copyContext.configData.config.data.envFile = undefined; + copyContext.configData.dependentTasks = [tsk]; + copyContext.depContext.data = { isCreateChiselTask: true }; + + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs( + `A task for opening the VPN tunnel to the Cloud Foundry space has been created. Name: '${chiselLabel}'` + ) + .resolves(); + expect( + await new DependencyHandler("test.Handler.id").bind(copyContext) + ).to.be.deep.equal({ + configData: copyContext.configData, + resource: { + name: "testInstance", + type: "resourceType", + data: { chiselTask: chiselJson }, + }, + }); + assert.deepEqual(copyContext.configData.dependentTasks, [ + tsk, + chiselJson, + ]); + }); + + it("ok:: 'unbind' succeddeed", async () => { + const configurationData = { + config: { data: {}, type: ConfigurationTarget.launch }, + dependentTasks: [{}], + }; + configurationData.dependentTasks = []; + const property = { + resourceName: "propName", + envPath: "env Path", + resourceData: { label: "resource-type" }, + }; + sandbox.stub(utils, "removeResourceFromEnv").resolves(property); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs( + messages.service_unbound_successful(property.resourceName) + ) + .resolves(); + assert.deepEqual( + await new DependencyHandler("test.Handler.id").unbind(bindContext), + { + configData: configurationData, + resource: { + name: "propName", + type: "resource-type", + data: { label: "resource-type" }, + }, + } + ); + }); + + it("ok:: 'unbind' - empty property", async () => { + const configurationData = { + config: { data: {}, type: ConfigurationTarget.launch }, + dependentTasks: [{}], + }; + configurationData.dependentTasks = []; + const property = { resourceName: "", envPath: "", resourceData: {} }; + sandbox.stub(utils, "removeResourceFromEnv").resolves(property); + vscodeWindowMock + .expects("showInformationMessage") + .withExactArgs( + messages.service_unbound_successful(_.get(property, "resourceName")) + ) + .resolves(); + assert.deepEqual( + await new DependencyHandler("test.Handler.id").unbind(bindContext), + { + configData: configurationData, + resource: { name: "", type: "", data: {} }, + } + ); + }); + + it("ok:: 'unbind' - exception", async () => { + const error = new Error("my error"); + sandbox.stub(utils, "removeResourceFromEnv").rejects(error); + vscodeWindowMock + .expects("showErrorMessage") + .withExactArgs(error.message) + .resolves(); + expect( + await new DependencyHandler("test.Handler.id").unbind(bindContext) + ).to.be.equal(undefined); + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tests/utils.spec.ts b/projects/cloud-foundry-tools/packages/backend/tests/utils.spec.ts new file mode 100644 index 000000000..fffdf90da --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tests/utils.spec.ts @@ -0,0 +1,905 @@ +import { expect, assert } from "chai"; +import * as sinon from "sinon"; +import * as path from "path"; +import * as fs from "fs"; +import * as fsSync from "fs"; +import _ = require("lodash"); +import { stringify } from "comment-json"; +import { fail } from "assert"; +import * as nsVsMock from "./ext/mockVscode"; +import { mockVscode } from "./ext/mockUtil"; + +mockVscode(nsVsMock.testVscode, "src/utils.ts"); +import * as utils from "../src/utils"; +import * as cfLocal from "@sap/cf-tools/out/src/cf-local"; +import { ServiceInstanceInfo, eFilters, ITarget } from "@sap/cf-tools"; +import * as PropertiesReader from "properties-reader"; +import * as messages from "../src/messages"; + +describe("utils unit tests", () => { + let sandbox: sinon.SinonSandbox; + let vscodeWorkspaceMock: sinon.SinonMock; + let fsMock: sinon.SinonMock; + + before(() => { + sandbox = sinon.createSandbox(); + }); + + beforeEach(() => { + vscodeWorkspaceMock = sandbox.mock(nsVsMock.testVscode.workspace); + fsMock = sandbox.mock(fs.promises); + }); + + afterEach(() => { + vscodeWorkspaceMock.verify(); + fsMock.verify(); + sandbox.restore(); + }); + + describe("toText", () => { + it("ok:: 'name' attribute defined", () => { + const name = "test name"; + const e = new Error(); + e.name = name; + expect(utils.toText(e)).to.be.equal(name); + }); + + it("ok:: 'message' attribute defined", () => { + const message = "test description"; + const e = new Error(message); + expect(utils.toText(e)).to.be.equal(message); + }); + + it("ok:: neither 'message' and 'name' attribute is defined", () => { + const e = new Error(); + e.stack = "stack trace"; + expect(utils.toText(e)).to.be.equal(_.toString(e)); + }); + }); + + describe("getEnvResources", () => { + it("ok:: sanity with valid VCAP_SERVICES value", async () => { + sandbox.stub(fsSync, "existsSync").returns(true); + const envFilePath: string = path.join( + __dirname, + "resources", + ".testValidEnv" + ); + const envResources = await utils.getEnvResources(envFilePath); + const vcapServicesObj = envResources.vcapObject; + expect(vcapServicesObj).to.not.be.empty; + expect(vcapServicesObj.hana).to.exist; + }); + + it("ok:: sanity with valid VCAP_SERVICES value surrounded with single quotes", async () => { + sandbox.stub(fsSync, "existsSync").returns(true); + const envFilePath: string = path.join( + __dirname, + "resources", + ".testValidEnvVCAPWithQuotes" + ); + const envResources = await utils.getEnvResources(envFilePath); + const vcapServicesObj = envResources.vcapObject; + expect(vcapServicesObj).to.not.be.empty; + expect(vcapServicesObj.hana).to.exist; + }); + + it("ok:: sanity with valid VCAP_SERVICES value surrounded with single quotes and spaces", async () => { + sandbox.stub(fsSync, "existsSync").returns(true); + const envFilePath: string = path.join( + __dirname, + "resources", + ".testValidEnvVCAPWithQuotesAndSpaces" + ); + const envResources = await utils.getEnvResources(envFilePath); + const vcapServicesObj = envResources.vcapObject; + expect(vcapServicesObj).to.not.be.empty; + expect(vcapServicesObj.hana).to.exist; + }); + + it("fail:: invalid VCAP_SERVICES value in env file", async () => { + sandbox.stub(fsSync, "existsSync").returns(true); + const envFilePath: string = path.join( + __dirname, + "resources", + ".testInValidVCAP" + ); + let vcapServicesObj; + try { + const envResources = await utils.getEnvResources(envFilePath); + vcapServicesObj = envResources.vcapObject; + fail("test should fail here"); + } catch (e) { + expect(vcapServicesObj).to.be.undefined; + } + }); + + it("ok:: '.env' file does not exist", async () => { + sandbox.stub(fsSync, "existsSync").returns(false); + const envFilePath: string = path.join( + __dirname, + "resources", + ".notExists" + ); + expect((await utils.getEnvResources(envFilePath)).vcapObject).to.be.null; + }); + + it("ok:: '.env' file without VCAP_SERVICES key", async () => { + sandbox.stub(fsSync, "existsSync").returns(true); + const envFilePath: string = path.join( + __dirname, + "resources", + ".envNoVCAP" + ); + expect((await utils.getEnvResources(envFilePath)).vcapObject).to.be.null; + }); + }); + + describe("Tests for removeResourceFromEnv when VCAP_SERVICES surrounded with quotes", () => { + const envFilePath: string = path.join( + __dirname, + "resources", + ".testValidEnvVCAPWithQuotes" + ); + const propReader = PropertiesReader(envFilePath); + let vcapServicesObjBeforeChange: any; + before(() => { + sandbox = sinon.createSandbox(); + vcapServicesObjBeforeChange = propReader.get(utils.ENV_VCAP_RESOURCES); + }); + + afterEach(async () => { + sandbox.restore(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + propReader.set(utils.ENV_VCAP_RESOURCES, vcapServicesObjBeforeChange); + await propReader.save(envFilePath); + }); + + it("ok:: remove resource when VCAP is surrounded with single quotes and leave single quotes after removal", async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const bindContext: any = { + envPath: { + fsPath: envFilePath, + }, + depContext: { + type: "hana", + }, + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext); + const vcapServicesObjAfterChange: string | null = PropertiesReader( + envFilePath + ).getRaw(utils.ENV_VCAP_RESOURCES); + + if (vcapServicesObjAfterChange) { + expect(vcapServicesObjAfterChange).to.not.include("hana"); + expect(_.startsWith(vcapServicesObjAfterChange, "'")).to.be.true; + expect(_.endsWith(vcapServicesObjAfterChange, "'")).to.be.true; + } else { + expect( + fail( + "VCAP_SERVICES must exists and be empty after removing the last dependency" + ) + ); + } + }); + }); + + describe("Tests for removeResourceFromEnv", () => { + const envFilePath: string = path.join(__dirname, "resources", ".envtest"); + const propReader = PropertiesReader(envFilePath); + let vcapServicesObjBeforeChange: any; + before(() => { + sandbox = sinon.createSandbox(); + vcapServicesObjBeforeChange = propReader.get(utils.ENV_VCAP_RESOURCES); + }); + + afterEach(async () => { + sandbox.restore(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + propReader.set(utils.ENV_VCAP_RESOURCES, vcapServicesObjBeforeChange); + await propReader.save(envFilePath); + }); + + // does not work on linux ?! - clarification needed + it("Test removeResourceFromEnv - exception thrown", async () => { + const bindContext: any = { + envPath: { + fsPath: envFilePath + "_readonly", + }, + depContext: { + type: "hana", + data: { + resourceTag: "tag-not-found:", + }, + displayName: "bookshop-hdi-container", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + fsSync.chmodSync(bindContext.envPath.fsPath, 0o444); + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext); + fail("test should fail here"); + } catch (e) { + // ... + } + }); + + it("ok:: remove resource when it is not tagged & verify it is left unquoted as it was.", async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const bindContext: any = { + envPath: { + fsPath: envFilePath, + }, + depContext: { + type: "xsuaa", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext); + const envPropertiesAfterChange = PropertiesReader(envFilePath); + const vcapProperty: string = + envPropertiesAfterChange.getRaw(utils.ENV_VCAP_RESOURCES) ?? ""; + expect(vcapProperty).to.not.include("xsuaa"); + expect(vcapProperty).to.include('"instance_name":"hdi-test-instance"'); + expect(vcapProperty).to.not.include("xsuaa"); + + expect(_.startsWith(vcapProperty, "'")).to.be.false; + expect(_.endsWith(vcapProperty, "'")).to.be.false; + }); + + it("ok:: remove resource when it is not tagged by wrong type", async () => { + const bindContext: any = { + envPath: { + fsPath: envFilePath, + }, + depContext: { + type: "_xsuaa", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext); + assert.deepEqual( + PropertiesReader(envFilePath).get(utils.ENV_VCAP_RESOURCES), + vcapServicesObjBeforeChange + ); + }); + + it("ok:: remove resource when it has a tag", async () => { + const bindContext: any = { + envPath: { + fsPath: envFilePath, + }, + depContext: { + type: "hana", + data: { + resourceTag: "mta-resource-name:", + resourceName: "bookshop-hdi-container", + }, + displayName: "bookshop-hdi-container", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext); + const envPropertiesAfterChange = PropertiesReader(envFilePath); + expect( + envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES) + ).to.not.include('"instance_name":"hdi-test-instance"'); + expect(envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES)).to.include( + "xsuaa" + ); + }); + + it("ok:: remove resource when it has a tag, resource type has more then 1 instance (e.g. two dependencies bound to different instances)", async () => { + let envPropertiesAfterChange; + const bindContext: any = { + envPath: { + fsPath: envFilePath, + }, + depContext: { + type: "hanatest", + data: { + resourceTag: "mta-resource-name:", + resourceName: "hana1-mta", + }, + displayName: "hana1-mta", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext); + envPropertiesAfterChange = PropertiesReader(envFilePath); + expect(envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES)).to.include( + "hanatest" + ); + expect(envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES)).to.include( + '"instance_name":"hana-2"' + ); + expect( + envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES) + ).to.not.include('"instance_name":"hana-1"'); + + // Another remove should remove this key completely and not leave an empty array + const bindContext2: any = { + envPath: { + fsPath: envFilePath, + }, + depContext: { + type: "hanatest", + data: { + resourceTag: "mta-resource-name:", + resourceName: "hana2-mta", + }, + displayName: "hana2-mta", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext2); + envPropertiesAfterChange = PropertiesReader(envFilePath); + expect( + envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES) + ).to.not.include("hanatest"); + expect( + envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES) + ).to.not.include('"instance_name":"hana-2"'); + expect( + envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES) + ).to.not.include('"instance_name":"hana-1"'); + }); + + it("ok:: remove resource when it has a tag but it has not been found", async () => { + const bindContext: any = { + envPath: { + fsPath: envFilePath, + }, + depContext: { + type: "hana", + data: { + resourceTag: "tag-not-found:", + }, + displayName: "bookshop-hdi-container", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await utils.removeResourceFromEnv(bindContext); + const envPropertiesAfterChange = PropertiesReader(envFilePath); + expect(envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES)).to.include( + '"instance_name":"hdi-test-instance"' + ); + expect(envPropertiesAfterChange.get(utils.ENV_VCAP_RESOURCES)).to.include( + "xsuaa" + ); + }); + }); + + describe("API for creation service", () => { + const plan = "application"; + it("generateParams4Service - xsuaa, plan - application", () => { + const data = utils.generateParams4Service("xsuaa", plan); + expect(_.startsWith(data.xsappname as string, "xsuaa_")).to.be.true; + expect(data["tenant-mode"]).to.be.equal("dedicated"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect(_.size(data["oauth2-configuration"]["redirect-uris"])).to.be.equal( + 1 + ); + }); + + it("generateParams4Service - xsuaa, plan - other", () => { + assert.deepEqual(utils.generateParams4Service("xsauu", "other"), {}); + }); + + it("validateParams - other : valid", () => { + expect(utils.validateParams("other")('{ "key" : "value" }')).to.be + .undefined; + }); + + it("validateParams - other : incorrect", () => { + expect( + _.size(utils.validateParams("other")('{ "key" :: "value" }')) + ).to.be.gt(1); + }); + + it("validateParams - xsuaa : valid", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + expect(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))).to.be + .undefined; + }); + + it("validateParams - xsuaa : invalid xsappname", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + xsuaaData.xsappname = "xs.ass_345"; + expect( + _.size(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))) + ).to.be.gt(1); + }); + + it("validateParams - xsuaa : padded xsappname", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + xsuaaData.xsappname = " "; + expect( + _.size(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))) + ).to.be.gt(1); + }); + + it("validateParams - xsuaa : missing xsappname", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + delete xsuaaData.xsappname; + expect( + _.size(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))) + ).to.be.gt(1); + }); + + it("validateParams - xsuaa : long xsappname", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + xsuaaData.xsappname = _.repeat("x", 103); + expect( + _.size(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))) + ).to.be.gt(1); + }); + + it("validateParams - xsuaa : invalid tenant-mode", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + xsuaaData["tenant-mode"] = "lite"; + expect( + _.size(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))) + ).to.be.gt(1); + }); + + it("validateParams - xsuaa : invalid redirect-uris", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + xsuaaData["oauth2-configuration"]["redirect-uris"] = [ + "localhost://*", + "localhos://*", + ]; + expect( + _.size(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))) + ).to.be.gt(1); + }); + + it("validateParams - xsuaa : missing redirect-uris", () => { + const xsuaaData = utils.generateParams4Service("xsuaa", plan); + delete xsuaaData["oauth2-configuration"]["redirect-uris"]; + expect(utils.validateParams("xsuaa", plan)(stringify(xsuaaData))).to.be + .undefined; + }); + }); + + describe("suite for getAllServiceInstances", () => { + const services: ServiceInstanceInfo[] = [ + { + label: "s1", + serviceName: "hana", + plan: "s-plan-1", + plan_guid: "s-plan-guid-1", + }, + { + label: "s1", + serviceName: "hana", + plan: "s-plan-2", + plan_guid: "s-plan-guid-2", + }, + ]; + const ups = [ + { + label: "ups1", + serviceName: "user-provided-service", + credentials: { tags: ["monodb", "hana"] }, + }, + { + label: "ups2", + serviceName: "user-provided-service", + credentials: { tags: ["david"] }, + }, + ]; + + it("ok:: there are both kind of services", async () => { + sandbox.stub(cfLocal, "cfGetManagedServiceInstances").resolves(services); + sandbox.stub(cfLocal, "cfGetUpsInstances").resolves(ups); + assert.deepEqual( + await utils.getAllServiceInstances({ ups: { isShow: true } }), + _.concat(services, ups) + ); + }); + + it("ok:: contain 'managed' and 'ups' filtered services", async () => { + sandbox.stub(cfLocal, "cfGetManagedServiceInstances").resolves(services); + sandbox.stub(cfLocal, "cfGetUpsInstances").resolves(ups); + assert.deepEqual( + await utils.getAllServiceInstances({ ups: { tag: "/.*dav.*/" } }), + _.concat(services, ups[1]) + ); + }); + + it("ok:: ups services are not required", async () => { + sandbox.stub(cfLocal, "cfGetManagedServiceInstances").resolves(services); + sandbox.stub(cfLocal, "cfGetUpsInstances").resolves(ups); + assert.deepEqual(await utils.getAllServiceInstances(), services); + }); + + it("ok:: there are ups filtered services", async () => { + const opts = { + query: { + filters: [ + { key: eFilters.broker_catalog_ids, value: "test" }, + { key: eFilters.space_guids, value: "testSpace" }, + ], + }, + ups: { tag: "monodb" }, + }; + const expectedQuery = _.cloneDeep(opts.query); + expectedQuery.filters = [ + { key: eFilters.space_guids, value: "testSpace" }, + ]; + sandbox.stub(cfLocal, "cfGetManagedServiceInstances").resolves(services); + sandbox + .stub(cfLocal, "cfGetUpsInstances") + .withArgs(expectedQuery) + .resolves(ups); + assert.deepEqual( + await utils.getAllServiceInstances(opts), + _.concat(services, ups[0]) + ); + }); + }); + + describe("getUpsServiceInstances", () => { + const ups = [ + { + label: "ups1", + serviceName: "user-provided-service", + credentials: { tags: ["monodb", "hana"] }, + }, + { + label: "ups2", + serviceName: "user-provided-service", + credentials: { tags: ["david"] }, + }, + { + label: "ups3", + serviceName: "user-provided-service", + credentials: { tags: "hana" }, + }, + { + label: "wrong-credential-tags", + serviceName: "user-provided-service", + credentials: { tags: 2 }, + }, + { + label: "wrong-credential-tags2", + serviceName: "user-provided-service", + credentials: { tags: [2, { data: "hana" }] }, + }, + { label: "wrong-credential", serviceName: "user-provided-service" }, + ]; + + it("ok:: get all user provides services, tags not specified", async () => { + sandbox.stub(cfLocal, "cfGetUpsInstances").resolves(ups); + assert.deepEqual(await utils.getUpsServiceInstances(), ups); + }); + + it("ok:: get all user provides services filtered by credential tag", async () => { + const options = { credentials: { tag: "hana" } }; + sandbox.stub(cfLocal, "cfGetUpsInstances").resolves(ups); + assert.deepEqual(await utils.getUpsServiceInstances(options), [ + ups[0], + ups[2], + ]); + }); + + it("ok:: get all user provides services filtered by credential tag as regex expression", async () => { + const options = { credentials: { tag: "/.*mono|vid.*/" } }; + sandbox.stub(cfLocal, "cfGetUpsInstances").resolves(ups); + assert.deepEqual(await utils.getUpsServiceInstances(options), [ + ups[0], + ups[1], + ]); + }); + }); + + const UTF8 = "utf8"; + const EOL = require("os").EOL; + const GITIGNORE = ".gitignore"; + + describe("updateGitIgnoreList suite", () => { + const project = { + uri: nsVsMock.testVscode.Uri.file(path.resolve("myProject")), + }; + const ignorePattern = new nsVsMock.testVscode.RelativePattern( + project, + GITIGNORE + ); + const gitIgnoreFile = nsVsMock.testVscode.Uri.file( + path.resolve(`${project.uri.fsPath}/${GITIGNORE}`) + ); + + it("ok:: '.env' pattern added", async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const gitIgnoreContent = _.join([" # empty line"], EOL); + const testEnv = nsVsMock.testVscode.Uri.file( + path.resolve(`${project.uri.fsPath}/subFolder/.env`) + ); + vscodeWorkspaceMock.expects("getWorkspaceFolder").returns(project); + vscodeWorkspaceMock + .expects("findFiles") + .withExactArgs(ignorePattern) + .returns([gitIgnoreFile]); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + fsMock + .expects("readFile") + .withExactArgs(gitIgnoreFile.fsPath, { encoding: UTF8 }) + .resolves(gitIgnoreContent); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const patterns = _.split(gitIgnoreContent, EOL); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const expectedPatterns = _.join( + _.concat(patterns, [`# auto generated wildcard`, "subFolder/.env"]), + EOL + ); + fsMock + .expects("writeFile") + .withExactArgs(gitIgnoreFile.fsPath, expectedPatterns, { + encoding: UTF8, + }) + .resolves(undefined); + await utils.updateGitIgnoreList(testEnv.fsPath); + }); + + it("ok:: '.env' pattern not added - already exists", async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const gitIgnoreContent = _.join( + [" # empty line", "subFolder/.env"], + EOL + ); + const testEnv = nsVsMock.testVscode.Uri.file( + path.resolve(`${project.uri.fsPath}/subFolder/.env`) + ); + vscodeWorkspaceMock.expects("getWorkspaceFolder").returns(project); + vscodeWorkspaceMock.expects("findFiles").exactly(2).returns([testEnv]); + fsMock.expects("readFile").resolves(gitIgnoreContent); + fsMock.expects("writeFile").never(); + await utils.updateGitIgnoreList(testEnv.fsPath); + }); + + it("ok:: empty project", async () => { + const testEnv = nsVsMock.testVscode.Uri.file( + path.resolve(`${project.uri.fsPath}/subFolder/.env`) + ); + vscodeWorkspaceMock.expects("getWorkspaceFolder").returns(undefined); + await utils.updateGitIgnoreList(testEnv.fsPath); + }); + + it("ok:: '.env' pattern added, .gitIgnore file not exist", async () => { + const gitIgnoreContent = ""; + const testEnv = nsVsMock.testVscode.Uri.file( + path.resolve(`${project.uri.fsPath}/subFolder/.env`) + ); + vscodeWorkspaceMock.expects("getWorkspaceFolder").returns(project); + vscodeWorkspaceMock + .expects("findFiles") + .withExactArgs(ignorePattern) + .returns([]); + fsMock + .expects("open") + .withExactArgs(path.resolve(gitIgnoreFile.fsPath), "w") + .resolves(undefined); + fsMock + .expects("readFile") + .withExactArgs(gitIgnoreFile.fsPath, { encoding: UTF8 }) + .resolves(gitIgnoreContent); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const patterns = _.split(gitIgnoreContent, EOL); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const expectedPatterns = _.join( + _.concat(patterns, [`# auto generated wildcard`, "subFolder/.env"]), + EOL + ); + fsMock + .expects("writeFile") + .withExactArgs(gitIgnoreFile.fsPath, expectedPatterns, { + encoding: UTF8, + }) + .resolves(undefined); + await utils.updateGitIgnoreList(testEnv.fsPath); + }); + + it("fail:: '.env' pattern NOT added, .gitIgnore file not exist, creation failed", async () => { + const testEnv = nsVsMock.testVscode.Uri.file( + path.resolve(`${project.uri.fsPath}/subFolder/.env`) + ); + vscodeWorkspaceMock.expects("getWorkspaceFolder").returns(project); + vscodeWorkspaceMock + .expects("findFiles") + .withExactArgs(ignorePattern) + .returns([]); + fsMock + .expects("open") + .withExactArgs(path.resolve(gitIgnoreFile.fsPath), "w") + .throws(new Error("access denied")); + await utils.updateGitIgnoreList(testEnv.fsPath); + }); + + it("fail:: '.env' pattern NOT added, writeFile failed", async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const gitIgnoreContent = _.join([" # empty line"], EOL); + const testEnv = nsVsMock.testVscode.Uri.file( + path.resolve(`${project.uri.fsPath}/subFolder/.env`) + ); + vscodeWorkspaceMock.expects("getWorkspaceFolder").returns(project); + vscodeWorkspaceMock + .expects("findFiles") + .withExactArgs(ignorePattern) + .returns([gitIgnoreFile]); + fsMock + .expects("readFile") + .withExactArgs(gitIgnoreFile.fsPath, { encoding: UTF8 }) + .resolves(gitIgnoreContent); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const patterns = _.split(gitIgnoreContent, EOL); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const expectedPatterns = _.join( + _.concat(patterns, [`# auto generated wildcard`, "subFolder/.env"]), + EOL + ); + fsMock + .expects("writeFile") + .withExactArgs(gitIgnoreFile.fsPath, expectedPatterns, { + encoding: UTF8, + }) + .throws(new Error("access denied")); + await utils.updateGitIgnoreList(testEnv.fsPath); + }); + + describe("notifyWhenServicesInfoResultIncomplete method", () => { + let vscodeWindowMock: sinon.SinonMock; + + beforeEach(() => { + vscodeWindowMock = sandbox.mock(nsVsMock.testVscode.window); + }); + + afterEach(() => { + vscodeWindowMock.verify(); + }); + + it("ok:: serviceInstanceInfo list is fullfilled", async () => { + const infos: ServiceInstanceInfo[] = [ + { + label: "instance-1", + serviceName: "type-1", + plan: "plan-1", + }, + { + label: "instance-2", + serviceName: "type-2", + plan: "plan-2", + }, + ]; + expect( + await utils.notifyWhenServicesInfoResultIncomplete( + Promise.resolve(infos) + ) + ).to.be.deep.equal(infos); + }); + + it("ok:: serviceInstanceInfo list is empty", async () => { + const list = Promise.resolve([]); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect( + await utils.notifyWhenServicesInfoResultIncomplete(list) + ).to.be.deep.equal([]); + }); + + it("ok:: serviceInstanceInfo list is incomplete", async () => { + const infos: ServiceInstanceInfo[] = [ + { + label: "instance-1", + serviceName: "unknown", + plan: "plan-1", + }, + ]; + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.messages.service_instances_list_incomplete( + _.map(infos, "label") + ) + ) + .resolves(); + expect( + await utils.notifyWhenServicesInfoResultIncomplete( + Promise.resolve(infos) + ) + ).to.be.deep.equal(infos); + }); + + it("ok:: serviceInstanceInfo list is incomplete more than 3 instances", async () => { + const infos: ServiceInstanceInfo[] = [ + { + label: "instance-1", + serviceName: "unknown", + plan: "plan-1", + }, + { + label: "instance-2", + serviceName: "type-2", + plan: "unknown", + }, + { + label: "instance-3", + serviceName: "type-3", + plan: "unknown", + }, + { + label: "instance-4", + serviceName: "type-4", + plan: "unknown", + }, + ]; + vscodeWindowMock + .expects("showWarningMessage") + .withExactArgs( + messages.messages.service_instances_list_incomplete( + _.map(infos, "label") + ) + ) + .resolves(); + expect( + await utils.notifyWhenServicesInfoResultIncomplete( + Promise.resolve(infos) + ) + ).to.be.deep.equal(infos); + }); + }); + + describe("verifyCFTarget", () => { + const target: ITarget = { + "api endpoint": "endPoint", + org: "test-org", + space: "test-space", + user: "user", + "api version": "3.100.0", + }; + + it("ok:: validate", async () => { + sandbox.stub(cfLocal, "cfGetTarget").resolves(target); + expect(await utils.examCFTarget("", ["user"], false)).to.be.deep.equal( + target + ); + }); + + it("error:: invalid key requested", async () => { + sandbox.stub(cfLocal, "cfGetTarget").resolves(target); + const error = "invalid key"; + try { + await utils.examCFTarget(error, ["user", "wrong"], false); + fail("should fail"); + } catch (e) { + expect(e.message).to.be.equal(error); + } + }); + + it("ok:: weak mode", async () => { + const t: ITarget = { + "api endpoint": "endPoint", + user: "user", + "api version": "3.100.0", + }; + sandbox.stub(cfLocal, "cfGetTarget").resolves(t); + expect( + await utils.examCFTarget("", ["api endpoint"], true) + ).to.be.deep.equal(t); + }); + + it("exception:: throws error", async () => { + const text = "error text"; + sandbox.stub(cfLocal, "cfGetTarget").throws(new Error(text)); + try { + await utils.examCFTarget("", [], false); + fail("should fail"); + } catch (e) { + expect(e.message).to.be.equal(text); + } + }); + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/backend/tsconfig.json b/projects/cloud-foundry-tools/packages/backend/tsconfig.json new file mode 100644 index 000000000..ded225140 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "rootDir": ".", + "rootDirs": ["./src", "./tests"], + "useUnknownInCatchVariables": false + } +} diff --git a/projects/cloud-foundry-tools/packages/backend/webpack.config.js b/projects/cloud-foundry-tools/packages/backend/webpack.config.js new file mode 100644 index 000000000..7bfaa1534 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/backend/webpack.config.js @@ -0,0 +1,31 @@ +const path = require("path"); +const baseConfig = require("../../webpack.config.vscode.base"); + +const config = Object.assign({}, baseConfig, { + entry: "./out/src/extension.js", + output: { + path: path.resolve(__dirname, "dist"), + filename: "extension.js", + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + module: { + // https://webpack.js.org/configuration/module/#modulenoparse + // used to avoid transforming native require usage in `optional-require` implementation + noParse: /native-require\.(js|ts)$/, + }, + // 📖 -> https://webpack.js.org/configuration/externals/ + externals: { + // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed. + vscode: "commonjs vscode", + // To enable bundling for @sap/artifact-management + // fsevents is a macos file system event library that is compiled during installation. + fsevents: "commonjs fsevents", + }, + node: { + // needed to bundle artifact-management successfully + __dirname: false, + }, +}); + +module.exports = config; diff --git a/projects/cloud-foundry-tools/packages/frontend/.vscode/launch.json b/projects/cloud-foundry-tools/packages/frontend/.vscode/launch.json new file mode 100644 index 000000000..7155e5192 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "vuejs: chrome", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}/src", + "breakOnLoad": true, + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + }, + "preLaunchTask": "serve" + } + ] +} diff --git a/projects/cloud-foundry-tools/packages/frontend/.vscode/tasks.json b/projects/cloud-foundry-tools/packages/frontend/.vscode/tasks.json new file mode 100644 index 000000000..f44649a7f --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "serve", + "type": "npm", + "script": "serve", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^(.*)$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^(.*)$", + "endsPattern": "^(.*)DONE(.*)$" + } + } + } + ] +} diff --git a/projects/cloud-foundry-tools/packages/frontend/CHANGELOG.md b/projects/cloud-foundry-tools/packages/frontend/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/projects/cloud-foundry-tools/packages/frontend/CHANGELOG.old.md b/projects/cloud-foundry-tools/packages/frontend/CHANGELOG.old.md new file mode 100644 index 000000000..d674f491b --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/CHANGELOG.old.md @@ -0,0 +1,160 @@ +# Historic Changelog + +> This file contains the changelog history from the standalone [SAP/cloud-foundry-tools](https://github.com/SAP/cloud-foundry-tools) repository, prior to migration into this monorepo. New changes are recorded in this package's `CHANGELOG.md`. + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [8.1.2](https://github.com/SAP/cloud-foundry-tools/compare/v8.1.0...v8.1.2) (2026-06-21) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [8.1.1](https://github.com/SAP/cloud-foundry-tools/compare/v8.1.0...v8.1.1) (2026-06-21) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +# [8.1.0](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.2...v8.1.0) (2026-02-11) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [8.0.2](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.1...v8.0.2) (2025-12-09) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [8.0.1](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.0...v8.0.1) (2025-08-24) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +# [8.0.0](https://github.com/SAP/cloud-foundry-tools/compare/v7.5.2...v8.0.0) (2025-08-20) + +### Features + +- package name and publisher are changed ([#304](https://github.com/SAP/cloud-foundry-tools/issues/304)) ([33e586d](https://github.com/SAP/cloud-foundry-tools/commit/33e586d67270602128d3c4c17179ac632a7c82a5)) + +### BREAKING CHANGES + +- VS Code Marketplace Publishing Support + +## [7.5.2](https://github.com/SAP/cloud-foundry-tools/compare/v7.5.1...v7.5.2) (2025-08-06) + +### Bug Fixes + +- organization dropdown wrapping ([5e69cf8](https://github.com/SAP/cloud-foundry-tools/commit/5e69cf8408cfa2f7eb45e5fc3ebaee38842568ef)) +- sign in button disabled state ([208eb68](https://github.com/SAP/cloud-foundry-tools/commit/208eb680372ce54fd176eabcd88c49a8f97bafde)) +- space dropdown wrapping ([b7e9f4f](https://github.com/SAP/cloud-foundry-tools/commit/b7e9f4fb6bf71ca50497b3e1f0d1daf702756674)) + +## [7.5.1](https://github.com/SAP/cloud-foundry-tools/compare/v7.5.0...v7.5.1) (2025-06-24) + +### Bug Fixes + +- `paste` is not happening in cloud foundry `sign in` page ([#301](https://github.com/SAP/cloud-foundry-tools/issues/301)) ([0385390](https://github.com/SAP/cloud-foundry-tools/commit/0385390328284840464673a665778c2e7a8511df)) +- prevent webview default `contextmenu` ([#300](https://github.com/SAP/cloud-foundry-tools/issues/300)) ([e79a1cb](https://github.com/SAP/cloud-foundry-tools/commit/e79a1cbdb2702e060ada5909aff7d8e15675c3a1)) + +# [7.5.0](https://github.com/SAP/cloud-foundry-tools/compare/v7.4.5...v7.5.0) (2025-02-10) + +### Bug Fixes + +- adding back the missing clip icon in the passphrase input field ([63aa214](https://github.com/SAP/cloud-foundry-tools/commit/63aa21498c7778224f38d9b1a0c4954557b42c77)) +- address live demo comments ([0ae1abb](https://github.com/SAP/cloud-foundry-tools/commit/0ae1abbb647a4c191afbdebbe3caecc78b49ed12)) +- icon inside vscode-button added id for codicon link ([aa530e6](https://github.com/SAP/cloud-foundry-tools/commit/aa530e6601234449bf488c92dd46ac65725c3c96)) +- reverting selection of first org in list ([a839de7](https://github.com/SAP/cloud-foundry-tools/commit/a839de7c2d9c426b684434b0490cc5f8615a55be)) +- updating user facing messages ([8421d55](https://github.com/SAP/cloud-foundry-tools/commit/8421d55d7cfc32fd16d49992546673ab287d0458)) + +### Features + +- devxcore-3010 cf missing next steps after signing in to cf adding org and space ([ae116e4](https://github.com/SAP/cloud-foundry-tools/commit/ae116e4e81bd4310c60e070d77c6052783f4f29f)) +- migrate to vscode elements ([22547a5](https://github.com/SAP/cloud-foundry-tools/commit/22547a5a9e0c3425212c8220f17ee7ba0469bbce)) + +## [7.4.5](https://github.com/SAP/cloud-foundry-tools/compare/v7.4.4...v7.4.5) (2024-10-14) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [7.4.4](https://github.com/SAP/cloud-foundry-tools/compare/v7.4.3...v7.4.4) (2024-05-30) + +### Bug Fixes + +- failed to login to CF with SSO passcode starting with a hyphen ([#293](https://github.com/SAP/cloud-foundry-tools/issues/293)) ([9bb5e3a](https://github.com/SAP/cloud-foundry-tools/commit/9bb5e3adfef757238cf01c0d83190af957349b0e)) + +## [7.4.3](https://github.com/SAP/cloud-foundry-tools/compare/v7.4.2...v7.4.3) (2024-05-05) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [7.4.2](https://github.com/SAP/cloud-foundry-tools/compare/v7.4.1...v7.4.2) (2023-11-26) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [7.4.1](https://github.com/SAP/cloud-foundry-tools/compare/v7.4.0...v7.4.1) (2023-11-12) + +### Bug Fixes + +- sorted orgs and spaces + tests ([#277](https://github.com/SAP/cloud-foundry-tools/issues/277)) ([459801a](https://github.com/SAP/cloud-foundry-tools/commit/459801a3bd2a8bb796fef4a3e0fcb6a74d9ab9f6)) + +# [7.4.0](https://github.com/SAP/cloud-foundry-tools/compare/v7.3.4...v7.4.0) (2023-10-03) + +### Features + +- migrate to vite.js and upgrade deps ([#271](https://github.com/SAP/cloud-foundry-tools/issues/271)) ([6e4381c](https://github.com/SAP/cloud-foundry-tools/commit/6e4381cd889da81424da5ce0e11bd5b78b15121c)) +- migrating to Vue3 ([#258](https://github.com/SAP/cloud-foundry-tools/issues/258)) ([1bbe976](https://github.com/SAP/cloud-foundry-tools/commit/1bbe9764f3af8989d15cd5ba48cb706fc97c83d3)) + +## [7.3.4](https://github.com/SAP/cloud-foundry-tools/compare/v7.3.3...v7.3.4) (2023-08-09) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [7.3.3](https://github.com/SAP/cloud-foundry-tools/compare/v7.3.2...v7.3.3) (2023-07-09) + +### Bug Fixes + +- validate CF endpoint URL for security ([#262](https://github.com/SAP/cloud-foundry-tools/issues/262)) ([c675000](https://github.com/SAP/cloud-foundry-tools/commit/c675000514ced213ff21f6c4534161435b2ce794)) + +## [7.3.2](https://github.com/SAP/cloud-foundry-tools/compare/v7.3.1...v7.3.2) (2023-07-06) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [7.3.1](https://github.com/SAP/cloud-foundry-tools/compare/v7.3.0...v7.3.1) (2023-07-04) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +# [7.3.0](https://github.com/SAP/cloud-foundry-tools/compare/v7.2.2...v7.3.0) (2023-07-04) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +## [7.2.2](https://github.com/SAP/cloud-foundry-tools/compare/v7.2.1...v7.2.2) (2023-06-29) + +### Bug Fixes + +- sign out button in fiori themes ([#259](https://github.com/SAP/cloud-foundry-tools/issues/259)) ([d4bfa2e](https://github.com/SAP/cloud-foundry-tools/commit/d4bfa2e4d8f0dd6e130ca7a8f1865fffd7ece3d5)) + +## [7.2.1](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.2.0...v7.2.1) (2023-02-15) + +**Note:** Version bump only for package cloud-foundry-tools-frontend + +# [7.1.0](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.0.4...v7.1.0) (2022-11-16) + +### Bug Fixes + +- [DEVXCORE-2589] fix type of endpoint control ([#245](https://github.com/sap-staging/cloud-foundry-tools/issues/245)) ([48ef31d](https://github.com/sap-staging/cloud-foundry-tools/commit/48ef31dc61478bd56190edfc14c3587111732a74)) +- make dropdown control searchable ([#248](https://github.com/sap-staging/cloud-foundry-tools/issues/248)) ([126d27b](https://github.com/sap-staging/cloud-foundry-tools/commit/126d27b7c24da77fc91bf3fac7b59e771a9e5c74)) + +## [7.0.4](https://github.com/sap-staging/cloud-foundry-tools/compare/v7.0.1...v7.0.4) (2022-11-03) + +### Bug Fixes + +- [DEVXBUGS-10581] CF sign doesn't detect the last used API endpoint ([#243](https://github.com/sap-staging/cloud-foundry-tools/issues/243)) ([a2b7c93](https://github.com/sap-staging/cloud-foundry-tools/commit/a2b7c93b24db17bd292d6b30dfc3310f67680d47)) +- adding tooltips to target ([e4f53e4](https://github.com/sap-staging/cloud-foundry-tools/commit/e4f53e4009971ef61f1b5305fcba4b784fb3d5dd)) +- bugs ([dc5d257](https://github.com/sap-staging/cloud-foundry-tools/commit/dc5d2574c86d1f9b1bca01ea44983c1e97151988)) +- remove the important from the style ([a426e5c](https://github.com/sap-staging/cloud-foundry-tools/commit/a426e5cb88df128c623c08dc4cedfcc98d9376a6)) +- review fixes ([cb0361d](https://github.com/sap-staging/cloud-foundry-tools/commit/cb0361d25817a06fbe8379e583f9cbda6eb2fa05)) + +## [7.0.1](https://github.com/SAP/cloud-foundry-tools/compare/v7.0.0...v7.0.1) (2022-06-29) + +### Bug Fixes + +- leftovers ([#225](https://github.com/SAP/cloud-foundry-tools/issues/225)) ([c6cedcb](https://github.com/SAP/cloud-foundry-tools/commit/c6cedcb0a67f40f3040d258b442128cb19fc70cf)) + +# [7.0.0](https://github.com/SAP/cloud-foundry-tools/compare/v6.5.0...v7.0.0) (2022-06-28) + +### Features + +- support 2fa in the cf tools ui ([#224](https://github.com/SAP/cloud-foundry-tools/issues/224)) ([5ed4d20](https://github.com/SAP/cloud-foundry-tools/commit/5ed4d20a88b86df180cf98db3dbf615e5e20ddda)) diff --git a/projects/cloud-foundry-tools/packages/frontend/babel.config.js b/projects/cloud-foundry-tools/packages/frontend/babel.config.js new file mode 100644 index 000000000..fa911e233 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ["@babel/preset-env", "@babel/preset-typescript"], + plugins: ["@babel/plugin-transform-modules-commonjs"], +}; diff --git a/projects/cloud-foundry-tools/packages/frontend/codicon.css b/projects/cloud-foundry-tools/packages/frontend/codicon.css new file mode 100644 index 000000000..aac4b12d1 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/codicon.css @@ -0,0 +1,553 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +@font-face { + font-family: "codicon"; + font-display: block; + src: url("./codicon.ttf?f6ab4abcfdcac2bb563bb55b9602bc7f") format("truetype"); +} + +.codicon[class*='codicon-'] { + font: normal normal normal 16px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +/*--------------------- + * Modifiers + *-------------------*/ + +@keyframes codicon-spin { + 100% { + transform:rotate(360deg); + } +} + +.codicon-sync.codicon-modifier-spin, +.codicon-loading.codicon-modifier-spin, +.codicon-gear.codicon-modifier-spin { + /* Use steps to throttle FPS to reduce CPU usage */ + animation: codicon-spin 1.5s steps(30) infinite; +} + +.codicon-modifier-disabled { + opacity: 0.5; +} + +/* custom speed & easing for loading icon */ +.codicon-loading { + animation-duration: 1s !important; + animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important; +} + +/*--------------------- + * Icons + *-------------------*/ + +.codicon-add:before { content: "\ea60" } +.codicon-plus:before { content: "\ea60" } +.codicon-gist-new:before { content: "\ea60" } +.codicon-repo-create:before { content: "\ea60" } +.codicon-lightbulb:before { content: "\ea61" } +.codicon-light-bulb:before { content: "\ea61" } +.codicon-repo:before { content: "\ea62" } +.codicon-repo-delete:before { content: "\ea62" } +.codicon-gist-fork:before { content: "\ea63" } +.codicon-repo-forked:before { content: "\ea63" } +.codicon-git-pull-request:before { content: "\ea64" } +.codicon-git-pull-request-abandoned:before { content: "\ea64" } +.codicon-record-keys:before { content: "\ea65" } +.codicon-keyboard:before { content: "\ea65" } +.codicon-tag:before { content: "\ea66" } +.codicon-tag-add:before { content: "\ea66" } +.codicon-tag-remove:before { content: "\ea66" } +.codicon-person:before { content: "\ea67" } +.codicon-person-follow:before { content: "\ea67" } +.codicon-person-outline:before { content: "\ea67" } +.codicon-person-filled:before { content: "\ea67" } +.codicon-git-branch:before { content: "\ea68" } +.codicon-git-branch-create:before { content: "\ea68" } +.codicon-git-branch-delete:before { content: "\ea68" } +.codicon-source-control:before { content: "\ea68" } +.codicon-mirror:before { content: "\ea69" } +.codicon-mirror-public:before { content: "\ea69" } +.codicon-star:before { content: "\ea6a" } +.codicon-star-add:before { content: "\ea6a" } +.codicon-star-delete:before { content: "\ea6a" } +.codicon-star-empty:before { content: "\ea6a" } +.codicon-comment:before { content: "\ea6b" } +.codicon-comment-add:before { content: "\ea6b" } +.codicon-alert:before { content: "\ea6c" } +.codicon-warning:before { content: "\ea6c" } +.codicon-search:before { content: "\ea6d" } +.codicon-search-save:before { content: "\ea6d" } +.codicon-log-out:before { content: "\ea6e" } +.codicon-sign-out:before { content: "\ea6e" } +.codicon-log-in:before { content: "\ea6f" } +.codicon-sign-in:before { content: "\ea6f" } +.codicon-eye:before { content: "\ea70" } +.codicon-eye-unwatch:before { content: "\ea70" } +.codicon-eye-watch:before { content: "\ea70" } +.codicon-circle-filled:before { content: "\ea71" } +.codicon-primitive-dot:before { content: "\ea71" } +.codicon-close-dirty:before { content: "\ea71" } +.codicon-debug-breakpoint:before { content: "\ea71" } +.codicon-debug-breakpoint-disabled:before { content: "\ea71" } +.codicon-debug-hint:before { content: "\ea71" } +.codicon-primitive-square:before { content: "\ea72" } +.codicon-edit:before { content: "\ea73" } +.codicon-pencil:before { content: "\ea73" } +.codicon-info:before { content: "\ea74" } +.codicon-issue-opened:before { content: "\ea74" } +.codicon-gist-private:before { content: "\ea75" } +.codicon-git-fork-private:before { content: "\ea75" } +.codicon-lock:before { content: "\ea75" } +.codicon-mirror-private:before { content: "\ea75" } +.codicon-close:before { content: "\ea76" } +.codicon-remove-close:before { content: "\ea76" } +.codicon-x:before { content: "\ea76" } +.codicon-repo-sync:before { content: "\ea77" } +.codicon-sync:before { content: "\ea77" } +.codicon-clone:before { content: "\ea78" } +.codicon-desktop-download:before { content: "\ea78" } +.codicon-beaker:before { content: "\ea79" } +.codicon-microscope:before { content: "\ea79" } +.codicon-vm:before { content: "\ea7a" } +.codicon-device-desktop:before { content: "\ea7a" } +.codicon-file:before { content: "\ea7b" } +.codicon-file-text:before { content: "\ea7b" } +.codicon-more:before { content: "\ea7c" } +.codicon-ellipsis:before { content: "\ea7c" } +.codicon-kebab-horizontal:before { content: "\ea7c" } +.codicon-mail-reply:before { content: "\ea7d" } +.codicon-reply:before { content: "\ea7d" } +.codicon-organization:before { content: "\ea7e" } +.codicon-organization-filled:before { content: "\ea7e" } +.codicon-organization-outline:before { content: "\ea7e" } +.codicon-new-file:before { content: "\ea7f" } +.codicon-file-add:before { content: "\ea7f" } +.codicon-new-folder:before { content: "\ea80" } +.codicon-file-directory-create:before { content: "\ea80" } +.codicon-trash:before { content: "\ea81" } +.codicon-trashcan:before { content: "\ea81" } +.codicon-history:before { content: "\ea82" } +.codicon-clock:before { content: "\ea82" } +.codicon-folder:before { content: "\ea83" } +.codicon-file-directory:before { content: "\ea83" } +.codicon-symbol-folder:before { content: "\ea83" } +.codicon-logo-github:before { content: "\ea84" } +.codicon-mark-github:before { content: "\ea84" } +.codicon-github:before { content: "\ea84" } +.codicon-terminal:before { content: "\ea85" } +.codicon-console:before { content: "\ea85" } +.codicon-repl:before { content: "\ea85" } +.codicon-zap:before { content: "\ea86" } +.codicon-symbol-event:before { content: "\ea86" } +.codicon-error:before { content: "\ea87" } +.codicon-stop:before { content: "\ea87" } +.codicon-variable:before { content: "\ea88" } +.codicon-symbol-variable:before { content: "\ea88" } +.codicon-array:before { content: "\ea8a" } +.codicon-symbol-array:before { content: "\ea8a" } +.codicon-symbol-module:before { content: "\ea8b" } +.codicon-symbol-package:before { content: "\ea8b" } +.codicon-symbol-namespace:before { content: "\ea8b" } +.codicon-symbol-object:before { content: "\ea8b" } +.codicon-symbol-method:before { content: "\ea8c" } +.codicon-symbol-function:before { content: "\ea8c" } +.codicon-symbol-constructor:before { content: "\ea8c" } +.codicon-symbol-boolean:before { content: "\ea8f" } +.codicon-symbol-null:before { content: "\ea8f" } +.codicon-symbol-numeric:before { content: "\ea90" } +.codicon-symbol-number:before { content: "\ea90" } +.codicon-symbol-structure:before { content: "\ea91" } +.codicon-symbol-struct:before { content: "\ea91" } +.codicon-symbol-parameter:before { content: "\ea92" } +.codicon-symbol-type-parameter:before { content: "\ea92" } +.codicon-symbol-key:before { content: "\ea93" } +.codicon-symbol-text:before { content: "\ea93" } +.codicon-symbol-reference:before { content: "\ea94" } +.codicon-go-to-file:before { content: "\ea94" } +.codicon-symbol-enum:before { content: "\ea95" } +.codicon-symbol-value:before { content: "\ea95" } +.codicon-symbol-ruler:before { content: "\ea96" } +.codicon-symbol-unit:before { content: "\ea96" } +.codicon-activate-breakpoints:before { content: "\ea97" } +.codicon-archive:before { content: "\ea98" } +.codicon-arrow-both:before { content: "\ea99" } +.codicon-arrow-down:before { content: "\ea9a" } +.codicon-arrow-left:before { content: "\ea9b" } +.codicon-arrow-right:before { content: "\ea9c" } +.codicon-arrow-small-down:before { content: "\ea9d" } +.codicon-arrow-small-left:before { content: "\ea9e" } +.codicon-arrow-small-right:before { content: "\ea9f" } +.codicon-arrow-small-up:before { content: "\eaa0" } +.codicon-arrow-up:before { content: "\eaa1" } +.codicon-bell:before { content: "\eaa2" } +.codicon-bold:before { content: "\eaa3" } +.codicon-book:before { content: "\eaa4" } +.codicon-bookmark:before { content: "\eaa5" } +.codicon-debug-breakpoint-conditional-unverified:before { content: "\eaa6" } +.codicon-debug-breakpoint-conditional:before { content: "\eaa7" } +.codicon-debug-breakpoint-conditional-disabled:before { content: "\eaa7" } +.codicon-debug-breakpoint-data-unverified:before { content: "\eaa8" } +.codicon-debug-breakpoint-data:before { content: "\eaa9" } +.codicon-debug-breakpoint-data-disabled:before { content: "\eaa9" } +.codicon-debug-breakpoint-log-unverified:before { content: "\eaaa" } +.codicon-debug-breakpoint-log:before { content: "\eaab" } +.codicon-debug-breakpoint-log-disabled:before { content: "\eaab" } +.codicon-briefcase:before { content: "\eaac" } +.codicon-broadcast:before { content: "\eaad" } +.codicon-browser:before { content: "\eaae" } +.codicon-bug:before { content: "\eaaf" } +.codicon-calendar:before { content: "\eab0" } +.codicon-case-sensitive:before { content: "\eab1" } +.codicon-check:before { content: "\eab2" } +.codicon-checklist:before { content: "\eab3" } +.codicon-chevron-down:before { content: "\eab4" } +.codicon-chevron-left:before { content: "\eab5" } +.codicon-chevron-right:before { content: "\eab6" } +.codicon-chevron-up:before { content: "\eab7" } +.codicon-chrome-close:before { content: "\eab8" } +.codicon-chrome-maximize:before { content: "\eab9" } +.codicon-chrome-minimize:before { content: "\eaba" } +.codicon-chrome-restore:before { content: "\eabb" } +.codicon-circle-outline:before { content: "\eabc" } +.codicon-debug-breakpoint-unverified:before { content: "\eabc" } +.codicon-circle-slash:before { content: "\eabd" } +.codicon-circuit-board:before { content: "\eabe" } +.codicon-clear-all:before { content: "\eabf" } +.codicon-clippy:before { content: "\eac0" } +.codicon-close-all:before { content: "\eac1" } +.codicon-cloud-download:before { content: "\eac2" } +.codicon-cloud-upload:before { content: "\eac3" } +.codicon-code:before { content: "\eac4" } +.codicon-collapse-all:before { content: "\eac5" } +.codicon-color-mode:before { content: "\eac6" } +.codicon-comment-discussion:before { content: "\eac7" } +.codicon-credit-card:before { content: "\eac9" } +.codicon-dash:before { content: "\eacc" } +.codicon-dashboard:before { content: "\eacd" } +.codicon-database:before { content: "\eace" } +.codicon-debug-continue:before { content: "\eacf" } +.codicon-debug-disconnect:before { content: "\ead0" } +.codicon-debug-pause:before { content: "\ead1" } +.codicon-debug-restart:before { content: "\ead2" } +.codicon-debug-start:before { content: "\ead3" } +.codicon-debug-step-into:before { content: "\ead4" } +.codicon-debug-step-out:before { content: "\ead5" } +.codicon-debug-step-over:before { content: "\ead6" } +.codicon-debug-stop:before { content: "\ead7" } +.codicon-debug:before { content: "\ead8" } +.codicon-device-camera-video:before { content: "\ead9" } +.codicon-device-camera:before { content: "\eada" } +.codicon-device-mobile:before { content: "\eadb" } +.codicon-diff-added:before { content: "\eadc" } +.codicon-diff-ignored:before { content: "\eadd" } +.codicon-diff-modified:before { content: "\eade" } +.codicon-diff-removed:before { content: "\eadf" } +.codicon-diff-renamed:before { content: "\eae0" } +.codicon-diff:before { content: "\eae1" } +.codicon-discard:before { content: "\eae2" } +.codicon-editor-layout:before { content: "\eae3" } +.codicon-empty-window:before { content: "\eae4" } +.codicon-exclude:before { content: "\eae5" } +.codicon-extensions:before { content: "\eae6" } +.codicon-eye-closed:before { content: "\eae7" } +.codicon-file-binary:before { content: "\eae8" } +.codicon-file-code:before { content: "\eae9" } +.codicon-file-media:before { content: "\eaea" } +.codicon-file-pdf:before { content: "\eaeb" } +.codicon-file-submodule:before { content: "\eaec" } +.codicon-file-symlink-directory:before { content: "\eaed" } +.codicon-file-symlink-file:before { content: "\eaee" } +.codicon-file-zip:before { content: "\eaef" } +.codicon-files:before { content: "\eaf0" } +.codicon-filter:before { content: "\eaf1" } +.codicon-flame:before { content: "\eaf2" } +.codicon-fold-down:before { content: "\eaf3" } +.codicon-fold-up:before { content: "\eaf4" } +.codicon-fold:before { content: "\eaf5" } +.codicon-folder-active:before { content: "\eaf6" } +.codicon-folder-opened:before { content: "\eaf7" } +.codicon-gear:before { content: "\eaf8" } +.codicon-gift:before { content: "\eaf9" } +.codicon-gist-secret:before { content: "\eafa" } +.codicon-gist:before { content: "\eafb" } +.codicon-git-commit:before { content: "\eafc" } +.codicon-git-compare:before { content: "\eafd" } +.codicon-compare-changes:before { content: "\eafd" } +.codicon-git-merge:before { content: "\eafe" } +.codicon-github-action:before { content: "\eaff" } +.codicon-github-alt:before { content: "\eb00" } +.codicon-globe:before { content: "\eb01" } +.codicon-grabber:before { content: "\eb02" } +.codicon-graph:before { content: "\eb03" } +.codicon-gripper:before { content: "\eb04" } +.codicon-heart:before { content: "\eb05" } +.codicon-home:before { content: "\eb06" } +.codicon-horizontal-rule:before { content: "\eb07" } +.codicon-hubot:before { content: "\eb08" } +.codicon-inbox:before { content: "\eb09" } +.codicon-issue-reopened:before { content: "\eb0b" } +.codicon-issues:before { content: "\eb0c" } +.codicon-italic:before { content: "\eb0d" } +.codicon-jersey:before { content: "\eb0e" } +.codicon-json:before { content: "\eb0f" } +.codicon-kebab-vertical:before { content: "\eb10" } +.codicon-key:before { content: "\eb11" } +.codicon-law:before { content: "\eb12" } +.codicon-lightbulb-autofix:before { content: "\eb13" } +.codicon-link-external:before { content: "\eb14" } +.codicon-link:before { content: "\eb15" } +.codicon-list-ordered:before { content: "\eb16" } +.codicon-list-unordered:before { content: "\eb17" } +.codicon-live-share:before { content: "\eb18" } +.codicon-loading:before { content: "\eb19" } +.codicon-location:before { content: "\eb1a" } +.codicon-mail-read:before { content: "\eb1b" } +.codicon-mail:before { content: "\eb1c" } +.codicon-markdown:before { content: "\eb1d" } +.codicon-megaphone:before { content: "\eb1e" } +.codicon-mention:before { content: "\eb1f" } +.codicon-milestone:before { content: "\eb20" } +.codicon-mortar-board:before { content: "\eb21" } +.codicon-move:before { content: "\eb22" } +.codicon-multiple-windows:before { content: "\eb23" } +.codicon-mute:before { content: "\eb24" } +.codicon-no-newline:before { content: "\eb25" } +.codicon-note:before { content: "\eb26" } +.codicon-octoface:before { content: "\eb27" } +.codicon-open-preview:before { content: "\eb28" } +.codicon-package:before { content: "\eb29" } +.codicon-paintcan:before { content: "\eb2a" } +.codicon-pin:before { content: "\eb2b" } +.codicon-play:before { content: "\eb2c" } +.codicon-run:before { content: "\eb2c" } +.codicon-plug:before { content: "\eb2d" } +.codicon-preserve-case:before { content: "\eb2e" } +.codicon-preview:before { content: "\eb2f" } +.codicon-project:before { content: "\eb30" } +.codicon-pulse:before { content: "\eb31" } +.codicon-question:before { content: "\eb32" } +.codicon-quote:before { content: "\eb33" } +.codicon-radio-tower:before { content: "\eb34" } +.codicon-reactions:before { content: "\eb35" } +.codicon-references:before { content: "\eb36" } +.codicon-refresh:before { content: "\eb37" } +.codicon-regex:before { content: "\eb38" } +.codicon-remote-explorer:before { content: "\eb39" } +.codicon-remote:before { content: "\eb3a" } +.codicon-remove:before { content: "\eb3b" } +.codicon-replace-all:before { content: "\eb3c" } +.codicon-replace:before { content: "\eb3d" } +.codicon-repo-clone:before { content: "\eb3e" } +.codicon-repo-force-push:before { content: "\eb3f" } +.codicon-repo-pull:before { content: "\eb40" } +.codicon-repo-push:before { content: "\eb41" } +.codicon-report:before { content: "\eb42" } +.codicon-request-changes:before { content: "\eb43" } +.codicon-rocket:before { content: "\eb44" } +.codicon-root-folder-opened:before { content: "\eb45" } +.codicon-root-folder:before { content: "\eb46" } +.codicon-rss:before { content: "\eb47" } +.codicon-ruby:before { content: "\eb48" } +.codicon-save-all:before { content: "\eb49" } +.codicon-save-as:before { content: "\eb4a" } +.codicon-save:before { content: "\eb4b" } +.codicon-screen-full:before { content: "\eb4c" } +.codicon-screen-normal:before { content: "\eb4d" } +.codicon-search-stop:before { content: "\eb4e" } +.codicon-server:before { content: "\eb50" } +.codicon-settings-gear:before { content: "\eb51" } +.codicon-settings:before { content: "\eb52" } +.codicon-shield:before { content: "\eb53" } +.codicon-smiley:before { content: "\eb54" } +.codicon-sort-precedence:before { content: "\eb55" } +.codicon-split-horizontal:before { content: "\eb56" } +.codicon-split-vertical:before { content: "\eb57" } +.codicon-squirrel:before { content: "\eb58" } +.codicon-star-full:before { content: "\eb59" } +.codicon-star-half:before { content: "\eb5a" } +.codicon-symbol-class:before { content: "\eb5b" } +.codicon-symbol-color:before { content: "\eb5c" } +.codicon-symbol-constant:before { content: "\eb5d" } +.codicon-symbol-enum-member:before { content: "\eb5e" } +.codicon-symbol-field:before { content: "\eb5f" } +.codicon-symbol-file:before { content: "\eb60" } +.codicon-symbol-interface:before { content: "\eb61" } +.codicon-symbol-keyword:before { content: "\eb62" } +.codicon-symbol-misc:before { content: "\eb63" } +.codicon-symbol-operator:before { content: "\eb64" } +.codicon-symbol-property:before { content: "\eb65" } +.codicon-wrench:before { content: "\eb65" } +.codicon-wrench-subaction:before { content: "\eb65" } +.codicon-symbol-snippet:before { content: "\eb66" } +.codicon-tasklist:before { content: "\eb67" } +.codicon-telescope:before { content: "\eb68" } +.codicon-text-size:before { content: "\eb69" } +.codicon-three-bars:before { content: "\eb6a" } +.codicon-thumbsdown:before { content: "\eb6b" } +.codicon-thumbsup:before { content: "\eb6c" } +.codicon-tools:before { content: "\eb6d" } +.codicon-triangle-down:before { content: "\eb6e" } +.codicon-triangle-left:before { content: "\eb6f" } +.codicon-triangle-right:before { content: "\eb70" } +.codicon-triangle-up:before { content: "\eb71" } +.codicon-twitter:before { content: "\eb72" } +.codicon-unfold:before { content: "\eb73" } +.codicon-unlock:before { content: "\eb74" } +.codicon-unmute:before { content: "\eb75" } +.codicon-unverified:before { content: "\eb76" } +.codicon-verified:before { content: "\eb77" } +.codicon-versions:before { content: "\eb78" } +.codicon-vm-active:before { content: "\eb79" } +.codicon-vm-outline:before { content: "\eb7a" } +.codicon-vm-running:before { content: "\eb7b" } +.codicon-watch:before { content: "\eb7c" } +.codicon-whitespace:before { content: "\eb7d" } +.codicon-whole-word:before { content: "\eb7e" } +.codicon-window:before { content: "\eb7f" } +.codicon-word-wrap:before { content: "\eb80" } +.codicon-zoom-in:before { content: "\eb81" } +.codicon-zoom-out:before { content: "\eb82" } +.codicon-list-filter:before { content: "\eb83" } +.codicon-list-flat:before { content: "\eb84" } +.codicon-list-selection:before { content: "\eb85" } +.codicon-selection:before { content: "\eb85" } +.codicon-list-tree:before { content: "\eb86" } +.codicon-debug-breakpoint-function-unverified:before { content: "\eb87" } +.codicon-debug-breakpoint-function:before { content: "\eb88" } +.codicon-debug-breakpoint-function-disabled:before { content: "\eb88" } +.codicon-debug-stackframe-active:before { content: "\eb89" } +.codicon-debug-stackframe-dot:before { content: "\eb8a" } +.codicon-debug-stackframe:before { content: "\eb8b" } +.codicon-debug-stackframe-focused:before { content: "\eb8b" } +.codicon-debug-breakpoint-unsupported:before { content: "\eb8c" } +.codicon-symbol-string:before { content: "\eb8d" } +.codicon-debug-reverse-continue:before { content: "\eb8e" } +.codicon-debug-step-back:before { content: "\eb8f" } +.codicon-debug-restart-frame:before { content: "\eb90" } +.codicon-debug-alt:before { content: "\eb91" } +.codicon-call-incoming:before { content: "\eb92" } +.codicon-call-outgoing:before { content: "\eb93" } +.codicon-menu:before { content: "\eb94" } +.codicon-expand-all:before { content: "\eb95" } +.codicon-feedback:before { content: "\eb96" } +.codicon-group-by-ref-type:before { content: "\eb97" } +.codicon-ungroup-by-ref-type:before { content: "\eb98" } +.codicon-account:before { content: "\eb99" } +.codicon-bell-dot:before { content: "\eb9a" } +.codicon-debug-console:before { content: "\eb9b" } +.codicon-library:before { content: "\eb9c" } +.codicon-output:before { content: "\eb9d" } +.codicon-run-all:before { content: "\eb9e" } +.codicon-sync-ignored:before { content: "\eb9f" } +.codicon-pinned:before { content: "\eba0" } +.codicon-github-inverted:before { content: "\eba1" } +.codicon-server-process:before { content: "\eba2" } +.codicon-server-environment:before { content: "\eba3" } +.codicon-pass:before { content: "\eba4" } +.codicon-issue-closed:before { content: "\eba4" } +.codicon-stop-circle:before { content: "\eba5" } +.codicon-play-circle:before { content: "\eba6" } +.codicon-record:before { content: "\eba7" } +.codicon-debug-alt-small:before { content: "\eba8" } +.codicon-vm-connect:before { content: "\eba9" } +.codicon-cloud:before { content: "\ebaa" } +.codicon-merge:before { content: "\ebab" } +.codicon-export:before { content: "\ebac" } +.codicon-graph-left:before { content: "\ebad" } +.codicon-magnet:before { content: "\ebae" } +.codicon-notebook:before { content: "\ebaf" } +.codicon-redo:before { content: "\ebb0" } +.codicon-check-all:before { content: "\ebb1" } +.codicon-pinned-dirty:before { content: "\ebb2" } +.codicon-pass-filled:before { content: "\ebb3" } +.codicon-circle-large-filled:before { content: "\ebb4" } +.codicon-circle-large-outline:before { content: "\ebb5" } +.codicon-combine:before { content: "\ebb6" } +.codicon-gather:before { content: "\ebb6" } +.codicon-table:before { content: "\ebb7" } +.codicon-variable-group:before { content: "\ebb8" } +.codicon-type-hierarchy:before { content: "\ebb9" } +.codicon-type-hierarchy-sub:before { content: "\ebba" } +.codicon-type-hierarchy-super:before { content: "\ebbb" } +.codicon-git-pull-request-create:before { content: "\ebbc" } +.codicon-run-above:before { content: "\ebbd" } +.codicon-run-below:before { content: "\ebbe" } +.codicon-notebook-template:before { content: "\ebbf" } +.codicon-debug-rerun:before { content: "\ebc0" } +.codicon-workspace-trusted:before { content: "\ebc1" } +.codicon-workspace-untrusted:before { content: "\ebc2" } +.codicon-workspace-unknown:before { content: "\ebc3" } +.codicon-terminal-cmd:before { content: "\ebc4" } +.codicon-terminal-debian:before { content: "\ebc5" } +.codicon-terminal-linux:before { content: "\ebc6" } +.codicon-terminal-powershell:before { content: "\ebc7" } +.codicon-terminal-tmux:before { content: "\ebc8" } +.codicon-terminal-ubuntu:before { content: "\ebc9" } +.codicon-terminal-bash:before { content: "\ebca" } +.codicon-arrow-swap:before { content: "\ebcb" } +.codicon-copy:before { content: "\ebcc" } +.codicon-person-add:before { content: "\ebcd" } +.codicon-filter-filled:before { content: "\ebce" } +.codicon-wand:before { content: "\ebcf" } +.codicon-debug-line-by-line:before { content: "\ebd0" } +.codicon-inspect:before { content: "\ebd1" } +.codicon-layers:before { content: "\ebd2" } +.codicon-layers-dot:before { content: "\ebd3" } +.codicon-layers-active:before { content: "\ebd4" } +.codicon-compass:before { content: "\ebd5" } +.codicon-compass-dot:before { content: "\ebd6" } +.codicon-compass-active:before { content: "\ebd7" } +.codicon-azure:before { content: "\ebd8" } +.codicon-issue-draft:before { content: "\ebd9" } +.codicon-git-pull-request-closed:before { content: "\ebda" } +.codicon-git-pull-request-draft:before { content: "\ebdb" } +.codicon-debug-all:before { content: "\ebdc" } +.codicon-debug-coverage:before { content: "\ebdd" } +.codicon-run-errors:before { content: "\ebde" } +.codicon-folder-library:before { content: "\ebdf" } +.codicon-debug-continue-small:before { content: "\ebe0" } +.codicon-beaker-stop:before { content: "\ebe1" } +.codicon-graph-line:before { content: "\ebe2" } +.codicon-graph-scatter:before { content: "\ebe3" } +.codicon-pie-chart:before { content: "\ebe4" } +.codicon-bracket:before { content: "\eb0f" } +.codicon-bracket-dot:before { content: "\ebe5" } +.codicon-bracket-error:before { content: "\ebe6" } +.codicon-lock-small:before { content: "\ebe7" } +.codicon-azure-devops:before { content: "\ebe8" } +.codicon-verified-filled:before { content: "\ebe9" } +.codicon-newline:before { content: "\ebea" } +.codicon-layout:before { content: "\ebeb" } +.codicon-layout-activitybar-left:before { content: "\ebec" } +.codicon-layout-activitybar-right:before { content: "\ebed" } +.codicon-layout-panel-left:before { content: "\ebee" } +.codicon-layout-panel-center:before { content: "\ebef" } +.codicon-layout-panel-justify:before { content: "\ebf0" } +.codicon-layout-panel-right:before { content: "\ebf1" } +.codicon-layout-panel:before { content: "\ebf2" } +.codicon-layout-sidebar-left:before { content: "\ebf3" } +.codicon-layout-sidebar-right:before { content: "\ebf4" } +.codicon-layout-statusbar:before { content: "\ebf5" } +.codicon-layout-menubar:before { content: "\ebf6" } +.codicon-layout-centered:before { content: "\ebf7" } +.codicon-target:before { content: "\ebf8" } +.codicon-indent:before { content: "\ebf9" } +.codicon-record-small:before { content: "\ebfa" } +.codicon-error-small:before { content: "\ebfb" } +.codicon-arrow-circle-down:before { content: "\ebfc" } +.codicon-arrow-circle-left:before { content: "\ebfd" } +.codicon-arrow-circle-right:before { content: "\ebfe" } +.codicon-arrow-circle-up:before { content: "\ebff" } diff --git a/projects/cloud-foundry-tools/packages/frontend/codicon.ttf b/projects/cloud-foundry-tools/packages/frontend/codicon.ttf new file mode 100644 index 000000000..2f5dbfcc7 Binary files /dev/null and b/projects/cloud-foundry-tools/packages/frontend/codicon.ttf differ diff --git a/projects/cloud-foundry-tools/packages/frontend/index.html b/projects/cloud-foundry-tools/packages/frontend/index.html new file mode 100644 index 000000000..db52fe922 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/index.html @@ -0,0 +1,18 @@ + + + + + + + + + Cloud Foundry Sign-in + + + +
+ + + diff --git a/projects/cloud-foundry-tools/packages/frontend/jest.config.js b/projects/cloud-foundry-tools/packages/frontend/jest.config.js new file mode 100644 index 000000000..17bf31310 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/jest.config.js @@ -0,0 +1,40 @@ +module.exports = { + verbose: true, + testRegex: "(/test/(.*).(test|spec)).[jt]sx?$", + collectCoverage: true, + coverageProvider: "v8", + collectCoverageFrom: [ + "src/**/*.{js,vue}", + "!**/node_modules/**", + "!/src/main.js", + "!/src/directives/**", + "!/src/styles/**", + ], + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + }, + moduleFileExtensions: ["js", "vue", "json"], + transformIgnorePatterns: [ + "/node_modules/.pnpm/(?!(@vscode-elements\\+|@lit\\+|@lit-labs\\+|lit@|lit-html@|lit-element@|lodash@))", + ], + modulePaths: ["/src", "/node_modules"], + transform: { + "^.+\\.vue$": "@vue/vue3-jest", + "^.+\\.tsx?$": "ts-jest", + "^.+\\.js$": "babel-jest", + "^.+\\.jsx?$": "babel-jest", + }, + setupFilesAfterEnv: ["/jest.setup.js"], + testEnvironmentOptions: { + customExportConditions: ["node", "node-addons"], + }, + testEnvironment: "jsdom", + coverageThreshold: { + global: { + branches: 85, + functions: 80, + lines: 87, + statements: 87, + }, + }, +}; diff --git a/projects/cloud-foundry-tools/packages/frontend/jest.setup.js b/projects/cloud-foundry-tools/packages/frontend/jest.setup.js new file mode 100644 index 000000000..d3433d9a2 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/jest.setup.js @@ -0,0 +1,12 @@ +import "@testing-library/jest-dom"; // If you're using Vue Test Utils with Vue 3 + +// Mock window.matchMedia +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: query === "(min-width: 768px)", // Adjust as needed + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); diff --git a/projects/cloud-foundry-tools/packages/frontend/package.json b/projects/cloud-foundry-tools/packages/frontend/package.json new file mode 100644 index 000000000..2c1fc68c5 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/package.json @@ -0,0 +1,68 @@ +{ + "name": "cloud-foundry-tools-frontend", + "displayName": "Cloud Foundry Tools Sign Frontend", + "version": "8.1.2", + "private": true, + "description": "Frontend for the cf sign-in", + "license": "Apache 2.0", + "publisher": "SAP", + "scripts": { + "build": "vite build", + "build-dev": "vite build --mode development", + "ci": "npm-run-all clean build test", + "ci:artifacts_only": "npm-run-all clean build", + "clean": "rimraf ./dist ./coverage", + "dev": "vite", + "format:fix": "prettier --print-width 120 --write \"**/*.@(js|ts|json|md|vue)\" --ignore-path=.gitignore", + "format:validate": "prettier --print-width 120 --check \"**/*.@(js|ts|json|md|vue)\" --ignore-path=.gitignore", + "serve": "vite preview", + "test": "jest -w 1 --colors" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ], + "eslintConfig": { + "env": { + "es2022": true, + "jest": true, + "node": true + }, + "extends": [ + "plugin:vue/vue3-recommended", + "eslint:recommended", + "prettier" + ], + "rules": { + "vue/attribute-hyphenation": "off" + }, + "root": true + }, + "dependencies": { + "@sap-devx/webview-rpc": "0.4.1", + "@vscode-elements/elements": "1.16.1", + "lodash": "4.17.21", + "vue": "^3.3.4", + "vue-loader": "17.2.2", + "vue-loading-overlay": "6.0.3" + }, + "devDependencies": { + "@babel/preset-env": "^7.22.20", + "@babel/preset-typescript": "^7.23.0", + "@testing-library/jest-dom": "^6.6.3", + "@vitejs/plugin-vue": "^5.2.0", + "@vue/compiler-sfc": "3.3.8", + "@vue/test-utils": "2.4.1", + "@vue/vue3-jest": "29.2.6", + "eslint-plugin-vue": "9.18.1", + "jest": "29.7.0", + "jest-environment-jsdom": "^29.7.0", + "ts-jest": "29.1.1", + "vite": "^6.0.6" + }, + "repository": { + "type": "git", + "url": "https://github.com/SAP/app-studio-toolkit.git", + "directory": "projects/cloud-foundry-tools/packages/frontend" + } +} diff --git a/projects/cloud-foundry-tools/packages/frontend/src/App.vue b/projects/cloud-foundry-tools/packages/frontend/src/App.vue new file mode 100644 index 000000000..c44ff2386 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/src/App.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/projects/cloud-foundry-tools/packages/frontend/src/components/CFHeader.vue b/projects/cloud-foundry-tools/packages/frontend/src/components/CFHeader.vue new file mode 100644 index 000000000..38ef5a6d7 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/src/components/CFHeader.vue @@ -0,0 +1,26 @@ + + + + diff --git a/projects/cloud-foundry-tools/packages/frontend/src/components/CFSignin.vue b/projects/cloud-foundry-tools/packages/frontend/src/components/CFSignin.vue new file mode 100644 index 000000000..6fe4bceb0 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/src/components/CFSignin.vue @@ -0,0 +1,416 @@ + + + + + + diff --git a/projects/cloud-foundry-tools/packages/frontend/src/components/CFTarget.vue b/projects/cloud-foundry-tools/packages/frontend/src/components/CFTarget.vue new file mode 100644 index 000000000..e0a63cc87 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/src/components/CFTarget.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/projects/cloud-foundry-tools/packages/frontend/src/directives/tooltip.js b/projects/cloud-foundry-tools/packages/frontend/src/directives/tooltip.js new file mode 100644 index 000000000..cd93b144e --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/src/directives/tooltip.js @@ -0,0 +1,159 @@ +export default { + updateTooltip(el, { value, modifiers }) { + if (typeof value === "string") { + // we can pass either a string + el.setAttribute("data-v-tooltip", value); + + // this check if when v-tooltip receives string with .arrow modifier + if (modifiers.arrow) { + el.style.setProperty("--v-tooltip-arrow-display", "inline"); + } + } else { + // or an object + if (value.text) { + el.setAttribute("data-v-tooltip", value.text); + } + if (value.displayArrow || modifiers.arrow) { + // if there is a prop global: true then mutate the :root css variables + // otherwise it adds given variables to the element, which makes it possible to be different than others + const targetEl = value.global ? document.documentElement : el; + targetEl.style.setProperty("--v-tooltip-arrow-display", "inline"); + } + if (value.theme) { + // if there is a prop global: true then mutate the :root css variables + // otherwise it adds given variables to the element, which makes it possible to be different than others + const targetEl = value.global ? document.documentElement : el; + for (const [key, val] of Object.entries(value.theme)) { + if (key === "placement") { + switch (val) { + case "top": + targetEl.style.setProperty("--v-tooltip-left", "50%"); + targetEl.style.setProperty("--v-tooltip-top", "0%"); + targetEl.style.setProperty( + "--v-tooltip-translate", + "translate(-50%, -110%)" + ); + if (value.displayArrow || modifiers.arrow) { + targetEl.style.setProperty( + "--v-tooltip-arrow-border-color", + "var(--v-tooltip-background-color) transparent transparent transparent" + ); + targetEl.style.setProperty( + "--v-tooltip-arrow-top", + "calc(var(--v-tooltip-top) - var(--v-tooltip-top-offset) + 8px)" + ); + } + break; + case "bottom": + targetEl.style.setProperty("--v-tooltip-left", "50%"); + targetEl.style.setProperty("--v-tooltip-top", "100%"); + targetEl.style.setProperty( + "--v-tooltip-translate", + "translate(-50%, 10%)" + ); + if (value.displayArrow || modifiers.arrow) { + targetEl.style.setProperty( + "--v-tooltip-arrow-border-color", + "transparent transparent var(--v-tooltip-background-color) transparent" + ); + targetEl.style.setProperty( + "--v-tooltip-arrow-top", + "calc(var(--v-tooltip-top) - var(--v-tooltip-top-offset) - 7px)" + ); + } + break; + case "left": + targetEl.style.setProperty("--v-tooltip-left", "0%"); + targetEl.style.setProperty("--v-tooltip-top", "50%"); + targetEl.style.setProperty( + "--v-tooltip-translate", + "translate(-110%, -50%)" + ); + if (value.displayArrow || modifiers.arrow) { + targetEl.style.setProperty( + "--v-tooltip-arrow-border-color", + "transparent transparent transparent var(--v-tooltip-background-color)" + ); + targetEl.style.setProperty( + "--v-tooltip-arrow-top", + "calc(var(--v-tooltip-top)" + ); + targetEl.style.setProperty( + "--v-tooltip-arrow-left", + "calc( var(--v-tooltip-left) - var(--v-tooltip-left-offset) + 1.5px)" + ); + } + break; + case "right": + targetEl.style.setProperty("--v-tooltip-left", "100%"); + targetEl.style.setProperty("--v-tooltip-top", "50%"); + targetEl.style.setProperty( + "--v-tooltip-translate", + "translate(3%, -50%)" + ); + if (value.displayArrow || modifiers.arrow) { + targetEl.style.setProperty( + "--v-tooltip-arrow-border-color", + "transparent var(--v-tooltip-background-color) transparent transparent" + ); + targetEl.style.setProperty( + "--v-tooltip-arrow-top", + "calc(var(--v-tooltip-top)" + ); + targetEl.style.setProperty( + "--v-tooltip-arrow-left", + "calc( var(--v-tooltip-left) - var(--v-tooltip-left-offset) - 2px)" + ); + } + break; + default: + break; + } + } else if (key === "offset" && !value.global) { + for (const direction of val) { + if (direction === "left") { + targetEl.style.setProperty( + "--v-tooltip-left-offset", + `-${targetEl.scrollWidth - targetEl.clientWidth}px` + ); + } else if (direction === "right") { + targetEl.style.setProperty( + "--v-tooltip-left-offset", + `${targetEl.scrollWidth - targetEl.clientWidth}px` + ); + } else if (direction === "top") { + targetEl.style.setProperty( + "--v-tooltip-top-offset", + `-${targetEl.scrollHeight - targetEl.clientHeight}px` + ); + } else if (direction === "bottom") { + targetEl.style.setProperty( + "--v-tooltip-top-offset", + `${targetEl.scrollHeight - targetEl.clientHeight}px` + ); + } + } + } else { + targetEl.style.setProperty(`--v-tooltip-${key}`, val); + } + } + } + } + }, + // hooks + mounted(el, { value, dir, modifiers }) { + // v-tooltips with global prop won't show the tooltip + // also object notation without text prop won't show neither + if (typeof value === "object" && !value.global && value.text) { + el.classList.add("data-v-tooltip"); + } else if (typeof value === "string") { + el.classList.add("data-v-tooltip"); + } + + // to use functions in Vue's directives which are inside this object, we can't use this, we have to use dir, which is the directive object + dir.updateTooltip(el, { value, modifiers }); + }, + updated(el, { value, dir, modifiers }) { + dir.updateTooltip(el, { value, modifiers }); + }, +}; diff --git a/projects/cloud-foundry-tools/packages/frontend/src/main.js b/projects/cloud-foundry-tools/packages/frontend/src/main.js new file mode 100644 index 000000000..597549a43 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/src/main.js @@ -0,0 +1,11 @@ +import { createApp } from "vue"; +import CFSignIn from "./App.vue"; +import "./styles/tooltip.css"; +import tooltip from "./directives/tooltip.js"; + +const app = createApp(CFSignIn); + +// register all directives +app.directive("tooltip", tooltip); + +app.mount("#app"); diff --git a/projects/cloud-foundry-tools/packages/frontend/src/styles/tooltip.css b/projects/cloud-foundry-tools/packages/frontend/src/styles/tooltip.css new file mode 100644 index 000000000..aefb93248 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/src/styles/tooltip.css @@ -0,0 +1,83 @@ +:root { + --v-tooltip-left: 50%; + --v-tooltip-top: 0%; + --v-tooltip-translate: translate(-50%, -110%); + --v-tooltip-left-offset: 0px; + --v-tooltip-top-offset: 0px; + --v-tooltip-width: max-content; + --v-tooltip-background-color: var(--vscode-editor-background, white); + --v-tooltip-color: var(--vscode-foreground, black); + --v-tooltip-border-radius: 0.4rem; + --v-tooltip-padding: 0.6em; + --v-tooltip-font-size: 0.7rem; + --v-tooltip-font-family: var(--vscode-font-family, Roboto, Avenir, Helvetica, Arial, sans-serif); + --v-tooltip-border: 0.1rem solid var(--v-tooltip-color); + --v-tooltip-box-shadow: none; + --v-tooltip-transition-duration: 0.25s; + --v-tooltip-transition-delay: 0.3s; + --v-tooltip-transition-timing-function: ease; + --v-tooltip-arrow-display: none; + --v-tooltip-arrow-border-color: var(--v-tooltip-background-color) + transparent transparent transparent; + --v-tooltip-arrow-top: calc( + var(--v-tooltip-top) - var(--v-tooltip-top-offset) + 8px + ); + --v-tooltip-arrow-left: calc( + var(--v-tooltip-left) - var(--v-tooltip-left-offset) + ); +} +.data-v-tooltip { + position: relative; +} +.data-v-tooltip::after { + z-index: 2147483647; + content: attr(data-v-tooltip); + position: absolute; + left: calc(var(--v-tooltip-left) - var(--v-tooltip-left-offset)); + top: calc(var(--v-tooltip-top) - var(--v-tooltip-top-offset)); + transform: var(--v-tooltip-translate); + width: var(--v-tooltip-width); + background-color: var(--v-tooltip-background-color); + color: var(--v-tooltip-color); + border-radius: var(--v-tooltip-border-radius); + padding: var(--v-tooltip-padding); + font-size: var(--v-tooltip-font-size); + font-family: var(--v-tooltip-font-family); + border: var(--v-tooltip-border); + box-shadow: var(--v-tooltip-box-shadow); + transition-property: all; + transition-duration: var(--v-tooltip-transition-duration); + transition-delay: 0s; + transition-timing-function: var(--v-tooltip-transition-timing-function); + opacity: 0; + visibility: hidden; + pointer-events: none; +} +/* arrow */ +.data-v-tooltip::before { + z-index: 2147483647; + content: ""; + position: absolute; + display: var(--v-tooltip-arrow-display); + left: var(--v-tooltip-arrow-left); + top: var(--v-tooltip-arrow-top); + transform: var(--v-tooltip-translate); + border-width: 5px; + border-style: solid; + border-color: var(--v-tooltip-arrow-border-color); + transition-property: all; + transition-duration: var(--v-tooltip-transition-duration); + transition-delay: 0s; + transition-timing-function: var(--v-tooltip-transition-timing-function); + opacity: 0; + visibility: hidden; + pointer-events: none; +} +.data-v-tooltip:hover::after, +.data-v-tooltip:hover::before { + transition-duration: var(--v-tooltip-transition-duration); + transition-delay: var(--v-tooltip-transition-delay); + transition-timing-function: var(--v-tooltip-transition-timing-function); + opacity: 1; + visibility: visible; +} \ No newline at end of file diff --git a/projects/cloud-foundry-tools/packages/frontend/test/App.spec.js b/projects/cloud-foundry-tools/packages/frontend/test/App.spec.js new file mode 100644 index 000000000..27bdb8818 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/test/App.spec.js @@ -0,0 +1,184 @@ +import { expect } from "chai"; +import { shallowMount } from "@vue/test-utils"; +import App from "../src/App"; + +jest.mock("@sap-devx/webview-rpc/out.browser/rpc-browser", () => ({ + RpcBrowser: jest.fn().mockImplementation(() => ({ + invoke: jest.fn(), + registerMethod: jest.fn(), + })), +})); + +// Mock RpcBrowserWebSockets class +jest.mock("@sap-devx/webview-rpc/out.browser/rpc-browser-ws", () => { + class MockRpcBrowserWebSockets { + constructor(ws, logger) { + this.ws = ws; + this.logger = logger; + this.eventListeners = {}; + + // Simulate the behavior of the real constructor + this.ws.addEventListener = (eventName, callback) => { + this.eventListeners[eventName] = callback; + }; + + // Initialize an empty invoke method + this.invoke = jest.fn(); + } + + registerMethod() {} + } + + return { + RpcBrowserWebSockets: MockRpcBrowserWebSockets, + }; +}); + +jest.mock("../src/components/CFHeader.vue", () => ({ + name: "CFHeader", + template: "
Mocked CFHeader
", // Replace with your desired mock template +})); + +// Mock the CFSignin.vue module +jest.mock("../src/components/CFSignin.vue", () => ({ + name: "CFSignin", + template: "
Mocked CFSignin
", // Replace with your desired mock template +})); + +// Stub is used to avoid console warnings of vscode custom elements +const global = { + stubs: { + VscodeProgressRing: { + template: "
", + }, + }, +}; + +describe("App.vue", () => { + // Mock the rpc object with the required methods + const mockRpc = { + invoke: jest.fn(() => Promise.resolve({})), + registerMethod: jest.fn(), + }; + + it("renders without errors", () => { + // Mount the component with the mocked rpc object and target prop + const wrapper = shallowMount(App, { + data() { + return { + rpc: mockRpc, // Provide the mock rpc object + initialTarget: {}, + }; + }, + global, + }); + + // Assert that the component exists without errors + expect(wrapper.exists()).to.be.true; + }); + + it("updates isLoggedIn when updateIsLoggedIn is called", () => { + const wrapper = shallowMount(App, { + data() { + return { + rpc: mockRpc, // Provide the mock rpc object + initialTarget: {}, + }; + }, + global, + }); + expect(wrapper.vm.isLoggedIn).to.be.false; // Initial state + + // Call the updateIsLoggedIn method with a value of true + wrapper.vm.updateIsLoggedIn(true); + + // Check that isLoggedIn has been updated to true + expect(wrapper.vm.isLoggedIn).to.be.true; + }); + + it("updates orgAndSpaceSet when updateOrgAndSpaceSet is called", () => { + const wrapper = shallowMount(App, { + data() { + return { + rpc: mockRpc, // Provide the mock rpc object + initialTarget: {}, + }; + }, + global, + }); + expect(wrapper.vm.orgAndSpaceSet).to.be.false; // Initial state + + // Call the updateOrgAndSpaceSet method with a value of true + wrapper.vm.updateOrgAndSpaceSet(true); + + // Check that isLoggedIn has been updated to true + expect(wrapper.vm.orgAndSpaceSet).to.be.true; + }); + + it("renders the correct HTML structure", () => { + const wrapper = shallowMount(App, { + data() { + return { + rpc: mockRpc, // Provide the mock rpc object + initialTarget: {}, + }; + }, + global, + }); + const expectedHtml = ``; + expect(wrapper.html().replace(/\s/g, "")).to.equal(expectedHtml); + }); + + it("computes isLoadingColor as true", () => { + const wrapper = shallowMount(App, { + data() { + return { + rpc: mockRpc, // Provide the mock rpc object + initialTarget: {}, + }; + }, + global, + }); + + // Check that the computed property returns true + expect(wrapper.vm.isLoadingColor).to.be.true; + }); + + it("sets showBusyIndicator to the specified value", () => { + const wrapper = shallowMount(App, { + data() { + return { + rpc: mockRpc, // Provide the mock rpc object + showBusyIndicator: false, // Initialize with a specific value + initialTarget: {}, + }; + }, + global, + }); + + // Call the setBusyIndicator method with a value of true + wrapper.vm.setBusyIndicator(true); + + // Check that the showBusyIndicator data property is now true + expect(wrapper.vm.showBusyIndicator).to.be.true; + }); + + it("sets showBusyIndicator to the specified value (false)", () => { + const wrapper = shallowMount(App, { + data() { + return { + rpc: mockRpc, // Provide the mock rpc object + showBusyIndicator: true, // Initialize with a specific value + initialTarget: {}, + }; + }, + global, + }); + + // Call the setBusyIndicator method with a value of false + wrapper.vm.setBusyIndicator(false); + + // Check that the showBusyIndicator data property is now false + expect(wrapper.vm.showBusyIndicator).to.be.false; + }); +}); diff --git a/projects/cloud-foundry-tools/packages/frontend/test/components/CFHeader.spec.js b/projects/cloud-foundry-tools/packages/frontend/test/components/CFHeader.spec.js new file mode 100644 index 000000000..1efd54f89 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/test/components/CFHeader.spec.js @@ -0,0 +1,59 @@ +import CFHeader from "../../src/components/CFHeader.vue"; +import { expect } from "chai"; +import { mount } from "@vue/test-utils"; + +jest.spyOn(window, "getComputedStyle").mockImplementation(() => ({ + getPropertyValue: (property) => { + if (property === "--vscode-descriptionForeground") { + return "#717171"; // Mock the CSS variable value + } + return ""; + }, +})); + +const global = { + stubs: { + VscodeDivider: { + template: "
", + }, + }, +}; + +describe("CFHeader.vue", () => { + it("renders without errors", () => { + const wrapper = mount(CFHeader, { + global, + }); + expect(wrapper.exists()).to.be.true; + }); + + it("renders the component with correct content", () => { + const wrapper = mount(CFHeader, { global }); + expect(wrapper.text()).contain("Cloud Foundry Sign In and Targets"); + expect(wrapper.text()).contain( + "Provide your credentials to sign in to the Cloud Foundry environment and then select the target org and space." + ); + expect(wrapper.find('[role="separator"]').exists()).to.be.true; + }); + + it("applies the correct subtitle-field style", () => { + const wrapper = mount(CFHeader, { global }); + + // Access the element with class "subtitle-field" + const subtitleField = wrapper.find(".subtitle-field").element; + + // Get the computed style of the element + const computedStyle = window.getComputedStyle(subtitleField); + + // Assert the computed style properties + expect( + computedStyle.getPropertyValue("--vscode-descriptionForeground") + ).to.equal("#717171"); + }); + + it("renders the correct HTML structure", () => { + const wrapper = mount(CFHeader, { global }); + const expectedHtml = `
CloudFoundrySignInandTargetsProvideyourcredentialstosignintotheCloudFoundryenvironmentandthenselectthetargetorgandspace.


`; + expect(wrapper.html().replace(/\s/g, "")).to.equal(expectedHtml); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/frontend/test/components/CFSignin.spec.js b/projects/cloud-foundry-tools/packages/frontend/test/components/CFSignin.spec.js new file mode 100644 index 000000000..e36aabd19 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/test/components/CFSignin.spec.js @@ -0,0 +1,428 @@ +import { expect } from "chai"; +import { shallowMount } from "@vue/test-utils"; +import CFSignin from "../../src/components/CFSignin.vue"; + +const mockTooltipDirective = jest.fn(); + +const global = { + directives: { + // Use the mockTooltipDirective for v-tooltip + tooltip: mockTooltipDirective, + }, + stubs: { + VscodeLink: { + template: "
", + }, + VscodeRadioGroup: { + template: "
", + }, + VscodeRadio: { + template: "", + }, + VscodeTextfield: { + template: "
", + }, + VscodeButton: { + template: "", + }, + VscodeIcon: { + template: "", + }, + }, +}; + +describe("CFSignin.vue", () => { + it("renders without errors", () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: {}, + rpc: {}, + orgAndSpaceSet: false, + }, + }); + expect(wrapper.exists()).to.be.true; + }); + + it("initializes with default data values", () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "https://example.com", + isLoggedIn: false, + }, + rpc: {}, + orgAndSpaceSet: false, + }, + }); + + // Check initial data values + expect(wrapper.vm.endpoint).to.equal("https://example.com"); + expect(wrapper.vm.passcode).to.equal(""); + expect(wrapper.vm.username).to.equal(""); + expect(wrapper.vm.password).to.equal(""); + expect(wrapper.vm.isCFEndpointValid).to.be.true; + expect(wrapper.vm.disableButton).to.be.true; + }); + + it("disables the Sign in button when no input is provided", async () => { + //avoid showing warning for custom element in console log. + jest.spyOn(console, "warn").mockImplementation(() => { + return; + }); + + const wrapper = shallowMount(CFSignin, { + // Do not mock button + global: { + directives: { + // Use the mockTooltipDirective for v-tooltip + tooltip: mockTooltipDirective, + }, + stubs: { + VscodeLink: { + template: "
", + }, + VscodeRadioGroup: { + template: "
", + }, + VscodeRadio: { + template: "", + }, + VscodeTextField: { + template: "
", + }, + }, + }, + props: { + target: { + defaultEndpoint: "", + isLoggedIn: false, + }, + rpc: {}, + orgAndSpaceSet: false, + }, + }); + + // Confirm that the Sign in button is initially not visible + expect(wrapper.find("vscode-button").isVisible()).to.be.false; + }); + + it("handles successful sign-in with valid credentials", async () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "https://example.com", + isLoggedIn: false, + }, + rpc: { + invoke: jest.fn().mockResolvedValue(true), // Mock RPC to return success + }, + orgAndSpaceSet: false, + }, + }); + // Set endPoint to be the default one + wrapper.vm.setEndpoint({ target: { value: "" } }); + + // Set SSO target to be Credentials + wrapper.vm.setSSO({ target: { value: "Credentials" } }); + + // Set valid username and password or passcode + wrapper.setData({ username: "validUsername", password: "validPassword" }); + + const invokeSpy = jest.spyOn(wrapper.props().rpc, "invoke"); + + // Trigger the SigninClicked method + await wrapper.vm.SigninClicked(); + + // Assert that authentication failed message is not displayed and isLoggedIn is set to true + expect(wrapper.vm.authFailed).to.equal(""); + expect(wrapper.vm.isLoggedIn).to.be.true; + // Check that loginClick was called with correct params + expect(invokeSpy.mock.calls[0][0]).to.be.equals("loginClick"); + expect(invokeSpy.mock.calls[0][1][0]).to.deep.equal({ + endpoint: "https://example.com", + user: "validUsername", + password: "validPassword", + }); + }); + + it("handles successful sign-in with valid SSO", async () => { + const endpoint = "https://example.com"; + const passcode = "-_validPasscode"; + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: endpoint, + isLoggedIn: false, + }, + rpc: { + invoke: async (command, args) => { + expect(command).to.equal("loginClick"); + expect(args[0]).to.deep.equal({ + endpoint: endpoint, + ssoPasscode: `"${passcode}"`, + }); + return true; + }, + }, + orgAndSpaceSet: false, + }, + }); + + // Set SSO authentication method + wrapper.vm.setSSO({ target: { value: "SSO" } }); + + wrapper.vm.passcode = passcode; + + // Trigger the SigninClicked method + await wrapper.vm.SigninClicked(); + + // Assert that authentication failed message is not displayed and isLoggedIn is set to true + expect(wrapper.vm.authFailed).to.equal(""); + expect(wrapper.vm.isLoggedIn).to.be.true; + }); + + it("handles unsuccessful sign-in with invalid credentials", async () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "https://example.com", + isLoggedIn: false, + }, + rpc: { + invoke: jest.fn().mockResolvedValue(false), // Mock RPC to return failure + }, + orgAndSpaceSet: false, + }, + }); + + // Set invalid username and password or passcode + wrapper.setData({ + username: "invalidUsername", + password: "invalidPassword", + }); + + // Trigger the SigninClicked method + await wrapper.vm.SigninClicked(); + + // Assert that authentication failed message is displayed and isLoggedIn remains false + expect(wrapper.vm.authFailed).not.to.equal(""); + expect(wrapper.vm.isLoggedIn).to.be.false; + }); + + it("handles successful sign-out", async () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "https://example.com", + isLoggedIn: true, + }, + rpc: { + invoke: jest.fn().mockResolvedValue(true), // Mock RPC to return success + }, + orgAndSpaceSet: false, + }, + }); + + // Trigger the SignoutClicked method + await wrapper.vm.SignoutClicked(); + + // Assert that isLoggedIn is set to false after sign-out + expect(wrapper.vm.isLoggedIn).to.be.false; + }); + + it("sets SSO or Credentials authentication method", () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "", + isLoggedIn: false, + }, + rpc: {}, + orgAndSpaceSet: false, + }, + }); + + // Assert that ssoOrCredentials is set to "SSO" by default + expect(wrapper.vm.ssoOrCredentials).to.equal("SSO"); + + // Set Credentials authentication method + wrapper.vm.setSSO({ target: { value: "Credentials" } }); + + // Assert that ssoOrCredentials is set to "Credentials" + expect(wrapper.vm.ssoOrCredentials).to.equal("Credentials"); + }); + + it("should set correct button state when conditions are met", async () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "", + isLoggedIn: false, + }, + rpc: {}, + orgAndSpaceSet: false, + }, + }); + + // Set enabled credentials data + await wrapper.setData({ + endpoint: "https://example.com", // Set a valid endpoint + isCFEndpointValid: true, + ssoOrCredentials: "Credentials", + username: "user", + password: "password", + }); + + // Trigger the btnStatus method + await wrapper.vm.btnStatus(); + + // Check that the button is enabled + expect(wrapper.vm.disableButton).to.be.false; + + // Set enabled sso data + await wrapper.setData({ + endpoint: "https://example.com", // Set a valid endpoint + isCFEndpointValid: true, + ssoOrCredentials: "SSO", + passcode: "passcode", + }); + + // Trigger the btnStatus method + await wrapper.vm.btnStatus(); + + // Check that the button is enabled + expect(wrapper.vm.disableButton).to.be.false; + + // Set disabled sso data + await wrapper.setData({ + endpoint: "https://example.com", // Set a valid endpoint + isCFEndpointValid: true, + ssoOrCredentials: "SSO", + passcode: "", + }); + + // Trigger the btnStatus method + await wrapper.vm.btnStatus(); + + // Check that the button is enabled + expect(wrapper.vm.disableButton).to.be.true; + }); + + it("should disable the button when conditions are not met", async () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "", + isLoggedIn: false, + }, + rpc: {}, + orgAndSpaceSet: false, + }, + }); + + // Set some initial values to trigger button disabling + await wrapper.setData({ + endpoint: "", // Set an empty endpoint + isCFEndpointValid: false, + ssoOrCredentials: "Credentials", + username: "", + password: "", + }); + + // Trigger the btnStatus method + await wrapper.vm.btnStatus(); + + // Check that the button is disabled + expect(wrapper.vm.disableButton).to.be.true; + }); + + it("should set CF endpoint and validate it", async () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "", + isLoggedIn: false, + }, + rpc: {}, + orgAndSpaceSet: false, + }, + }); + + // Set a valid CF endpoint + const validEndpoint = "https://example.com"; + await wrapper.vm.setEndpoint({ target: { value: validEndpoint } }); + + // Check if endpoint is correctly set + expect(wrapper.vm.endpoint).to.be.equals(validEndpoint); + + // Set an invalid CF endpoint + const invalidEndpoint = "invalid-url"; + await wrapper.vm.setEndpoint({ target: { value: invalidEndpoint } }); + + // Check if endpoint is correctly set + expect(wrapper.vm.endpoint).to.be.equals(invalidEndpoint); + }); + + it("handles successful open passcode link", async () => { + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "https://example.com", + isLoggedIn: true, + }, + rpc: { + invoke: jest.fn().mockResolvedValue(true), // Mock RPC to return success + }, + orgAndSpaceSet: false, + }, + }); + + const invokeSpy = jest.spyOn(wrapper.props().rpc, "invoke"); + + // Trigger the method that should call openPasscodeLink + await wrapper.vm.openPasscodeLink(); + + // Check that openPasscodeLink was called + expect(invokeSpy.mock.calls[0][0]).to.be.equals("openPasscodeLink"); + }); + + it("handles successful paste", async () => { + // Mock the clipboard API + navigator.clipboard = { + readText: jest.fn().mockResolvedValue("mocked-passcode"), + }; + + const wrapper = shallowMount(CFSignin, { + global, + props: { + target: { + defaultEndpoint: "https://example.com", + isLoggedIn: true, + }, + rpc: { + invoke: jest.fn().mockResolvedValue(true), // Mock RPC to return success + }, + orgAndSpaceSet: false, + }, + }); + + // Trigger the paste method + wrapper.vm.paste(); + + // Assert the passcode is set correctly + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.passcode).to.be.equals("mocked-passcode"); + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/frontend/test/components/CFTarget.spec.js b/projects/cloud-foundry-tools/packages/frontend/test/components/CFTarget.spec.js new file mode 100644 index 000000000..9d7eb7ff0 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/test/components/CFTarget.spec.js @@ -0,0 +1,742 @@ +import CFTarget from "../../src/components/CFTarget.vue"; +import { expect } from "chai"; +import { shallowMount } from "@vue/test-utils"; + +jest.spyOn(window, "getComputedStyle").mockImplementation(() => ({ + getPropertyValue: (property) => { + if (property === "--vscode-descriptionForeground") { + return "#717171"; // Mock the CSS variable value + } + return ""; + }, +})); + +// Stub is used to avoid console warnings of vscode custom elements +const global = { + stubs: { + VscodeDivider: { + template: "
", + }, + VscodeOption: { + template: "
", + }, + VscodeButton: { + template: "", + }, + VscodeSingleSelect: { + template: "
", + }, + }, +}; + +describe("CFTarget.vue", () => { + it("renders without errors", () => { + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", + }, + rpc: {}, + isLoggedIn: true, // Provide isLoggedIn value as needed + }, + global, + }); + expect(wrapper.exists()).to.be.true; + }); + + it("invokes setTarget method on Apply button click", async () => { + const rpcMock = { + invoke: jest.fn().mockResolvedValue(), // Mock the applyTarget method + }; + //avoid showing warning for custom element in console log. + jest.spyOn(console, "warn").mockImplementation(() => { + return; + }); + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", + }, + rpc: rpcMock, + isLoggedIn: true, + }, + global: { + stubs: { + VscodeDivider: { + template: "
", + }, + VscodeOption: { + template: "
", + }, + VscodeDropdown: { + template: "
", + }, + }, + }, + }); + + // Simulate selecting an organization and space + wrapper.setData({ + selectedOrg: { label: "Org Label" }, + selectedSpace: { label: "Space Label" }, + }); + + // Simulate clicking the "Apply" button + await wrapper.find("vscode-button").trigger("click"); + + // Add more assertions for component state updates + expect(wrapper.vm.areOrgAndSpaceSet).to.be.true; // Ensure it's set to true + expect(wrapper.vm.currentOrg).to.equal("Org Label"); // Ensure it's updated with the selected organization + expect(wrapper.vm.currentSpace).to.equal("Space Label"); // Ensure it's updated with the selected space + }); + + it("displays certain elements when logged in", () => { + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", + }, + rpc: {}, + isLoggedIn: true, // Logged in + }, + global, + }); + + // Assert that certain elements are visible + expect(wrapper.find(".cloud-foundry-target").isVisible()).to.be.true; + expect(wrapper.vm.loggedInVisibility).to.equal(""); + }); + + it("hides certain elements when not logged in", () => { + const wrapper = shallowMount(CFTarget, { + props: { + target: {}, + rpc: {}, + isLoggedIn: false, // Simulate not being logged in + }, + global, + }); + + // Assert that certain elements are hidden + expect(wrapper.vm.orgAndSpaceSetVisibility).to.equal("none"); + expect(wrapper.vm.loggedInVisibility).to.equal("none"); + }); + + it("populates organization dropdown", async () => { + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", + }, + rpc: { + invoke: jest.fn().mockResolvedValue([]), // Mock the getOrgs and getSpaces methods + }, + isLoggedIn: true, + }, + global, + }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Assert that the organization dropdown is populated correctly + const orgDropdown = wrapper.find(".cf-drop-down"); // Adjust the selector as needed + expect(orgDropdown.exists()).to.be.true; + }); + + it("populates space dropdown when an organization is selected", async () => { + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", + }, + rpc: { + invoke: jest.fn().mockResolvedValue([]), // Mock the getOrgs and getSpaces methods + }, + isLoggedIn: true, + }, + global, + }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Simulate selecting an organization + await wrapper.setData({ + selectedOrg: { guid: "org-guid", label: "Org Label" }, + }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Assert that the space dropdown is populated correctly + const spaceDropdown = wrapper.find(".cf-drop-down"); // Adjust the selector as needed + expect(spaceDropdown.exists()).to.be.true; + }); + + it("populates organization and space dropdowns", async () => { + const rpcMock = { + invoke: jest.fn().mockResolvedValue([]), // Mock the getOrgs and getSpaces methods + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", + }, + rpc: rpcMock, + isLoggedIn: true, + }, + global, + }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Assert that the organization and space dropdowns are populated correctly + const orgDropdown = wrapper.find(".cf-drop-down"); // Adjust the selector as needed + const spaceDropdown = wrapper.findAll(".cf-drop-down"); // Adjust the selector as needed + expect(orgDropdown.exists()).to.be.true; + expect(spaceDropdown.length).to.equal(2); // You may need to adjust this based on your component's structure + }); + + it("sets currentOrg and currentSpace to undefined when target is undefined", async () => { + const wrapper = shallowMount(CFTarget, { + props: { + target: undefined, + rpc: { + invoke: jest.fn().mockResolvedValue([]), // Mock the getOrgs and getSpaces methods + }, + isLoggedIn: true, + }, + global, + }); + await wrapper.vm.$nextTick(); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ currentOrg: "", currentSpace: "" }); + + // // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.currentOrg).to.equal(undefined); + expect(wrapper.vm.currentSpace).to.equal(undefined); + }); + + it("does select Organizaion when Organizaion is returned from selectedTarget", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([ + { guid: "47423742", label: "bbbb" }, + { guid: "32432423", label: "aaaaa" }, + ]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "32432423", org: "aaaaa" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Assert orgs got sorted + const orgsList = wrapper.vm.$data.orgs; + expect(orgsList[0]?.label).equal("aaaaa"); + expect(orgsList[1]?.label).equal("bbbb"); + expect(wrapper.vm.selectedOrg).to.deep.equal({ + label: "aaaaa", + guid: "32432423", + selected: true, + }); + }); + + it("does not select Organizaion when multiple Organizaions returned and unknown Organizaion from selectedTarget", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([ + { guid: "47423742", label: "bbbb" }, + { guid: "32432423", label: "aaaaa" }, + ]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "00000000", org: "cccccc" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([ + { + label: "space1", + guid: "xxxxxxxx", + orgGUID: "32432423", + }, + { + label: "space2", + guid: "yyyyyyyy", + orgGUID: "47423742", + }, + ]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.selectedOrg).to.deep.equal({}); + expect(wrapper.vm.selectedSpace).to.deep.equal({}); + }); + + it("does select Organizaion and no Space when one Organizaion is returned but multiple Spaces and unknown org from selectedTarget", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([{ guid: "32432423", label: "aaaaa" }]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "00000000", org: "cccccc" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([ + { + label: "space1", + guid: "xxxxxxxx", + orgGUID: "32432423", + }, + { + label: "space2", + guid: "yyyyyyyy", + orgGUID: "47423742", + }, + ]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.selectedOrg).to.deep.equal({ + label: "aaaaa", + guid: "32432423", + }); + expect(wrapper.vm.selectedSpace).to.deep.equal({}); + }); + + it("does select Organisaion and Space when one Organisation and one Space and unknown org from selectedTarget", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([{ guid: "32432423", label: "aaaaa" }]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "00000000", org: "cccccc" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([ + { + label: "space1", + guid: "xxxxxxxx", + orgGUID: "32432423", + }, + ]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.selectedOrg).to.deep.equal({ + label: "aaaaa", + guid: "32432423", + }); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.selectedSpace).to.deep.equal({ + guid: "xxxxxxxx", + label: "space1", + selected: false, + }); + }); + + it("does select Organisaion and Space when one Organisation and multiple Space and known org from selectedTarget", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([{ guid: "32432423", label: "aaaaa" }]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ + guid: "32432423", + org: "aaaaa", + space: "space1", + }); + } else if (funcName === "getSpaces") { + return Promise.resolve([ + { + label: "space1", + guid: "xxxxxxxx", + orgGUID: "32432423", + }, + { + label: "space2", + guid: "xxxxxxxx", + orgGUID: "32432423", + }, + ]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.selectedOrg).to.deep.equal({ + label: "aaaaa", + guid: "32432423", + selected: true, + }); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.selectedSpace).to.deep.equal({ + guid: "xxxxxxxx", + label: "space1", + selected: true, + }); + }); + + it("does show error message when no Organisations are returned", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "00000000", org: "cccccc" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + await wrapper.setData({ + orgMissing: true, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Trigger watch by changing isLoggedIn prop + expect(wrapper.vm.selectedOrg).to.deep.equal({}); + expect(wrapper.vm.selectedSpace).to.deep.equal({}); + expect(wrapper.text()).contain( + "There is no Org defined for this landscape." + ); + }); + + it("does show error message when there is Organisaion but no Spaces are returned", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([{ guid: "32432423", label: "aaaaa" }]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "00000000", org: "cccccc" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + await wrapper.setData({ + spaceMissing: true, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.selectedOrg).to.deep.equal({ + label: "aaaaa", + guid: "32432423", + }); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.selectedSpace).to.deep.equal({}); + expect(wrapper.text()).contain( + "There is no Space defined for the org you selected." + ); + }); + + it("updates selectedOrg when an organization is selected", async () => { + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", // Set your desired currentOrg value + currentSpace: "ExampleSpace", // Set your desired currentSpace value + }, + rpc: {}, + isLoggedIn: true, + }, + global, + }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Simulate selecting an organization + await wrapper.setData({ + selectedOrg: { guid: "org-guid", label: "Org Label" }, + }); + + // Assert that selectedOrg is updated correctly + expect(wrapper.vm.selectedOrg).to.deep.equal({ + guid: "org-guid", + label: "Org Label", + }); + }); + + it("selects a space when an organization is selected", async () => { + // Mock the RPC object with a custom implementation for getSpaces + const rpcMock = { + invoke: jest.fn((method, args) => { + if (method === "getSpaces" && args[0] === "org-guid") { + return Promise.resolve([ + { guid: "space-guid-1", label: "Space 1" }, + { guid: "space-guid-2", label: "Space 2" }, + ]); + } + return Promise.resolve([]); + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "ExampleOrg", + }, + rpc: rpcMock, + isLoggedIn: true, + }, + global, + }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Simulate selecting an organization + await wrapper.setData({ + selectedOrg: { guid: "org-guid", label: "Org Label" }, + }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + // Assert that the space dropdown is populated correctly + const spaceDropdown = wrapper.find(".cf-drop-down"); + expect(spaceDropdown.exists()).to.be.true; + + // Simulate selecting a space + await wrapper.setData({ + selectedSpace: { guid: "space-guid-1", label: "Space 1" }, + }); + + // Assert that selectedSpace is updated correctly + expect(wrapper.vm.selectedSpace).to.deep.equal({ + guid: "space-guid-1", + label: "Space 1", + }); + }); + + it("update selectedOrg and clears selected space when Organization is changed", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([ + { guid: "32432423", label: "aaaaa" }, + { guid: "47423742", label: "bbbb" }, + ]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "00000000", org: "cccccc" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([ + { + label: "space1", + guid: "xxxxxxxx", + orgGUID: "32432423", + }, + ]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + wrapper.vm.updateSelectedOrg({ target: { value: "bbbb" } }); + + expect(wrapper.vm.selectedOrg).to.deep.equal({ + guid: "47423742", + label: "bbbb", + selected: false, + }); + expect(wrapper.vm.selectedSpace).to.deep.equal({}); + }); + + it("update selectedSpace when new space is passed", async () => { + const rpcMock = { + invoke: jest.fn().mockImplementation((funcName) => { + if (funcName === "getOrgs") { + return Promise.resolve([{ guid: "32432423", label: "aaaaa" }]); + } else if (funcName === "getSelectedTarget") { + return Promise.resolve({ guid: "00000000", org: "cccccc" }); + } else if (funcName === "getSpaces") { + return Promise.resolve([]); + } + }), + }; + + const wrapper = shallowMount(CFTarget, { + props: { + target: { + currentOrg: "aaaaa", + }, + rpc: rpcMock, + isLoggedIn: false, + }, + global, + }); + + await wrapper.setData({ + spaces: [ + { + label: "space1", + guid: "xxxxxxxx", + orgGUID: "32432423", + selected: true, + }, + { + label: "space2", + guid: "yyyyyyyy", + orgGUID: "47423742", + }, + ], + }); + // Trigger watch by changing isLoggedIn prop + await wrapper.setProps({ isLoggedIn: true }); + + // Wait for Vue to update the component + await wrapper.vm.$nextTick(); + + wrapper.vm.updateSelectedSpace({ target: { value: "space2" } }); + + expect(wrapper.vm.selectedSpace).to.deep.equal({ + guid: "yyyyyyyy", + label: "space2", + orgGUID: "47423742", + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/frontend/vite.config.js b/projects/cloud-foundry-tools/packages/frontend/vite.config.js new file mode 100644 index 000000000..c1ca7e97a --- /dev/null +++ b/projects/cloud-foundry-tools/packages/frontend/vite.config.js @@ -0,0 +1,35 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +const path = require("path"); + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue({ + template: { + compilerOptions: { + // Instruct the compiler to treat UI5 Web Components and VSCode Elements Web Components as custom elements. + isCustomElement: (tag) => + tag.startsWith("ui5-") || tag.startsWith("vscode-"), + }, + }, + }), + { + name: "html-transform", + transformIndexHtml(html) { + // Add id="vscode-codicon-stylesheet" to the existing link tag + return html.replace( + //, + (match) => + match.replace(" This file contains the changelog history from the standalone [SAP/cloud-foundry-tools](https://github.com/SAP/cloud-foundry-tools) repository, prior to migration into this monorepo. New changes are recorded in this package's `CHANGELOG.md`. + +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [8.1.2](https://github.com/SAP/cloud-foundry-tools/compare/v8.1.0...v8.1.2) (2026-06-21) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [8.1.1](https://github.com/SAP/cloud-foundry-tools/compare/v8.1.0...v8.1.1) (2026-06-21) + +**Note:** Version bump only for package vscode-wing-cf-tools + +# [8.1.0](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.2...v8.1.0) (2026-02-11) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [8.0.2](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.1...v8.0.2) (2025-12-09) + +**Note:** Version bump only for package vscode-wing-cf-tools + +## [8.0.1](https://github.com/SAP/cloud-foundry-tools/compare/v8.0.0...v8.0.1) (2025-08-24) + +**Note:** Version bump only for package vscode-wing-cf-tools diff --git a/projects/cloud-foundry-tools/packages/wrapper/README.md b/projects/cloud-foundry-tools/packages/wrapper/README.md new file mode 100644 index 000000000..9af482727 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/wrapper/README.md @@ -0,0 +1,5 @@ +# Cloud Foundry Tools Wrapper Extension + +> **⚠️ This extension is deprecated.** + +Please use [`saposs.vscode-cf-tools`](https://marketplace.visualstudio.com/items?itemName=saposs.vscode-cf-tools) instead. diff --git a/projects/cloud-foundry-tools/packages/wrapper/nyc.config.js b/projects/cloud-foundry-tools/packages/wrapper/nyc.config.js new file mode 100644 index 000000000..4925b9990 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/wrapper/nyc.config.js @@ -0,0 +1,17 @@ +module.exports = { + reporter: ["text", "lcov"], + "check-coverage": true, + all: true, + extension: [".js", ".ts", ".vue"], + // Redirect coverage output away from the default .nyc_output so this VS Code + // extension is NOT pulled into the root merged 100% gate (which targets the + // library packages). It keeps its own thresholds, matching the + // guided-development / yeoman-ui extension-backend precedent. + "temp-dir": "./reports/.nyc_output", + "report-dir": "./reports/coverage", + branches: 100, + lines: 100, + functions: 100, + statements: 100, + include: ["**/src/**"], +}; diff --git a/projects/cloud-foundry-tools/packages/wrapper/package.json b/projects/cloud-foundry-tools/packages/wrapper/package.json new file mode 100644 index 000000000..123d417ec --- /dev/null +++ b/projects/cloud-foundry-tools/packages/wrapper/package.json @@ -0,0 +1,68 @@ +{ + "name": "vscode-wing-cf-tools", + "displayName": "Cloud Foundry Tools", + "version": "8.1.2", + "private": true, + "description": "Wrapper for vscode extension of Cloud Foundry tools", + "categories": [ + "Other" + ], + "bugs": { + "url": "https://github.com/SAP/app-studio-toolkit/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/SAP/app-studio-toolkit.git", + "directory": "projects/cloud-foundry-tools/packages/wrapper" + }, + "license": "Apache 2.0", + "publisher": "SAP", + "main": "./dist/extension.js", + "scripts": { + "build": "npm-run-all clean compile webpack-dev", + "bundle": "webpack --mode production", + "ci": "npm-run-all clean compile coverage bundle package coverage:copy", + "ci:artifacts_only": "npm-run-all clean compile webpack-dev package", + "clean": "npm-run-all clean:dist clean:out clean:vsix clean:reports", + "clean:dist": "rimraf ./dist", + "clean:out": "rimraf ./out", + "clean:vsix": "rimraf ./*.vsix", + "compile": "tsc -p ./", + "coverage": "nyc mocha --colors", + "package": "vsce package --no-dependencies", + "test": "mocha --colors", + "watch": "tsc -watch -p ./", + "webpack-dev": "webpack --mode development", + "webpack-dev:watch": "webpack --mode development --watch", + "clean:reports": "rimraf ./reports", + "coverage:copy": "shx mkdir -p ../../../../coverage && shx cp -u ./reports/coverage/lcov.info ../../../../coverage/lcov_cf_wrapper.info" + }, + "activationEvents": [ + "onStartupFinished" + ], + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "extensionDependencies": [ + "saposs.vscode-cf-tools" + ], + "devDependencies": { + "@types/node": "^14.14.6", + "@types/sinon": "^17.0.3", + "@types/vscode": "^1.43.0", + "@vscode-logging/types": "^2.0.0", + "@vscode/vsce": "2.24.0", + "mocha": "^9.2.2", + "sinon": "^19.0.2", + "ts-loader": "^9.2.8", + "ts-node": "^10.7.0", + "webpack": "^5.94.0", + "webpack-cli": "^4.9.2" + }, + "engines": { + "vscode": "^1.50.0" + }, + "icon": "resources/icon.png" +} diff --git a/projects/cloud-foundry-tools/packages/wrapper/resources/icon.png b/projects/cloud-foundry-tools/packages/wrapper/resources/icon.png new file mode 100644 index 000000000..0b8ee6692 Binary files /dev/null and b/projects/cloud-foundry-tools/packages/wrapper/resources/icon.png differ diff --git a/projects/cloud-foundry-tools/packages/wrapper/src/extension.ts b/projects/cloud-foundry-tools/packages/wrapper/src/extension.ts new file mode 100644 index 000000000..3e3893382 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/wrapper/src/extension.ts @@ -0,0 +1,7 @@ +export function activate(): void { + console.log("Activating extension..."); +} + +export function deactivate(): void { + console.log("Deactivating extension..."); +} diff --git a/projects/cloud-foundry-tools/packages/wrapper/tests/extension.spec.ts b/projects/cloud-foundry-tools/packages/wrapper/tests/extension.spec.ts new file mode 100644 index 000000000..fc35aff21 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/wrapper/tests/extension.spec.ts @@ -0,0 +1,29 @@ +import { expect } from "chai"; +import { createSandbox } from "sinon"; +import { activate, deactivate } from "./../src/extension"; + +describe("extension unit test", () => { + let sandbox: sinon.SinonSandbox; + + before(() => { + sandbox = createSandbox(); + }); + + beforeEach(() => {}); + + afterEach(() => { + sandbox.restore(); + }); + + describe("activate", () => { + it("activation test", () => { + expect(activate()).to.be.undefined; + }); + }); + + describe("deactivate", () => { + it("deactivation test", () => { + expect(deactivate()).to.be.undefined; + }); + }); +}); diff --git a/projects/cloud-foundry-tools/packages/wrapper/tsconfig.json b/projects/cloud-foundry-tools/packages/wrapper/tsconfig.json new file mode 100644 index 000000000..ded225140 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/wrapper/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "rootDir": ".", + "rootDirs": ["./src", "./tests"], + "useUnknownInCatchVariables": false + } +} diff --git a/projects/cloud-foundry-tools/packages/wrapper/webpack.config.js b/projects/cloud-foundry-tools/packages/wrapper/webpack.config.js new file mode 100644 index 000000000..7bfaa1534 --- /dev/null +++ b/projects/cloud-foundry-tools/packages/wrapper/webpack.config.js @@ -0,0 +1,31 @@ +const path = require("path"); +const baseConfig = require("../../webpack.config.vscode.base"); + +const config = Object.assign({}, baseConfig, { + entry: "./out/src/extension.js", + output: { + path: path.resolve(__dirname, "dist"), + filename: "extension.js", + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + module: { + // https://webpack.js.org/configuration/module/#modulenoparse + // used to avoid transforming native require usage in `optional-require` implementation + noParse: /native-require\.(js|ts)$/, + }, + // 📖 -> https://webpack.js.org/configuration/externals/ + externals: { + // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed. + vscode: "commonjs vscode", + // To enable bundling for @sap/artifact-management + // fsevents is a macos file system event library that is compiled during installation. + fsevents: "commonjs fsevents", + }, + node: { + // needed to bundle artifact-management successfully + __dirname: false, + }, +}); + +module.exports = config; diff --git a/projects/cloud-foundry-tools/tsconfig.base.json b/projects/cloud-foundry-tools/tsconfig.base.json new file mode 100644 index 000000000..7871e0cf0 --- /dev/null +++ b/projects/cloud-foundry-tools/tsconfig.base.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "skipLibCheck": true + } +} diff --git a/projects/cloud-foundry-tools/webpack.config.vscode.base.js b/projects/cloud-foundry-tools/webpack.config.vscode.base.js new file mode 100644 index 000000000..d21eb2511 --- /dev/null +++ b/projects/cloud-foundry-tools/webpack.config.vscode.base.js @@ -0,0 +1,20 @@ +const path = require("path"); + +const config = { + optimization: { + // The Default minimization options cause JavaScript runtime errors. + // Also we don't actually need to minimize as much (not targeted for browser). + // Rather we mostly need to reduce the number of fileSystem access requests + // by reducing the number of files packaged inside our VSCode extensions + minimize: false, + }, + target: "node", + devtool: "source-map", + resolve: { + // pnpm hoists deps to the workspace root; resolve from there first + modules: [path.resolve(__dirname, "../../node_modules"), "node_modules"], + extensions: [".js"], + }, +}; + +module.exports = config;