From 854762b36e4659f9ff599c2264a3a06af2ca392f Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Wed, 27 May 2026 15:33:38 -0400 Subject: [PATCH 01/28] feat: CDK LSP/Explorer Package Scaffolding (#1559) Basic scaffolding for new packages to be implemented: a core functionality library, the lsp server, and the web explorer. ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .github/dependabot.yml | 1 + .github/workflows/pull-request-lint.yml | 1 + .projenrc.ts | 45 +++ aws-cdk-cli.code-workspace | 3 + package.json | 6 +- packages/@aws-cdk/cdk-explorer/.eslintrc.js | 9 + packages/@aws-cdk/cdk-explorer/.eslintrc.json | 320 ++++++++++++++++++ packages/@aws-cdk/cdk-explorer/.gitattributes | 25 ++ packages/@aws-cdk/cdk-explorer/.gitignore | 53 +++ packages/@aws-cdk/cdk-explorer/.npmignore | 24 ++ .../@aws-cdk/cdk-explorer/.prettierignore | 2 + .../@aws-cdk/cdk-explorer/.prettierrc.json | 6 + .../@aws-cdk/cdk-explorer/.projen/deps.json | 136 ++++++++ .../@aws-cdk/cdk-explorer/.projen/files.json | 19 ++ .../@aws-cdk/cdk-explorer/.projen/tasks.json | 180 ++++++++++ packages/@aws-cdk/cdk-explorer/.yarnrc.yml | 3 + packages/@aws-cdk/cdk-explorer/LICENSE | 202 +++++++++++ packages/@aws-cdk/cdk-explorer/README.md | 1 + .../@aws-cdk/cdk-explorer/jest.config.json | 67 ++++ packages/@aws-cdk/cdk-explorer/lib/index.ts | 1 + packages/@aws-cdk/cdk-explorer/package.json | 87 +++++ .../@aws-cdk/cdk-explorer/test/index.test.ts | 5 + .../@aws-cdk/cdk-explorer/tsconfig.dev.json | 53 +++ packages/@aws-cdk/cdk-explorer/tsconfig.json | 51 +++ tsconfig.dev.json | 3 + tsconfig.json | 3 + yarn.lock | 254 ++++++++++++++ 27 files changed, 1558 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/.eslintrc.js create mode 100644 packages/@aws-cdk/cdk-explorer/.eslintrc.json create mode 100644 packages/@aws-cdk/cdk-explorer/.gitattributes create mode 100644 packages/@aws-cdk/cdk-explorer/.gitignore create mode 100644 packages/@aws-cdk/cdk-explorer/.npmignore create mode 100644 packages/@aws-cdk/cdk-explorer/.prettierignore create mode 100644 packages/@aws-cdk/cdk-explorer/.prettierrc.json create mode 100644 packages/@aws-cdk/cdk-explorer/.projen/deps.json create mode 100644 packages/@aws-cdk/cdk-explorer/.projen/files.json create mode 100644 packages/@aws-cdk/cdk-explorer/.projen/tasks.json create mode 100644 packages/@aws-cdk/cdk-explorer/.yarnrc.yml create mode 100644 packages/@aws-cdk/cdk-explorer/LICENSE create mode 100644 packages/@aws-cdk/cdk-explorer/README.md create mode 100644 packages/@aws-cdk/cdk-explorer/jest.config.json create mode 100644 packages/@aws-cdk/cdk-explorer/lib/index.ts create mode 100644 packages/@aws-cdk/cdk-explorer/package.json create mode 100644 packages/@aws-cdk/cdk-explorer/test/index.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/tsconfig.dev.json create mode 100644 packages/@aws-cdk/cdk-explorer/tsconfig.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 868813a88..460ef2bf2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,7 @@ updates: - / - /packages/@aws-cdk-testing/cli-integ - /packages/@aws-cdk/cdk-assets-lib + - /packages/@aws-cdk/cdk-explorer - /packages/@aws-cdk/cli-plugin-contract - /packages/@aws-cdk/cloud-assembly-api - /packages/@aws-cdk/cloud-assembly-schema diff --git a/.github/workflows/pull-request-lint.yml b/.github/workflows/pull-request-lint.yml index e62a4f179..c591c852d 100644 --- a/.github/workflows/pull-request-lint.yml +++ b/.github/workflows/pull-request-lint.yml @@ -36,6 +36,7 @@ jobs: bootstrap cdk-assets cdk-assets-lib + cdk-explorer cli cli-integ cli-plugin-contract diff --git a/.projenrc.ts b/.projenrc.ts index 2c1c94722..a594463b5 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1660,6 +1660,51 @@ cliInteg.npmignore?.addPatterns('!resources/**/*'); cliInteg.postCompileTask.exec('yarn-cling'); cliInteg.gitignore.addPatterns('npm-shrinkwrap.json'); +// #endregion +////////////////////////////////////////////////////////////////////// +// #region @aws-cdk/cdk-explorer + +const cdkExplorer = configureProject( + new yarn.TypeScriptWorkspace({ + ...genericCdkProps({ + private: true, + }), + parent: repo, + name: '@aws-cdk/cdk-explorer', + description: 'CDK Explorer — LSP server, synth daemon, and web interface for AWS CDK', + srcdir: 'lib', + deps: [ + cloudAssemblySchema.customizeReference({ versionType: 'any-future' }), + cloudAssemblyApi.customizeReference({ versionType: 'exact' }), + 'vscode-languageserver@^9', + 'vscode-languageserver-textdocument@^1', + 'vscode-jsonrpc@^8', + 'express@^4', + ], + devDeps: [ + 'vscode-languageserver-protocol@^3', + '@types/express@^4', + ], + tsconfig: { + compilerOptions: { + ...defaultTsOptions, + }, + }, + jestOptions: jestOptionsForProject({ + jestConfig: { + coverageThreshold: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, + }, + }), + }), +); +fixupTestTask(cdkExplorer); +void cdkExplorer; + // #endregion ////////////////////////////////////////////////////////////////////// // #region shared setup diff --git a/aws-cdk-cli.code-workspace b/aws-cdk-cli.code-workspace index bc246cdc2..b00594963 100644 --- a/aws-cdk-cli.code-workspace +++ b/aws-cdk-cli.code-workspace @@ -11,6 +11,9 @@ { "path": "packages/@aws-cdk/cdk-assets-lib" }, + { + "path": "packages/@aws-cdk/cdk-explorer" + }, { "path": "packages/@aws-cdk/cli-plugin-contract" }, diff --git a/package.json b/package.json index 963036721..82ffa9241 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "packages/aws-cdk", "packages/cdk", "packages/@aws-cdk/integ-runner", - "packages/@aws-cdk-testing/cli-integ" + "packages/@aws-cdk-testing/cli-integ", + "packages/@aws-cdk/cdk-explorer" ] }, "jest": { @@ -102,7 +103,8 @@ "/packages/aws-cdk", "/packages/cdk", "/packages/@aws-cdk/integ-runner", - "/packages/@aws-cdk-testing/cli-integ" + "/packages/@aws-cdk-testing/cli-integ", + "/packages/@aws-cdk/cdk-explorer" ] }, "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"yarn projen\"." diff --git a/packages/@aws-cdk/cdk-explorer/.eslintrc.js b/packages/@aws-cdk/cdk-explorer/.eslintrc.js new file mode 100644 index 000000000..8f296a38a --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.eslintrc.js @@ -0,0 +1,9 @@ +var path = require('path'); +var fs = require('fs'); +var contents = fs.readFileSync(`${__dirname}/.eslintrc.json`, { encoding: 'utf-8' }); +// Strip comments, JSON.parse() doesn't like those +contents = contents.replace(/^\/\/.*$/m, ''); +var json = JSON.parse(contents); +// Patch the .json config with something that can only be represented in JS +json.parserOptions.tsconfigRootDir = __dirname; +module.exports = json; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-explorer/.eslintrc.json b/packages/@aws-cdk/cdk-explorer/.eslintrc.json new file mode 100644 index 000000000..ae0501b14 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.eslintrc.json @@ -0,0 +1,320 @@ +// ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". +{ + "env": { + "jest": true, + "node": true + }, + "root": true, + "plugins": [ + "@typescript-eslint", + "import", + "@cdklabs", + "@stylistic", + "jest", + "jsdoc" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "project": "./tsconfig.dev.json" + }, + "extends": [ + "plugin:import/typescript", + "plugin:jest/recommended", + "plugin:prettier/recommended" + ], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [ + ".ts", + ".tsx" + ] + }, + "import/resolver": { + "node": {}, + "typescript": { + "project": "./tsconfig.dev.json", + "alwaysTryTypes": true + } + } + }, + "ignorePatterns": [ + "*.js", + "*.d.ts", + "node_modules/", + "*.generated.ts", + "coverage", + "*.generated.ts" + ], + "rules": { + "curly": [ + "error", + "multi-line", + "consistent" + ], + "@typescript-eslint/no-require-imports": "error", + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": [ + "**/test/**", + "**/build-tools/**" + ], + "optionalDependencies": false, + "peerDependencies": true + } + ], + "import/no-unresolved": [ + "error" + ], + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external" + ], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "import/no-duplicates": "error", + "no-shadow": [ + "off" + ], + "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/no-floating-promises": "error", + "no-return-await": [ + "off" + ], + "@typescript-eslint/return-await": "error", + "dot-notation": [ + "error" + ], + "no-bitwise": [ + "error" + ], + "@typescript-eslint/member-ordering": [ + "error", + { + "default": [ + "public-static-field", + "public-static-method", + "protected-static-field", + "protected-static-method", + "private-static-field", + "private-static-method", + "field", + "constructor", + "method" + ] + } + ], + "@cdklabs/promiseall-no-unbounded-parallelism": [ + "error" + ], + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression:matches([callee.name='createHash'], [callee.property.name='createHash']) Literal[value='md5']", + "message": "Use the md5hash() function from the core library if you want md5" + } + ], + "no-throw-literal": [ + "error" + ], + "no-console": [ + "error" + ], + "@cdklabs/no-invalid-path": [ + "error" + ], + "@typescript-eslint/unbound-method": "error", + "@cdklabs/no-core-construct": [ + "error" + ], + "@cdklabs/invalid-cfn-imports": [ + "error" + ], + "@cdklabs/no-literal-partition": [ + "error" + ], + "@typescript-eslint/consistent-type-imports": "error", + "import/no-relative-packages": "error", + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "punycode", + "message": "Package 'punycode' has to be imported with trailing slash, see warning in https://github.com/bestiejs/punycode.js#installation" + } + ], + "patterns": [ + "!punycode/" + ] + } + ], + "@stylistic/indent": [ + "error", + 2 + ], + "@stylistic/quotes": [ + "error", + "single", + { + "avoidEscape": true + } + ], + "@stylistic/member-delimiter-style": [ + "error" + ], + "@stylistic/comma-dangle": [ + "error", + "always-multiline" + ], + "@stylistic/no-extra-semi": [ + "error" + ], + "@stylistic/curly-newline": [ + "error", + "always" + ], + "@stylistic/comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "@stylistic/no-multi-spaces": [ + "error", + { + "ignoreEOLComments": false + } + ], + "@stylistic/array-bracket-spacing": [ + "error", + "never" + ], + "@stylistic/array-bracket-newline": [ + "error", + "consistent" + ], + "@stylistic/object-curly-spacing": [ + "error", + "always" + ], + "@stylistic/object-curly-newline": [ + "error", + { + "multiline": true, + "consistent": true + } + ], + "@stylistic/object-property-newline": [ + "error", + { + "allowAllPropertiesOnSameLine": true + } + ], + "@stylistic/keyword-spacing": [ + "error" + ], + "@stylistic/brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + "@stylistic/space-before-blocks": "error", + "@stylistic/eol-last": [ + "error", + "always" + ], + "@stylistic/spaced-comment": [ + "error", + "always", + { + "exceptions": [ + "/", + "*" + ], + "markers": [ + "/" + ] + } + ], + "@stylistic/padded-blocks": [ + "error", + { + "classes": "never", + "blocks": "never", + "switches": "never" + } + ], + "@stylistic/key-spacing": [ + "error" + ], + "@stylistic/quote-props": [ + "error", + "consistent-as-needed" + ], + "@stylistic/no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "@stylistic/no-trailing-spaces": [ + "error" + ], + "@stylistic/semi": [ + "error", + "always" + ], + "@stylistic/max-len": [ + "error", + { + "code": 150, + "ignoreUrls": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreComments": true, + "ignoreRegExpLiterals": true + } + ], + "jsdoc/require-param-description": [ + "error" + ], + "jsdoc/require-property-description": [ + "error" + ], + "jsdoc/require-returns-description": [ + "error" + ], + "jsdoc/check-alignment": [ + "error" + ], + "jsdoc/require-hyphen-before-param-description": [ + "error" + ], + "jest/expect-expect": "off", + "jest/no-conditional-expect": "off", + "jest/no-done-callback": "off", + "jest/no-standalone-expect": "off", + "jest/valid-expect": "off", + "jest/valid-title": "off", + "jest/no-identical-title": "off", + "jest/no-disabled-tests": "error", + "jest/no-focused-tests": "error", + "prettier/prettier": [ + "off" + ] + }, + "overrides": [] +} diff --git a/packages/@aws-cdk/cdk-explorer/.gitattributes b/packages/@aws-cdk/cdk-explorer/.gitattributes new file mode 100644 index 000000000..e0c2bceed --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.gitattributes @@ -0,0 +1,25 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". + +* text=auto eol=lf +/.eslintrc.js linguist-generated +/.eslintrc.json linguist-generated linguist-language=JSON-with-Comments +/.gitattributes linguist-generated +/.gitignore linguist-generated +/.npmignore linguist-generated +/.pnp.* binary linguist-vendored +/.prettierignore linguist-generated +/.prettierrc.json linguist-generated +/.projen/** linguist-generated +/.projen/deps.json linguist-generated +/.projen/files.json linguist-generated +/.projen/tasks.json linguist-generated +/.yarn/** linguist-vendored +/.yarn/plugins/**/* binary +/.yarn/releases/* binary +/.yarnrc.yml linguist-generated +/jest.config.json linguist-generated +/LICENSE linguist-generated +/package.json linguist-generated +/tsconfig.dev.json linguist-generated linguist-language=JSON-with-Comments +/tsconfig.json linguist-generated linguist-language=JSON-with-Comments +/yarn.lock linguist-generated \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-explorer/.gitignore b/packages/@aws-cdk/cdk-explorer/.gitignore new file mode 100644 index 000000000..7722c6259 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.gitignore @@ -0,0 +1,53 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". +!/.gitattributes +!/.projen/tasks.json +!/.projen/deps.json +!/.projen/files.json +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +.pnp.* +!/.yarnrc.yml +!/package.json +!/LICENSE +!/.npmignore +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +pids +*.pid +*.seed +*.pid.lock +lib-cov +coverage +*.lcov +.nyc_output +build/Release +node_modules/ +jspm_packages/ +*.tsbuildinfo +.eslintcache +*.tgz +.yarn-integrity +.cache +!/jest.config.json +/coverage/ +!/.prettierignore +!/.prettierrc.json +!/test/ +!/tsconfig.json +!/tsconfig.dev.json +!/lib/ +/lib/**/*.js +/lib/**/*.d.ts +/lib/**/*.d.ts.map +/dist/ +!/.eslintrc.json +!/.eslintrc.js diff --git a/packages/@aws-cdk/cdk-explorer/.npmignore b/packages/@aws-cdk/cdk-explorer/.npmignore new file mode 100644 index 000000000..75d96a45f --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.npmignore @@ -0,0 +1,24 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". +/.projen/ +/jest.config.json +/coverage/ +/.prettierignore +/.prettierrc.json +/test/ +/tsconfig.dev.json +!/lib/ +!/lib/**/*.js +!/lib/**/*.d.ts +dist +/tsconfig.json +/.github/ +/.vscode/ +/.idea/ +/.projenrc.js +tsconfig.tsbuildinfo +/.eslintrc.json +.eslintrc.js +*.ts +!*.d.ts +build-tools +/.gitattributes diff --git a/packages/@aws-cdk/cdk-explorer/.prettierignore b/packages/@aws-cdk/cdk-explorer/.prettierignore new file mode 100644 index 000000000..d715de5ca --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.prettierignore @@ -0,0 +1,2 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". +.eslintrc.js diff --git a/packages/@aws-cdk/cdk-explorer/.prettierrc.json b/packages/@aws-cdk/cdk-explorer/.prettierrc.json new file mode 100644 index 000000000..af318ca5f --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "overrides": [] +} diff --git a/packages/@aws-cdk/cdk-explorer/.projen/deps.json b/packages/@aws-cdk/cdk-explorer/.projen/deps.json new file mode 100644 index 000000000..8917a0cfe --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.projen/deps.json @@ -0,0 +1,136 @@ +{ + "dependencies": [ + { + "name": "@cdklabs/eslint-plugin", + "type": "build" + }, + { + "name": "@stylistic/eslint-plugin", + "version": "^3", + "type": "build" + }, + { + "name": "@types/express", + "version": "^4", + "type": "build" + }, + { + "name": "@types/jest", + "type": "build" + }, + { + "name": "@types/node", + "version": "^20", + "type": "build" + }, + { + "name": "@typescript-eslint/eslint-plugin", + "version": "^8", + "type": "build" + }, + { + "name": "@typescript-eslint/parser", + "version": "^8", + "type": "build" + }, + { + "name": "constructs", + "version": "^10.0.0", + "type": "build" + }, + { + "name": "eslint-config-prettier", + "type": "build" + }, + { + "name": "eslint-import-resolver-typescript", + "type": "build" + }, + { + "name": "eslint-plugin-import", + "type": "build" + }, + { + "name": "eslint-plugin-jest", + "type": "build" + }, + { + "name": "eslint-plugin-jsdoc", + "type": "build" + }, + { + "name": "eslint-plugin-prettier", + "type": "build" + }, + { + "name": "eslint", + "version": "^9", + "type": "build" + }, + { + "name": "jest", + "type": "build" + }, + { + "name": "jest-junit", + "version": "^16", + "type": "build" + }, + { + "name": "nx", + "type": "build" + }, + { + "name": "prettier", + "version": "^2.8", + "type": "build" + }, + { + "name": "projen", + "type": "build" + }, + { + "name": "ts-jest", + "type": "build" + }, + { + "name": "typescript", + "version": "5.9", + "type": "build" + }, + { + "name": "vscode-languageserver-protocol", + "version": "^3", + "type": "build" + }, + { + "name": "@aws-cdk/cloud-assembly-api", + "type": "runtime" + }, + { + "name": "@aws-cdk/cloud-assembly-schema", + "type": "runtime" + }, + { + "name": "express", + "version": "^4", + "type": "runtime" + }, + { + "name": "vscode-jsonrpc", + "version": "^8", + "type": "runtime" + }, + { + "name": "vscode-languageserver-textdocument", + "version": "^1", + "type": "runtime" + }, + { + "name": "vscode-languageserver", + "version": "^9", + "type": "runtime" + } + ], + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"yarn projen\"." +} diff --git a/packages/@aws-cdk/cdk-explorer/.projen/files.json b/packages/@aws-cdk/cdk-explorer/.projen/files.json new file mode 100644 index 000000000..19f0e3151 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.projen/files.json @@ -0,0 +1,19 @@ +{ + "files": [ + ".eslintrc.js", + ".eslintrc.json", + ".gitattributes", + ".gitignore", + ".npmignore", + ".prettierignore", + ".prettierrc.json", + ".projen/deps.json", + ".projen/files.json", + ".projen/tasks.json", + "jest.config.json", + "LICENSE", + "tsconfig.dev.json", + "tsconfig.json" + ], + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"yarn projen\"." +} diff --git a/packages/@aws-cdk/cdk-explorer/.projen/tasks.json b/packages/@aws-cdk/cdk-explorer/.projen/tasks.json new file mode 100644 index 000000000..a066ee121 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.projen/tasks.json @@ -0,0 +1,180 @@ +{ + "tasks": { + "build": { + "name": "build", + "description": "Full release build", + "steps": [ + { + "spawn": "pre-compile" + }, + { + "spawn": "compile" + }, + { + "spawn": "post-compile" + }, + { + "spawn": "test" + }, + { + "spawn": "package" + } + ] + }, + "bump": { + "name": "bump", + "description": "Bumps versions of local dependencies", + "steps": [ + { + "spawn": "gather-versions" + } + ] + }, + "check-for-updates": { + "name": "check-for-updates", + "env": { + "CI": "0", + "YARN_ENABLE_IMMUTABLE_INSTALLS": "false", + "YARN_NPM_MINIMAL_AGE_GATE": "4320" + }, + "steps": [ + { + "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,jest,nx,projen,ts-jest" + } + ] + }, + "compile": { + "name": "compile", + "description": "Only compile", + "steps": [ + { + "exec": "tsc --build", + "receiveArgs": true + } + ] + }, + "default": { + "name": "default", + "description": "Synthesize project files", + "steps": [ + { + "exec": "projen default", + "cwd": "../../.." + } + ] + }, + "eslint": { + "name": "eslint", + "description": "Runs eslint against the codebase", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + "NODE_NO_WARNINGS": "1" + }, + "steps": [ + { + "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ lib test build-tools", + "receiveArgs": true + } + ] + }, + "gather-versions": { + "name": "gather-versions", + "steps": [ + { + "exec": "node -e \"require(require.resolve('cdklabs-projen-project-types/lib/yarn/gather-versions.exec.js')).cliMain()\" @aws-cdk/cloud-assembly-schema=any-future @aws-cdk/cloud-assembly-api=exact", + "receiveArgs": true + } + ] + }, + "install": { + "name": "install", + "description": "Install project dependencies and update lockfile (non-frozen)", + "steps": [ + { + "exec": "yarn install --no-immutable" + } + ] + }, + "install:ci": { + "name": "install:ci", + "description": "Install project dependencies using frozen lockfile", + "steps": [ + { + "exec": "yarn install --immutable" + } + ] + }, + "nx": { + "name": "nx", + "steps": [ + { + "exec": "nx run", + "receiveArgs": true + } + ] + }, + "package": { + "name": "package", + "description": "Creates the distribution package" + }, + "post-compile": { + "name": "post-compile", + "description": "Runs after successful compilation" + }, + "pre-compile": { + "name": "pre-compile", + "description": "Prepare the project for compilation" + }, + "test": { + "name": "test", + "description": "Run tests", + "env": { + "TESTING_CDK": "1" + }, + "steps": [ + { + "exec": "jest --passWithNoTests --updateSnapshot", + "receiveArgs": true + }, + { + "spawn": "eslint" + } + ], + "condition": "[ \"$NO_UNIT_TESTS\" != \"1\" ]" + }, + "test:watch": { + "name": "test:watch", + "description": "Run jest in watch mode", + "steps": [ + { + "exec": "jest --watch" + } + ] + }, + "unbump": { + "name": "unbump", + "description": "Resets versions of local dependencies to 0.0.0", + "steps": [ + { + "spawn": "gather-versions", + "env": { + "RESET_VERSIONS": "true" + } + } + ] + }, + "watch": { + "name": "watch", + "description": "Watch & compile in the background", + "steps": [ + { + "exec": "tsc --build -w" + } + ] + } + }, + "env": { + "PATH": "$(yarn exec node --print process.env.PATH)" + }, + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"yarn projen\"." +} diff --git a/packages/@aws-cdk/cdk-explorer/.yarnrc.yml b/packages/@aws-cdk/cdk-explorer/.yarnrc.yml new file mode 100644 index 000000000..f0ac08c6b --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/.yarnrc.yml @@ -0,0 +1,3 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". + +{} diff --git a/packages/@aws-cdk/cdk-explorer/LICENSE b/packages/@aws-cdk/cdk-explorer/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/cdk-explorer/README.md b/packages/@aws-cdk/cdk-explorer/README.md new file mode 100644 index 000000000..b3fa7ddcd --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/README.md @@ -0,0 +1 @@ +# replace this \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-explorer/jest.config.json b/packages/@aws-cdk/cdk-explorer/jest.config.json new file mode 100644 index 000000000..ac29819e4 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/jest.config.json @@ -0,0 +1,67 @@ +{ + "coverageProvider": "v8", + "moduleFileExtensions": [ + "ts", + "js" + ], + "maxWorkers": "80%", + "testEnvironment": "node", + "coverageThreshold": { + "global": { + "statements": 80, + "branches": 80, + "functions": 80, + "lines": 80 + } + }, + "collectCoverage": true, + "coverageReporters": [ + "text-summary", + "cobertura", + [ + "html", + { + "subdir": "html-report" + } + ] + ], + "testMatch": [ + "/test/**/?(*.)+(test).ts", + "/@(lib|test)/**/*(*.)@(spec|test).ts?(x)", + "/@(lib|test)/**/__tests__/**/*.ts?(x)" + ], + "coveragePathIgnorePatterns": [ + "\\.generated\\.[jt]s$", + "/test/", + ".warnings.jsii.js$", + "/node_modules/" + ], + "reporters": [ + "default", + [ + "jest-junit", + { + "suiteName": "jest tests", + "outputDirectory": "coverage" + } + ] + ], + "randomize": true, + "clearMocks": true, + "coverageDirectory": "coverage", + "testPathIgnorePatterns": [ + "/node_modules/" + ], + "watchPathIgnorePatterns": [ + "/node_modules/" + ], + "transform": { + "^.+\\.[t]sx?$": [ + "ts-jest", + { + "tsconfig": "tsconfig.dev.json" + } + ] + }, + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"yarn projen\"." +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/index.ts b/packages/@aws-cdk/cdk-explorer/lib/index.ts new file mode 100644 index 000000000..0c18d5d94 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/index.ts @@ -0,0 +1 @@ +export const VERSION = '0.0.0'; diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json new file mode 100644 index 000000000..9fb88a37f --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-cdk/cdk-explorer", + "description": "CDK Explorer — LSP server, synth daemon, and web interface for AWS CDK", + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk-cli", + "directory": "packages/@aws-cdk/cdk-explorer" + }, + "scripts": { + "build": "projen build", + "bump": "projen bump", + "check-for-updates": "projen check-for-updates", + "compile": "projen compile", + "default": "projen default", + "eslint": "projen eslint", + "gather-versions": "projen gather-versions", + "nx": "projen nx", + "package": "projen package", + "post-compile": "projen post-compile", + "pre-compile": "projen pre-compile", + "test": "projen test", + "test:watch": "projen test:watch", + "unbump": "projen unbump", + "watch": "projen watch", + "projen": "projen" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "devDependencies": { + "@cdklabs/eslint-plugin": "^2.0.6", + "@stylistic/eslint-plugin": "^3", + "@types/express": "^4", + "@types/jest": "^29.5.14", + "@types/node": "^20", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "constructs": "^10.0.0", + "eslint": "^9", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^29.15.2", + "eslint-plugin-jsdoc": "^62.9.0", + "eslint-plugin-prettier": "^4.2.5", + "jest": "^29.7.0", + "jest-junit": "^16", + "nx": "^22.7.2", + "prettier": "^2.8", + "projen": "^0.99.63", + "ts-jest": "^29.4.11", + "typescript": "5.9", + "vscode-languageserver-protocol": "^3" + }, + "dependencies": { + "@aws-cdk/cloud-assembly-api": "^0.0.0", + "@aws-cdk/cloud-assembly-schema": "^0.0.0", + "express": "^4", + "vscode-jsonrpc": "^8", + "vscode-languageserver": "^9", + "vscode-languageserver-textdocument": "^1" + }, + "keywords": [ + "aws", + "cdk" + ], + "engines": { + "node": ">= 18.0.0" + }, + "devEngines": { + "packageManager": { + "name": "yarn", + "version": ">=4", + "onFail": "ignore" + } + }, + "main": "lib/index.js", + "license": "Apache-2.0", + "homepage": "https://github.com/aws/aws-cdk", + "version": "0.0.0", + "packageManager": "yarn@4.13.0", + "types": "lib/index.d.ts", + "private": true, + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"yarn projen\"." +} diff --git a/packages/@aws-cdk/cdk-explorer/test/index.test.ts b/packages/@aws-cdk/cdk-explorer/test/index.test.ts new file mode 100644 index 000000000..fd3b743a4 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/index.test.ts @@ -0,0 +1,5 @@ +import { VERSION } from '../lib'; + +test('package loads', () => { + expect(VERSION).toBe('0.0.0'); +}); diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json new file mode 100644 index 000000000..13954c1f1 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json @@ -0,0 +1,53 @@ +// ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". +{ + "compilerOptions": { + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": false, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2020" + ], + "module": "commonjs", + "noEmitOnError": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2020", + "types": [ + "express", + "jest", + "node" + ], + "incremental": true, + "skipLibCheck": true, + "isolatedModules": true, + "composite": true, + "outDir": "lib" + }, + "include": [ + "lib/**/*.ts", + "test/**/*.ts" + ], + "exclude": [ + "node_modules" + ], + "references": [ + { + "path": "../cloud-assembly-schema" + }, + { + "path": "../cloud-assembly-api" + } + ] +} diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.json b/packages/@aws-cdk/cdk-explorer/tsconfig.json new file mode 100644 index 000000000..a017140dd --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.json @@ -0,0 +1,51 @@ +// ~~ Generated by projen. To modify, edit .projenrc.js and run "yarn projen". +{ + "compilerOptions": { + "rootDir": "lib", + "outDir": "lib", + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": false, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2020" + ], + "module": "commonjs", + "noEmitOnError": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2020", + "types": [ + "express", + "jest", + "node" + ], + "incremental": true, + "skipLibCheck": true, + "isolatedModules": true, + "composite": true + }, + "include": [ + "lib/**/*.ts" + ], + "exclude": [], + "references": [ + { + "path": "../cloud-assembly-schema" + }, + { + "path": "../cloud-assembly-api" + } + ] +} diff --git a/tsconfig.dev.json b/tsconfig.dev.json index f083e9917..341b59602 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -75,6 +75,9 @@ }, { "path": "packages/@aws-cdk-testing/cli-integ" + }, + { + "path": "packages/@aws-cdk/cdk-explorer" } ] } diff --git a/tsconfig.json b/tsconfig.json index 2977d3697..27d309c91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -72,6 +72,9 @@ }, { "path": "packages/@aws-cdk-testing/cli-integ" + }, + { + "path": "packages/@aws-cdk/cdk-explorer" } ] } diff --git a/yarn.lock b/yarn.lock index 5c5ac11ab..9a65260ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -157,6 +157,42 @@ __metadata: languageName: unknown linkType: soft +"@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer": + version: 0.0.0-use.local + resolution: "@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer" + dependencies: + "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" + "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" + "@cdklabs/eslint-plugin": "npm:^2.0.6" + "@stylistic/eslint-plugin": "npm:^3" + "@types/express": "npm:^4" + "@types/jest": "npm:^29.5.14" + "@types/node": "npm:^20" + "@typescript-eslint/eslint-plugin": "npm:^8" + "@typescript-eslint/parser": "npm:^8" + constructs: "npm:^10.0.0" + eslint: "npm:^9" + eslint-config-prettier: "npm:^10.1.8" + eslint-import-resolver-typescript: "npm:^4.4.4" + eslint-plugin-import: "npm:^2.32.0" + eslint-plugin-jest: "npm:^29.15.2" + eslint-plugin-jsdoc: "npm:^62.9.0" + eslint-plugin-prettier: "npm:^4.2.5" + express: "npm:^4" + jest: "npm:^29.7.0" + jest-junit: "npm:^16" + nx: "npm:^22.7.2" + prettier: "npm:^2.8" + projen: "npm:^0.99.63" + ts-jest: "npm:^29.4.11" + typescript: "npm:5.9" + vscode-jsonrpc: "npm:^8" + vscode-languageserver: "npm:^9" + vscode-languageserver-protocol: "npm:^3" + vscode-languageserver-textdocument: "npm:^1" + languageName: unknown + linkType: soft + "@aws-cdk/cli-plugin-contract@npm:^0.0.0, @aws-cdk/cli-plugin-contract@workspace:packages/@aws-cdk/cli-plugin-contract": version: 0.0.0-use.local resolution: "@aws-cdk/cli-plugin-contract@workspace:packages/@aws-cdk/cli-plugin-contract" @@ -6315,6 +6351,16 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:*": + version: 1.19.6 + resolution: "@types/body-parser@npm:1.19.6" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/542da05c924dce58ee23f50a8b981fee36921850c82222e384931fda3e106f750f7880c47be665217d72dbe445129049db6eb1f44e7a06b09d62af8f3cca8ea7 + languageName: node + linkType: hard + "@types/chai@npm:^5.2.2": version: 5.2.3 resolution: "@types/chai@npm:5.2.3" @@ -6325,6 +6371,15 @@ __metadata: languageName: node linkType: hard +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + "@types/cors@npm:^2.8.6": version: 2.8.19 resolution: "@types/cors@npm:2.8.19" @@ -6348,6 +6403,30 @@ __metadata: languageName: node linkType: hard +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.19.8 + resolution: "@types/express-serve-static-core@npm:4.19.8" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10c0/6fb58a85b209e0e421b29c52e0a51dbf7c039b711c604cf45d46470937a5c7c16b30aa5ce9bf7da0bd8a2e9361c95b5055599c0500a96bf4414d26c81f02d7fe + languageName: node + linkType: hard + +"@types/express@npm:^4": + version: 4.17.25 + resolution: "@types/express@npm:4.17.25" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:^1" + checksum: 10c0/f42b616d2c9dbc50352c820db7de182f64ebbfa8dba6fb6c98e5f8f0e2ef3edde0131719d9dc6874803d25ad9ca2d53471d0fec2fbc60a6003a43d015bab72c4 + languageName: node + linkType: hard + "@types/fs-extra@npm:^11": version: 11.0.4 resolution: "@types/fs-extra@npm:11.0.4" @@ -6367,6 +6446,13 @@ __metadata: languageName: node linkType: hard +"@types/http-errors@npm:*": + version: 2.0.5 + resolution: "@types/http-errors@npm:2.0.5" + checksum: 10c0/00f8140fbc504f47356512bd88e1910c2f07e04233d99c88c854b3600ce0523c8cd0ba7d1897667243282eb44c59abb9245959e2428b9de004f93937f52f7c15 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -6460,6 +6546,13 @@ __metadata: languageName: node linkType: hard +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: 10c0/c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc + languageName: node + linkType: hard + "@types/mime@npm:^2": version: 2.0.3 resolution: "@types/mime@npm:2.0.3" @@ -6547,6 +6640,20 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:*": + version: 6.15.1 + resolution: "@types/qs@npm:6.15.1" + checksum: 10c0/1dfdbcb4cf2a8f66d57f0b9a9fe6b1c7091cb816687b6698c1351eaf31f62e412cea9b7453a9637b570cd5fad8dced527e5a9e69b4fcc6e318daacd8b749f094 + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 10c0/361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c + languageName: node + linkType: hard + "@types/readdir-glob@npm:*": version: 1.1.5 resolution: "@types/readdir-glob@npm:1.1.5" @@ -6563,6 +6670,36 @@ __metadata: languageName: node linkType: hard +"@types/send@npm:*": + version: 1.2.1 + resolution: "@types/send@npm:1.2.1" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/7673747f8c2d8e67f3b1b3b57e9d4d681801a4f7b526ecf09987bb9a84a61cf94aa411c736183884dc762c1c402a61681eb1ef200d8d45d7e5ec0ab67ea5f6c1 + languageName: node + linkType: hard + +"@types/send@npm:<1": + version: 0.17.6 + resolution: "@types/send@npm:0.17.6" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 10c0/a9d76797f0637738062f1b974e0fcf3d396a28c5dc18c3f95ecec5dabda82e223afbc2d56a0bca46b6326fd7bb229979916cea40de2270a98128fd94441b87c2 + languageName: node + linkType: hard + +"@types/serve-static@npm:^1": + version: 1.15.10 + resolution: "@types/serve-static@npm:1.15.10" + dependencies: + "@types/http-errors": "npm:*" + "@types/node": "npm:*" + "@types/send": "npm:<1" + checksum: 10c0/842fca14c9e80468f89b6cea361773f2dcd685d4616a9f59013b55e1e83f536e4c93d6d8e3ba5072d40c4e7e64085210edd6646b15d538ded94512940a23021f + languageName: node + linkType: hard + "@types/sinon@npm:^17.0.3, @types/sinon@npm:^17.0.4": version: 17.0.4 resolution: "@types/sinon@npm:17.0.4" @@ -8142,6 +8279,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:~1.20.5": + version: 1.20.5 + resolution: "body-parser@npm:1.20.5" + dependencies: + bytes: "npm:~3.1.2" + content-type: "npm:~1.0.5" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:~1.2.0" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.4.24" + on-finished: "npm:~2.4.1" + qs: "npm:~6.15.1" + raw-body: "npm:~2.5.3" + type-is: "npm:~1.6.18" + unpipe: "npm:~1.0.0" + checksum: 10c0/ad777ca5e4711eae253c93f50fdc4608c60b76a9710d79e5e5b84581c76691e6ad21ecc9158986d9ea2b365df73e403ca33c27a8bccc1a7cfc2ccc248548118d + languageName: node + linkType: hard + "bowser@npm:^2.11.0": version: 2.14.1 resolution: "bowser@npm:2.14.1" @@ -10558,6 +10715,45 @@ __metadata: languageName: node linkType: hard +"express@npm:^4": + version: 4.22.2 + resolution: "express@npm:4.22.2" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:~1.20.5" + content-disposition: "npm:~0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:~0.7.1" + cookie-signature: "npm:~1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:~1.3.1" + fresh: "npm:~0.5.2" + http-errors: "npm:~2.0.0" + merge-descriptors: "npm:1.0.3" + methods: "npm:~1.1.2" + on-finished: "npm:~2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:~0.1.12" + proxy-addr: "npm:~2.0.7" + qs: "npm:~6.15.1" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:~0.19.0" + serve-static: "npm:~1.16.2" + setprototypeof: "npm:1.2.0" + statuses: "npm:~2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 10c0/d06dd4379fd217440b30f8abbe45f0e74931114c1395034f03e7d635196ecdab530d4835a1962a6aa34838d61967dc6f1f77846999bba3032373e9e714222c44 + languageName: node + linkType: hard + "express@npm:^4.14.0": version: 4.22.1 resolution: "express@npm:4.22.1" @@ -15829,6 +16025,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:~6.15.1": + version: 6.15.2 + resolution: "qs@npm:6.15.2" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/e6fd5f6f0aab06d480fe9ab15cebfc4ce4235303e2f91dc69a8f7f4df1e668a61c11d1cfbabacf4295cbbeb7b670ed23db45307480726259761f98e5695e93a7 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -18208,6 +18413,55 @@ __metadata: languageName: node linkType: hard +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: 10c0/0789c227057a844f5ead55c84679206227a639b9fb76e881185053abc4e9848aa487245966cc2393fcb342c4541241b015a1a2559fddd20ac1e68945c95344e6 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:^8": + version: 8.2.1 + resolution: "vscode-jsonrpc@npm:8.2.1" + checksum: 10c0/595e07f779112d979d9cd37a00e4b973c39da09dd7c35b149f9fb7857c87db00d0ece7772c5a6f65cc19e38afc4fc64f8130d2f0e6b5f7fc160a0fc6b94e8c07 + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:3.17.5, vscode-languageserver-protocol@npm:^3": + version: 3.17.5 + resolution: "vscode-languageserver-protocol@npm:3.17.5" + dependencies: + vscode-jsonrpc: "npm:8.2.0" + vscode-languageserver-types: "npm:3.17.5" + checksum: 10c0/5f38fd80da9868d706eaa4a025f4aff9c3faad34646bcde1426f915cbd8d7e8b6c3755ce3fef6eebd256ba3145426af1085305f8a76e34276d2e95aaf339a90b + languageName: node + linkType: hard + +"vscode-languageserver-textdocument@npm:^1": + version: 1.0.13 + resolution: "vscode-languageserver-textdocument@npm:1.0.13" + checksum: 10c0/1de174f1de3bfa9e1660a4b3c1b1c711497196d761e33f4d00785fdbfc556ae5a1f440db04771cbc0f6bd66c2a6f303062bc17d8dc46e76820e39fa2d5626564 + languageName: node + linkType: hard + +"vscode-languageserver-types@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-types@npm:3.17.5" + checksum: 10c0/1e1260de79a2cc8de3e46f2e0182cdc94a7eddab487db5a3bd4ee716f67728e685852707d72c059721ce500447be9a46764a04f0611e94e4321ffa088eef36f8 + languageName: node + linkType: hard + +"vscode-languageserver@npm:^9": + version: 9.0.1 + resolution: "vscode-languageserver@npm:9.0.1" + dependencies: + vscode-languageserver-protocol: "npm:3.17.5" + bin: + installServerIntoExtension: bin/installServerIntoExtension + checksum: 10c0/8a0838d77c98a211c76e54bd3a6249fc877e4e1a73322673fb0e921168d8e91de4f170f1d4ff7e8b6289d0698207afc6aba6662d4c1cd8e4bd7cae96afd6b0c2 + languageName: node + linkType: hard + "walk-up-path@npm:^4.0.0": version: 4.0.0 resolution: "walk-up-path@npm:4.0.0" From 32fc322fb81fb95b8e49ede76557842c31f09093 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:04:13 -0400 Subject: [PATCH 02/28] chore: lsp branch refactor (#1593) Refactor: put LSP skeleton in a unique branch from web explorer - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .projenrc.ts | 9 +- .../@aws-cdk/cdk-explorer/.projen/deps.json | 4 + .../@aws-cdk/cdk-explorer/.projen/tasks.json | 2 +- .../@aws-cdk/cdk-explorer/jest.config.json | 2 +- packages/@aws-cdk/cdk-explorer/lib/index.ts | 3 + .../@aws-cdk/cdk-explorer/lib/lsp/main.ts | 11 + .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 110 ++++++++++ packages/@aws-cdk/cdk-explorer/package.json | 3 +- .../@aws-cdk/cdk-explorer/test/index.test.ts | 5 - .../cdk-explorer/test/lsp/server.test.ts | 199 ++++++++++++++++++ .../@aws-cdk/cdk-explorer/tsconfig.dev.json | 3 + packages/@aws-cdk/cdk-explorer/tsconfig.json | 3 + yarn.lock | 1 + 13 files changed, 345 insertions(+), 10 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts delete mode 100644 packages/@aws-cdk/cdk-explorer/test/index.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index a594463b5..75a37e31d 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1671,11 +1671,12 @@ const cdkExplorer = configureProject( }), parent: repo, name: '@aws-cdk/cdk-explorer', - description: 'CDK Explorer — LSP server, synth daemon, and web interface for AWS CDK', + description: 'CDK Explorer — LSP server and web interface for AWS CDK', srcdir: 'lib', deps: [ cloudAssemblySchema.customizeReference({ versionType: 'any-future' }), cloudAssemblyApi.customizeReference({ versionType: 'exact' }), + toolkitLib.customizeReference({ versionType: 'exact' }), 'vscode-languageserver@^9', 'vscode-languageserver-textdocument@^1', 'vscode-jsonrpc@^8', @@ -1694,8 +1695,12 @@ const cdkExplorer = configureProject( jestConfig: { coverageThreshold: { statements: 80, + // The vscode-languageserver onExit handler unconditionally calls + // process.exit, which is not unit-testable and counts as one of + // very few functions in this small skeleton package. Lowered to + // 50% until the package grows; raise as more code is added. branches: 80, - functions: 80, + functions: 50, lines: 80, }, }, diff --git a/packages/@aws-cdk/cdk-explorer/.projen/deps.json b/packages/@aws-cdk/cdk-explorer/.projen/deps.json index 8917a0cfe..541e976b0 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/deps.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/deps.json @@ -111,6 +111,10 @@ "name": "@aws-cdk/cloud-assembly-schema", "type": "runtime" }, + { + "name": "@aws-cdk/toolkit-lib", + "type": "runtime" + }, { "name": "express", "version": "^4", diff --git a/packages/@aws-cdk/cdk-explorer/.projen/tasks.json b/packages/@aws-cdk/cdk-explorer/.projen/tasks.json index a066ee121..2116f760c 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/tasks.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/tasks.json @@ -81,7 +81,7 @@ "name": "gather-versions", "steps": [ { - "exec": "node -e \"require(require.resolve('cdklabs-projen-project-types/lib/yarn/gather-versions.exec.js')).cliMain()\" @aws-cdk/cloud-assembly-schema=any-future @aws-cdk/cloud-assembly-api=exact", + "exec": "node -e \"require(require.resolve('cdklabs-projen-project-types/lib/yarn/gather-versions.exec.js')).cliMain()\" @aws-cdk/cloud-assembly-schema=any-future @aws-cdk/cloud-assembly-api=exact @aws-cdk/toolkit-lib=exact", "receiveArgs": true } ] diff --git a/packages/@aws-cdk/cdk-explorer/jest.config.json b/packages/@aws-cdk/cdk-explorer/jest.config.json index ac29819e4..4f6b37284 100644 --- a/packages/@aws-cdk/cdk-explorer/jest.config.json +++ b/packages/@aws-cdk/cdk-explorer/jest.config.json @@ -10,7 +10,7 @@ "global": { "statements": 80, "branches": 80, - "functions": 80, + "functions": 50, "lines": 80 } }, diff --git a/packages/@aws-cdk/cdk-explorer/lib/index.ts b/packages/@aws-cdk/cdk-explorer/lib/index.ts index 0c18d5d94..9e4838147 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/index.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/index.ts @@ -1 +1,4 @@ export const VERSION = '0.0.0'; + +export { startServer } from './lsp/server'; +export type { LspServerOptions } from './lsp/server'; diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts new file mode 100644 index 000000000..118ea0777 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts @@ -0,0 +1,11 @@ +import { startServer } from './server'; + +try { + startServer({ + readable: process.stdin, + writable: process.stdout, + }); +} catch (err) { + process.stderr.write(`CDK LSP startup fatal: ${err}\n`); + process.exit(1); +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts new file mode 100644 index 000000000..b0e4d764d --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -0,0 +1,110 @@ +import { fileURLToPath } from 'url'; +import { + StreamMessageReader, + StreamMessageWriter, +} from 'vscode-jsonrpc/node'; +import { + createConnection, + ProposedFeatures, + TextDocumentSyncKind, + type DidSaveTextDocumentParams, + type InitializeParams, + type InitializeResult, +} from 'vscode-languageserver/node'; +/* eslint-disable import/no-relative-packages */ +import { WATCH_EXCLUDE_DEFAULTS } from '../../../toolkit-lib/lib/actions/watch/private/helpers'; +import { createIgnoreMatcher } from '../../../toolkit-lib/lib/util/glob-matcher'; + +export interface LspServerOptions { + readonly readable: NodeJS.ReadableStream; + readonly writable: NodeJS.WritableStream; + /** + * Callback invoked on `didSave` for tracked source files + */ + readonly onSynthRequest?: (projectDir: string) => void; +} + +interface LspHandlers { + onInitialize(params: InitializeParams): InitializeResult; + onInitialized(): void; + onDidSaveTextDocument(params: DidSaveTextDocumentParams): void; + onShutdown(): void; +} + +interface LogSink { + error(message: string): void; +} + +function buildHandlers(onSynthRequest: (projectDir: string) => void, log: LogSink): LspHandlers { + let applicationDir: string | undefined; + let shutdownRequested = false; + let shouldIgnore: (filePath: string) => boolean = () => false; + + return { + onInitialize(params) { + applicationDir = params.initializationOptions?.applicationDir; + return { + capabilities: { + textDocumentSync: { + openClose: false, + // No keystroke-level edits needed yet. Upgrade to Incremental when + // we need didChange to mark diagnostics as stale on edit. + change: TextDocumentSyncKind.None, + save: { includeText: false }, + }, + }, + }; + }, + onInitialized() { + const projectDir = applicationDir ?? process.cwd(); + // Same exclusion logic as toolkit-lib's watch(): + // WATCH_EXCLUDE_DEFAULTS covers common non-source dirs, then we add cdk.out + // (our own output) and dotfiles (editor configs, .git, etc.) + shouldIgnore = createIgnoreMatcher({ + exclude: [ + ...WATCH_EXCLUDE_DEFAULTS, + '**/node_modules/**', + '**/cdk.out/**', + '.*', + '**/.*', + '**/.*/**', + ], + rootDir: projectDir, + }); + }, + onDidSaveTextDocument(params) { + if (shutdownRequested) return; + const filePath = fileURLToPath(params.textDocument.uri); + if (shouldIgnore(filePath)) return; + const projectDir = applicationDir ?? process.cwd(); + try { + onSynthRequest(projectDir); + } catch (err) { + log.error(`Synth request failed: ${err instanceof Error ? err.message : String(err)}`); + } + }, + onShutdown() { + shutdownRequested = true; + }, + }; +} + +export function startServer(options: LspServerOptions): void { + const connection = createConnection( + ProposedFeatures.all, + new StreamMessageReader(options.readable), + new StreamMessageWriter(options.writable), + ); + + const onSynthRequest = options.onSynthRequest ?? (() => { + }); + const handlers = buildHandlers(onSynthRequest, connection.console); + + connection.onInitialize((params) => handlers.onInitialize(params)); + connection.onInitialized(() => handlers.onInitialized()); + connection.onDidSaveTextDocument((params) => handlers.onDidSaveTextDocument(params)); + connection.onShutdown(() => handlers.onShutdown()); + connection.onExit(() => process.exit(0)); + + connection.listen(); +} diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index 9fb88a37f..7a75aadaa 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@aws-cdk/cdk-explorer", - "description": "CDK Explorer — LSP server, synth daemon, and web interface for AWS CDK", + "description": "CDK Explorer — LSP server and web interface for AWS CDK", "repository": { "type": "git", "url": "https://github.com/aws/aws-cdk-cli", @@ -57,6 +57,7 @@ "dependencies": { "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", + "@aws-cdk/toolkit-lib": "^0.0.0", "express": "^4", "vscode-jsonrpc": "^8", "vscode-languageserver": "^9", diff --git a/packages/@aws-cdk/cdk-explorer/test/index.test.ts b/packages/@aws-cdk/cdk-explorer/test/index.test.ts deleted file mode 100644 index fd3b743a4..000000000 --- a/packages/@aws-cdk/cdk-explorer/test/index.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { VERSION } from '../lib'; - -test('package loads', () => { - expect(VERSION).toBe('0.0.0'); -}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts new file mode 100644 index 000000000..e3860667c --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -0,0 +1,199 @@ +import { PassThrough } from 'stream'; +import type { MessageConnection } from 'vscode-jsonrpc/node'; +import { + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, +} from 'vscode-jsonrpc/node'; +import { startServer, type LspServerOptions } from '../../lib/lsp/server'; + +interface TestClient { + connection: MessageConnection; + serverIn: PassThrough; + serverOut: PassThrough; +} + +function createTestClient(opts?: Partial>): TestClient { + const serverIn = new PassThrough(); + const serverOut = new PassThrough(); + + startServer({ + readable: serverIn, + writable: serverOut, + onSynthRequest: opts?.onSynthRequest, + }); + + const connection = createMessageConnection( + new StreamMessageReader(serverOut), + new StreamMessageWriter(serverIn), + ); + connection.listen(); + + return { connection, serverIn, serverOut }; +} + +async function initializeClient(client: TestClient, options?: Record): Promise { + // processId: null prevents vscode-languageserver from registering an + // undisposable parent-process watchdog timer that hangs jest. + await client.connection.sendRequest('initialize', { + processId: null, + capabilities: {}, + rootUri: null, + initializationOptions: options ?? {}, + }); + await client.connection.sendNotification('initialized'); +} + +interface LogMessage { + type: number; + message: string; +} + +interface Deferred { + promise: Promise; + resolve(value: T): void; +} + +function deferred(): Deferred { + let resolve!: (value: T) => void; + const promise = new Promise((res) => { + resolve = res; + }); + return { promise, resolve }; +} + +// didSave is a fire-and-forget notification. Tests gate on a deferred that +// the relevant callback (onSynthRequest, or window/logMessage) resolves — +// no setImmediate / setTimeout polling. +const TIMEOUT_MS = 1000; + +async function withTimeout(promise: Promise, label: string): Promise { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timed out waiting for ${label}`)), TIMEOUT_MS); + }); + try { + return await Promise.race([promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } +} + +describe('LSP Server', () => { + let testClient: TestClient; + + afterEach(() => { + if (testClient) { + testClient.connection.dispose(); + testClient.serverIn.end(); + testClient.serverOut.end(); + } + }); + + test('responds to initialize with capabilities', async () => { + testClient = createTestClient(); + + const result = await testClient.connection.sendRequest('initialize', { + processId: null, + capabilities: {}, + rootUri: null, + initializationOptions: { applicationDir: '/tmp/test-project' }, + }); + + expect(result).toMatchObject({ + capabilities: { + textDocumentSync: { + openClose: false, + change: 0, + save: { includeText: false }, + }, + }, + }); + }); + + test('didSave triggers onSynthRequest for source files', async () => { + const seen = deferred(); + testClient = createTestClient({ + onSynthRequest: (dir) => seen.resolve(dir), + }); + await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + + await testClient.connection.sendNotification('textDocument/didSave', { + textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, + }); + + await expect(withTimeout(seen.promise, 'onSynthRequest')).resolves.toBe('/tmp/test-project'); + }); + + test('didSave does not trigger for ignored files', async () => { + const synthRequests: string[] = []; + testClient = createTestClient({ + onSynthRequest: (dir) => synthRequests.push(dir), + }); + await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + + await testClient.connection.sendNotification('textDocument/didSave', { + textDocument: { uri: 'file:///tmp/test-project/node_modules/foo/index.ts' }, + }); + await testClient.connection.sendNotification('textDocument/didSave', { + textDocument: { uri: 'file:///tmp/test-project/cdk.out/tree.json' }, + }); + + // Round-trip a request to drain the server's notification queue past the + // two didSaves, then assert the synth callback was never invoked. + await testClient.connection.sendRequest('shutdown'); + expect(synthRequests).toEqual([]); + }); + + test('didSave is a no-op when onSynthRequest is not provided', async () => { + testClient = createTestClient(); + await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + + await testClient.connection.sendNotification('textDocument/didSave', { + textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, + }); + + // Server stays responsive after didSave with no callback configured. + await testClient.connection.sendRequest('shutdown'); + }); + + test('didSave is ignored after shutdown', async () => { + const synthRequests: string[] = []; + testClient = createTestClient({ + onSynthRequest: (dir) => synthRequests.push(dir), + }); + await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + + await testClient.connection.sendRequest('shutdown'); + + await testClient.connection.sendNotification('textDocument/didSave', { + textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, + }); + + // Best-effort drain: the post-shutdown didSave is a notification, no + // request to await. Re-issuing shutdown rides the same queue and acks + // only after the prior didSave has been processed. + await testClient.connection.sendRequest('shutdown'); + expect(synthRequests).toEqual([]); + }); + + test('onSynthRequest errors are surfaced via window/logMessage', async () => { + const logged = deferred(); + testClient = createTestClient({ + onSynthRequest: () => { + throw new Error('synth failed'); + }, + }); + testClient.connection.onNotification('window/logMessage', (params: LogMessage) => { + if (params.message.includes('synth failed')) logged.resolve(params); + }); + await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + + await testClient.connection.sendNotification('textDocument/didSave', { + textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, + }); + + const message = await withTimeout(logged.promise, 'window/logMessage'); + expect(message.message).toContain('Synth request failed: synth failed'); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json index 13954c1f1..f07ed00a5 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json @@ -48,6 +48,9 @@ }, { "path": "../cloud-assembly-api" + }, + { + "path": "../toolkit-lib" } ] } diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.json b/packages/@aws-cdk/cdk-explorer/tsconfig.json index a017140dd..3cb3b5bcd 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.json @@ -46,6 +46,9 @@ }, { "path": "../cloud-assembly-api" + }, + { + "path": "../toolkit-lib" } ] } diff --git a/yarn.lock b/yarn.lock index 9a65260ae..9f81f4f7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -163,6 +163,7 @@ __metadata: dependencies: "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" + "@aws-cdk/toolkit-lib": "npm:^0.0.0" "@cdklabs/eslint-plugin": "npm:^2.0.6" "@stylistic/eslint-plugin": "npm:^3" "@types/express": "npm:^4" From aae2e60e471d8c4dee83ad05fe324b7a5ab5a0f2 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:15:45 -0400 Subject: [PATCH 03/28] feat: add web server scaffold and `cdk explore` command (#1598) - Adds a minimal Express web server in `@aws-cdk/cdk-explorer` with a `/api/health` endpoint - Wires up `cdk explore` as a CLI subcommand that starts the server and blocks until SIGINT/SIGTERM - Port behavior: auto-increments from 4200 if default is taken; throws if an explicit -`-port` is unavailable Fixes # - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .projenrc.ts | 2 +- packages/@aws-cdk/cdk-explorer/lib/index.ts | 3 + .../@aws-cdk/cdk-explorer/lib/web/server.ts | 90 + .../cdk-explorer/test/web/server.test.ts | 59 + packages/aws-cdk/.projen/deps.json | 4 + packages/aws-cdk/.projen/tasks.json | 2 +- packages/aws-cdk/THIRD_PARTY_LICENSES | 2139 +++++++++++++++-- packages/aws-cdk/lib/cli/cli-config.ts | 6 + .../aws-cdk/lib/cli/cli-type-registry.json | 10 + packages/aws-cdk/lib/cli/cli.ts | 8 + .../aws-cdk/lib/cli/convert-to-user-input.ts | 10 + .../aws-cdk/lib/cli/io-host/cli-io-host.ts | 1 + .../lib/cli/parse-command-line-arguments.ts | 7 + .../aws-cdk/lib/cli/user-configuration.ts | 1 + packages/aws-cdk/lib/cli/user-input.ts | 19 + packages/aws-cdk/lib/commands/explore.ts | 25 + packages/aws-cdk/package.json | 1 + .../aws-cdk/test/cli/cli-arguments.test.ts | 1 + .../aws-cdk/test/commands/explore.test.ts | 27 + yarn.lock | 3 +- 20 files changed, 2222 insertions(+), 196 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/server.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/server.test.ts create mode 100644 packages/aws-cdk/lib/commands/explore.ts create mode 100644 packages/aws-cdk/test/commands/explore.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index 75a37e31d..83d4bb6e1 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1708,7 +1708,7 @@ const cdkExplorer = configureProject( }), ); fixupTestTask(cdkExplorer); -void cdkExplorer; +cli.deps.addDependency('@aws-cdk/cdk-explorer', pj.DependencyType.RUNTIME); // #endregion ////////////////////////////////////////////////////////////////////// diff --git a/packages/@aws-cdk/cdk-explorer/lib/index.ts b/packages/@aws-cdk/cdk-explorer/lib/index.ts index 9e4838147..1d4597848 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/index.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/index.ts @@ -2,3 +2,6 @@ export const VERSION = '0.0.0'; export { startServer } from './lsp/server'; export type { LspServerOptions } from './lsp/server'; + +export { startWebServer } from './web/server'; +export type { WebServer, WebServerOptions } from './web/server'; diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts new file mode 100644 index 000000000..338a77106 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts @@ -0,0 +1,90 @@ +import * as http from 'http'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import express = require('express'); + +export const DEFAULT_PORT = 4200; +const MAX_PORT_ATTEMPTS = 100; + +export interface WebServerOptions { + readonly port?: number; + readonly host?: string; +} + +export interface WebServer { + readonly url: string; + stop(): Promise; +} + +/** + * Starts the CDK Explorer web server. + * + * If no port is specified, auto-increments from the default until one is available. + * If a port is explicitly specified and unavailable, throws. + * + * @returns A handle to the running server with its URL and a stop function. + */ +export async function startWebServer(options: WebServerOptions = {}): Promise { + const host = options.host ?? '127.0.0.1'; + + const app = express(); + + app.get('/api/health', (_req, res) => { + res.json({ status: 'ok' }); + }); + + const server = http.createServer(app); + + const port = options.port !== undefined + ? await listenOnPort(server, host, options.port) + : await listenWithPortSearch(server, host, DEFAULT_PORT); + + let stopped = false; + return { + url: `http://${host}:${port}`, + stop: () => { + if (stopped) return Promise.resolve(); + stopped = true; + return new Promise((resolve) => { + server.close(() => resolve()); + server.closeAllConnections(); + }); + }, + }; +} + +async function listenOnPort( + server: http.Server, + host: string, + port: number, +): Promise { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(port, host, () => { + server.removeListener('error', reject); + resolve(); + }); + }); + return port; +} + +async function listenWithPortSearch( + server: http.Server, + host: string, + startPort: number, +): Promise { + for (let port = startPort; port < startPort + MAX_PORT_ATTEMPTS; port++) { + try { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(port, host, () => { + server.removeListener('error', reject); + resolve(); + }); + }); + return port; + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw err; + } + } + throw new Error(`No available port found in range ${startPort}-${startPort + MAX_PORT_ATTEMPTS - 1}`); +} diff --git a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts new file mode 100644 index 000000000..c73549ae2 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts @@ -0,0 +1,59 @@ +import { startWebServer, DEFAULT_PORT, type WebServer } from '../../lib/web/server'; + +describe('Web Server', () => { + let server: WebServer; + + afterEach(async () => { + if (server) { + await server.stop(); + } + }); + + test('starts and responds to health check', async () => { + server = await startWebServer(); + + const res = await fetch(`${server.url}/api/health`); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body).toEqual({ status: 'ok' }); + }); + + test('binds to 127.0.0.1 by default', async () => { + server = await startWebServer(); + expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); + }); + + test('auto-increments port by 1 when default is taken', async () => { + const first = await startWebServer({ port: DEFAULT_PORT }); + server = await startWebServer(); + + expect(first.url).toBe(`http://127.0.0.1:${DEFAULT_PORT}`); + expect(server.url).toBe(`http://127.0.0.1:${DEFAULT_PORT + 1}`); + await first.stop(); + }); + + test('throws when explicit port is taken', async () => { + const first = await startWebServer({ port: 4567 }); + try { + await expect(startWebServer({ port: 4567 })).rejects.toThrow(); + } finally { + await first.stop(); + } + }); + + test('stops cleanly', async () => { + server = await startWebServer(); + const url = server.url; + + await server.stop(); + + await expect(fetch(`${url}/api/health`)).rejects.toThrow(); + }); + + test('stop is idempotent', async () => { + server = await startWebServer(); + await server.stop(); + await server.stop(); + }); +}); diff --git a/packages/aws-cdk/.projen/deps.json b/packages/aws-cdk/.projen/deps.json index b64450138..abc43b704 100644 --- a/packages/aws-cdk/.projen/deps.json +++ b/packages/aws-cdk/.projen/deps.json @@ -197,6 +197,10 @@ "name": "@aws-cdk/cdk-assets-lib", "type": "runtime" }, + { + "name": "@aws-cdk/cdk-explorer", + "type": "runtime" + }, { "name": "@aws-cdk/cloud-assembly-api", "type": "runtime" diff --git a/packages/aws-cdk/.projen/tasks.json b/packages/aws-cdk/.projen/tasks.json index 4c5733c7c..eea9f033c 100644 --- a/packages/aws-cdk/.projen/tasks.json +++ b/packages/aws-cdk/.projen/tasks.json @@ -55,7 +55,7 @@ }, "steps": [ { - "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/archiver,@types/jest,@types/mockery,@types/picomatch,@types/promptly,@types/semver,@types/sinon,aws-sdk-client-mock,aws-sdk-client-mock-jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,jest-environment-node,jest-mock,license-checker,node-backpack,nx,projen,sinon,ts-jest,ts-mock-imports,ts-node,tsx,archiver,cdk-from-cfn,enquirer,fast-glob,picomatch,promptly,proxy-agent,semver" + "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/archiver,@types/jest,@types/mockery,@types/picomatch,@types/promptly,@types/semver,@types/sinon,aws-sdk-client-mock,aws-sdk-client-mock-jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,jest-environment-node,jest-mock,license-checker,node-backpack,nx,projen,sinon,ts-jest,ts-mock-imports,ts-node,tsx,@aws-cdk/cdk-explorer,archiver,cdk-from-cfn,enquirer,fast-glob,picomatch,promptly,proxy-agent,semver" } ] }, diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index 4554bfe5b..e480a38ea 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -27670,6 +27670,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** accepts@1.3.8 - https://www.npmjs.com/package/accepts/v/1.3.8 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** agent-base@7.1.4 - https://www.npmjs.com/package/agent-base/v/7.1.4 | MIT @@ -27829,6 +27857,32 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** array-flatten@1.1.1 - https://www.npmjs.com/package/array-flatten/v/1.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** ast-types@0.13.4 - https://www.npmjs.com/package/ast-types/v/0.13.4 | MIT @@ -28125,6 +28179,34 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** body-parser@1.20.5 - https://www.npmjs.com/package/body-parser/v/1.20.5 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** bowser@2.14.1 - https://www.npmjs.com/package/bowser/v/2.14.1 | MIT @@ -28245,6 +28327,86 @@ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TOR ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** bytes@3.1.2 - https://www.npmjs.com/package/bytes/v/3.1.2 | MIT +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** call-bind-apply-helpers@1.0.2 - https://www.npmjs.com/package/call-bind-apply-helpers/v/1.0.2 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** call-bound@1.0.4 - https://www.npmjs.com/package/call-bound/v/1.0.4 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** camelcase@5.3.1 - https://www.npmjs.com/package/camelcase/v/5.3.1 | MIT @@ -28374,6 +28536,93 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** content-disposition@0.5.4 - https://www.npmjs.com/package/content-disposition/v/0.5.4 | MIT +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** content-type@1.0.5 - https://www.npmjs.com/package/content-type/v/1.0.5 | MIT +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** cookie-signature@1.0.7 - https://www.npmjs.com/package/cookie-signature/v/1.0.7 | MIT + +---------------- + +** cookie@0.7.2 - https://www.npmjs.com/package/cookie/v/0.7.2 | MIT +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + ---------------- ** core-util-is@1.0.3 - https://www.npmjs.com/package/core-util-is/v/1.0.3 | MIT @@ -28656,6 +28905,30 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** debug@2.6.9 - https://www.npmjs.com/package/debug/v/2.6.9 | MIT +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + ---------------- ** debug@4.4.3 - https://www.npmjs.com/package/debug/v/4.4.3 | MIT @@ -28727,12 +29000,14 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT -Copyright Mathias Bynens +** depd@2.0.0 - https://www.npmjs.com/package/depd/v/2.0.0 | MIT +(The MIT License) + +Copyright (c) 2014-2018 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -28741,16 +29016,149 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** destroy@1.2.0 - https://www.npmjs.com/package/destroy/v/1.2.0 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** dunder-proto@1.0.1 - https://www.npmjs.com/package/dunder-proto/v/1.0.1 | MIT +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** ee-first@1.1.1 - https://www.npmjs.com/package/ee-first/v/1.1.1 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** encodeurl@2.0.0 - https://www.npmjs.com/package/encodeurl/v/2.0.0 | MIT +(The MIT License) + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- ** enquirer@2.4.1 - https://www.npmjs.com/package/enquirer/v/2.4.1 | MIT The MIT License (MIT) @@ -28776,6 +29184,113 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** es-define-property@1.0.1 - https://www.npmjs.com/package/es-define-property/v/1.0.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** es-errors@1.3.0 - https://www.npmjs.com/package/es-errors/v/1.3.0 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** es-object-atoms@1.1.1 - https://www.npmjs.com/package/es-object-atoms/v/1.1.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** escape-html@1.0.3 - https://www.npmjs.com/package/escape-html/v/1.0.3 | MIT +(The MIT License) + +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** escodegen@2.1.0 - https://www.npmjs.com/package/escodegen/v/2.1.0 | BSD-2-Clause @@ -28876,6 +29391,33 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +---------------- + +** etag@1.8.1 - https://www.npmjs.com/package/etag/v/1.8.1 | MIT +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** event-target-shim@5.0.1 - https://www.npmjs.com/package/event-target-shim/v/5.0.1 | MIT @@ -29137,8 +29679,37 @@ SOFTWARE. ---------------- -** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT -MIT License +** express@4.22.2 - https://www.npmjs.com/package/express/v/4.22.2 | MIT +(The MIT License) + +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT +MIT License Copyright (c) 2017 Evgeny Poberezkin @@ -29215,36 +29786,435 @@ SOFTWARE. ---------------- -** fast-xml-parser@5.7.1 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.1 | MIT +** fast-xml-parser@5.7.1 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.1 | MIT +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fast-xml-parser@5.7.2 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.2 | MIT +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fast-xml-parser@5.7.3 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.3 | MIT +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fastq@1.20.1 - https://www.npmjs.com/package/fastq/v/1.20.1 | ISC +Copyright (c) 2015-2020, Matteo Collina + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** fill-range@7.1.1 - https://www.npmjs.com/package/fill-range/v/7.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** finalhandler@1.3.2 - https://www.npmjs.com/package/finalhandler/v/1.3.2 | MIT +(The MIT License) + +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** find-up@4.1.0 - https://www.npmjs.com/package/find-up/v/4.1.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** forwarded@0.2.0 - https://www.npmjs.com/package/forwarded/v/0.2.0 | MIT +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fresh@0.5.2 - https://www.npmjs.com/package/fresh/v/0.5.2 | MIT +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fs-extra@11.3.4 - https://www.npmjs.com/package/fs-extra/v/11.3.4 | MIT +(The MIT License) + +Copyright (c) 2011-2024 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** function-bind@1.1.2 - https://www.npmjs.com/package/function-bind/v/1.1.2 | MIT +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +---------------- + +** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC + +---------------- + +** get-intrinsic@1.3.0 - https://www.npmjs.com/package/get-intrinsic/v/1.3.0 | MIT +MIT License + +Copyright (c) 2020 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** get-proto@1.0.1 - https://www.npmjs.com/package/get-proto/v/1.0.1 | MIT +MIT License + +Copyright (c) 2025 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** get-uri@6.0.5 - https://www.npmjs.com/package/get-uri/v/6.0.5 | MIT +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** glob-parent@5.1.2 - https://www.npmjs.com/package/glob-parent/v/5.1.2 | ISC +The ISC License + +Copyright (c) 2015, 2019 Elan Shanker + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** gopd@1.2.0 - https://www.npmjs.com/package/gopd/v/1.2.0 | MIT +MIT License + +Copyright (c) 2022 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** graceful-fs@4.2.11 - https://www.npmjs.com/package/graceful-fs/v/4.2.11 | ISC +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT MIT License -Copyright (c) 2017 Amit Kumar Gupta +Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** fast-xml-parser@5.7.2 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.2 | MIT +** has-symbols@1.1.0 - https://www.npmjs.com/package/has-symbols/v/1.1.0 | MIT MIT License -Copyright (c) 2017 Amit Kumar Gupta +Copyright (c) 2016 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29267,10 +30237,10 @@ SOFTWARE. ---------------- -** fast-xml-parser@5.7.3 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.3 | MIT +** hasown@2.0.2 - https://www.npmjs.com/package/hasown/v/2.0.2 | MIT MIT License -Copyright (c) 2017 Amit Kumar Gupta +Copyright (c) Jordan Harband and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29293,28 +30263,12 @@ SOFTWARE. ---------------- -** fastq@1.20.1 - https://www.npmjs.com/package/fastq/v/1.20.1 | ISC -Copyright (c) 2015-2020, Matteo Collina - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ----------------- +** http-errors@2.0.1 - https://www.npmjs.com/package/http-errors/v/2.0.1 | MIT -** fill-range@7.1.1 - https://www.npmjs.com/package/fill-range/v/7.1.1 | MIT The MIT License (MIT) -Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29337,48 +30291,10 @@ THE SOFTWARE. ---------------- -** find-up@4.1.0 - https://www.npmjs.com/package/find-up/v/4.1.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** fs-extra@11.3.4 - https://www.npmjs.com/package/fs-extra/v/11.3.4 | MIT -(The MIT License) - -Copyright (c) 2011-2024 JP Richardson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC - ----------------- - -** get-uri@6.0.5 - https://www.npmjs.com/package/get-uri/v/6.0.5 | MIT +** http-proxy-agent@7.0.2 - https://www.npmjs.com/package/http-proxy-agent/v/7.0.2 | MIT (The MIT License) -Copyright (c) 2014 Nathan Rajlich +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -29399,63 +30315,10 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** glob-parent@5.1.2 - https://www.npmjs.com/package/glob-parent/v/5.1.2 | ISC -The ISC License - -Copyright (c) 2015, 2019 Elan Shanker - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ----------------- - -** graceful-fs@4.2.11 - https://www.npmjs.com/package/graceful-fs/v/4.2.11 | ISC -The ISC License - -Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ----------------- - -** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------- -** http-proxy-agent@7.0.2 - https://www.npmjs.com/package/http-proxy-agent/v/7.0.2 | MIT +** https-proxy-agent@7.0.6 - https://www.npmjs.com/package/https-proxy-agent/v/7.0.6 | MIT (The MIT License) Copyright (c) 2013 Nathan Rajlich @@ -29479,17 +30342,14 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------- -** https-proxy-agent@7.0.6 - https://www.npmjs.com/package/https-proxy-agent/v/7.0.6 | MIT -(The MIT License) - -Copyright (c) 2013 Nathan Rajlich +** iconv-lite@0.4.24 - https://www.npmjs.com/package/iconv-lite/v/0.4.24 | MIT +Copyright (c) 2011 Alexander Shtuchkin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including +"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -29498,13 +30358,15 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- @@ -29551,6 +30413,30 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** ipaddr.js@1.9.1 - https://www.npmjs.com/package/ipaddr.js/v/1.9.1 | MIT +Copyright (C) 2011-2017 whitequark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** is-extglob@2.1.1 - https://www.npmjs.com/package/is-extglob/v/2.1.1 | MIT @@ -29873,6 +30759,87 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** math-intrinsics@1.1.0 - https://www.npmjs.com/package/math-intrinsics/v/1.1.0 | MIT +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** media-typer@0.3.0 - https://www.npmjs.com/package/media-typer/v/0.3.0 | MIT +(The MIT License) + +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** merge-descriptors@1.0.3 - https://www.npmjs.com/package/merge-descriptors/v/1.0.3 | MIT +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** merge2@1.4.1 - https://www.npmjs.com/package/merge2/v/1.4.1 | MIT @@ -29899,6 +30866,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** methods@1.1.2 - https://www.npmjs.com/package/methods/v/1.1.2 | MIT +(The MIT License) + +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + ---------------- ** micromatch@4.0.8 - https://www.npmjs.com/package/micromatch/v/4.0.8 | MIT @@ -29925,6 +30921,88 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** mime-db@1.52.0 - https://www.npmjs.com/package/mime-db/v/1.52.0 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** mime-types@2.1.35 - https://www.npmjs.com/package/mime-types/v/2.1.35 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** mime@1.6.0 - https://www.npmjs.com/package/mime/v/1.6.0 | MIT +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** mime@2.6.0 - https://www.npmjs.com/package/mime/v/2.6.0 | MIT @@ -29971,6 +31049,10 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** ms@2.0.0 - https://www.npmjs.com/package/ms/v/2.0.0 | MIT + ---------------- ** ms@2.1.3 - https://www.npmjs.com/package/ms/v/2.1.3 | MIT @@ -29995,6 +31077,35 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** negotiator@0.6.3 - https://www.npmjs.com/package/negotiator/v/0.6.3 | MIT +(The MIT License) + +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** netmask@2.1.1 - https://www.npmjs.com/package/netmask/v/2.1.1 | MIT @@ -30025,6 +31136,60 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** object-inspect@1.13.4 - https://www.npmjs.com/package/object-inspect/v/1.13.4 | MIT +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** on-finished@2.4.1 - https://www.npmjs.com/package/on-finished/v/2.4.1 | MIT +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** p-finally@1.0.0 - https://www.npmjs.com/package/p-finally/v/1.0.0 | MIT @@ -30188,6 +31353,35 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** parseurl@1.3.3 - https://www.npmjs.com/package/parseurl/v/1.3.3 | MIT + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** path-exists@4.0.0 - https://www.npmjs.com/package/path-exists/v/4.0.0 | MIT @@ -30202,6 +31396,32 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** path-to-regexp@0.1.13 - https://www.npmjs.com/package/path-to-regexp/v/0.1.13 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** picomatch@2.3.2 - https://www.npmjs.com/package/picomatch/v/2.3.2 | MIT @@ -30299,16 +31519,43 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** proxy-addr@2.0.7 - https://www.npmjs.com/package/proxy-addr/v/2.0.7 | MIT +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- @@ -30362,6 +31609,10 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** qs@6.15.2 - https://www.npmjs.com/package/qs/v/6.15.2 | BSD-3-Clause + ---------------- ** queue-microtask@1.2.3 - https://www.npmjs.com/package/queue-microtask/v/1.2.3 | MIT @@ -30387,6 +31638,61 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** range-parser@1.2.1 - https://www.npmjs.com/package/range-parser/v/1.2.1 | MIT +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** read@1.0.7 - https://www.npmjs.com/package/read/v/1.0.7 | ISC @@ -30866,6 +32172,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** safer-buffer@2.1.2 - https://www.npmjs.com/package/safer-buffer/v/2.1.2 | MIT +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** semver@7.8.0 - https://www.npmjs.com/package/semver/v/7.8.0 | ISC @@ -30873,36 +32205,216 @@ The ISC License Copyright (c) Isaac Z. Schlueter and Contributors -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** send@0.19.2 - https://www.npmjs.com/package/send/v/0.19.2 | MIT +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** serve-static@1.16.3 - https://www.npmjs.com/package/serve-static/v/1.16.3 | MIT +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** set-blocking@2.0.0 - https://www.npmjs.com/package/set-blocking/v/2.0.0 | ISC +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** setprototypeof@1.2.0 - https://www.npmjs.com/package/setprototypeof/v/1.2.0 | ISC +Copyright (c) 2015, Wes Todd + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** side-channel-list@1.0.1 - https://www.npmjs.com/package/side-channel-list/v/1.0.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** side-channel-map@1.0.1 - https://www.npmjs.com/package/side-channel-map/v/1.0.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** side-channel-weakmap@1.0.2 - https://www.npmjs.com/package/side-channel-weakmap/v/1.0.2 | MIT +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- +** side-channel@1.1.0 - https://www.npmjs.com/package/side-channel/v/1.1.0 | MIT +MIT License ----------------- +Copyright (c) 2019 Jordan Harband -** set-blocking@2.0.0 - https://www.npmjs.com/package/set-blocking/v/2.0.0 | ISC -Copyright (c) 2016, Contributors +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Permission to use, copy, modify, and/or distribute this software -for any purpose with or without fee is hereby granted, provided -that the above copyright notice and this permission notice -appear in all copies. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE -LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- @@ -31047,6 +32559,34 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** statuses@2.0.2 - https://www.npmjs.com/package/statuses/v/2.0.2 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** streamx@2.25.0 - https://www.npmjs.com/package/streamx/v/2.25.0 | MIT @@ -31507,6 +33047,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** toidentifier@1.0.1 - https://www.npmjs.com/package/toidentifier/v/1.0.1 | MIT +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** tslib@2.8.1 - https://www.npmjs.com/package/tslib/v/2.8.1 | 0BSD @@ -31523,6 +33089,34 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** type-is@1.6.18 - https://www.npmjs.com/package/type-is/v/1.6.18 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** universalify@2.0.1 - https://www.npmjs.com/package/universalify/v/2.0.1 | MIT @@ -31548,6 +33142,33 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** unpipe@1.0.0 - https://www.npmjs.com/package/unpipe/v/1.0.0 | MIT +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** util-deprecate@1.0.2 - https://www.npmjs.com/package/util-deprecate/v/1.0.2 | MIT @@ -31577,6 +33198,138 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** utils-merge@1.0.1 - https://www.npmjs.com/package/utils-merge/v/1.0.1 | MIT +The MIT License (MIT) + +Copyright (c) 2013-2017 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vary@1.1.2 - https://www.npmjs.com/package/vary/v/1.1.2 | MIT +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-jsonrpc@8.2.0 - https://www.npmjs.com/package/vscode-jsonrpc/v/8.2.0 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-jsonrpc@8.2.1 - https://www.npmjs.com/package/vscode-jsonrpc/v/8.2.1 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver-protocol@3.17.5 - https://www.npmjs.com/package/vscode-languageserver-protocol/v/3.17.5 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver-types@3.17.5 - https://www.npmjs.com/package/vscode-languageserver-types/v/3.17.5 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver@9.0.1 - https://www.npmjs.com/package/vscode-languageserver/v/9.0.1 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** which-module@2.0.1 - https://www.npmjs.com/package/which-module/v/2.0.1 | ISC diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index ffb8f0119..1119930a8 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -512,6 +512,12 @@ export async function makeConfig(): Promise { 'doctor': { description: 'Check your set-up for potential problems', }, + 'explore': { + description: 'Opens an interactive web explorer for your CDK app', + options: { + port: { type: 'number', desc: 'Port to bind the web server on', default: 4200 }, + }, + }, 'orphan': { arg: { name: 'PATHS', diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 97aeea0b5..29fc59abc 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -1121,6 +1121,16 @@ "doctor": { "description": "Check your set-up for potential problems" }, + "explore": { + "description": "Opens an interactive web explorer for your CDK app", + "options": { + "port": { + "type": "number", + "desc": "Port to bind the web server on", + "default": 4200 + } + } + }, "orphan": { "arg": { "name": "PATHS", diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index f406498e0..c036483d1 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -32,6 +32,7 @@ import type { Settings } from '../api/settings'; import { contextHandler as context } from '../commands/context'; import { docs } from '../commands/docs'; import { doctor } from '../commands/doctor'; +import { explore } from '../commands/explore'; import { FlagCommandHandler } from '../commands/flags/flags'; import { cliInit, printAvailableTemplates } from '../commands/init'; import { getLanguageFromAlias } from '../commands/language'; @@ -318,6 +319,13 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { }), ) .command('doctor', 'Check your set-up for potential problems') + .command('explore', 'Opens an interactive web explorer for your CDK app', (yargs: Argv) => + yargs.option('port', { + default: 4200, + type: 'number', + desc: 'Port to bind the web server on', + }), + ) .command('orphan [PATHS..]', 'Detach resources from a CloudFormation stack without deleting them', (yargs: Argv) => yargs) .command('refactor [STACKS..]', 'Moves resources between stacks or within the same stack', (yargs: Argv) => yargs diff --git a/packages/aws-cdk/lib/cli/user-configuration.ts b/packages/aws-cdk/lib/cli/user-configuration.ts index 055d8bc0a..bb211f391 100644 --- a/packages/aws-cdk/lib/cli/user-configuration.ts +++ b/packages/aws-cdk/lib/cli/user-configuration.ts @@ -38,6 +38,7 @@ export enum Command { DOCS = 'docs', DOC = 'doc', DOCTOR = 'doctor', + EXPLORE = 'explore', ORPHAN = 'orphan', REFACTOR = 'refactor', DRIFT = 'drift', diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 08d103b42..837c9a970 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -139,6 +139,11 @@ export interface UserInput { */ readonly doctor?: {}; + /** + * Opens an interactive web explorer for your CDK app + */ + readonly explore?: ExploreOptions; + /** * Detach resources from a CloudFormation stack without deleting them */ @@ -1710,6 +1715,20 @@ export interface DocsOptions { readonly browser?: string; } +/** + * Opens an interactive web explorer for your CDK app + * + * @struct + */ +export interface ExploreOptions { + /** + * Port to bind the web server on + * + * @default - 4200 + */ + readonly port?: number; +} + /** * Detach resources from a CloudFormation stack without deleting them * diff --git a/packages/aws-cdk/lib/commands/explore.ts b/packages/aws-cdk/lib/commands/explore.ts new file mode 100644 index 000000000..5dc63f616 --- /dev/null +++ b/packages/aws-cdk/lib/commands/explore.ts @@ -0,0 +1,25 @@ +import { startWebServer } from '@aws-cdk/cdk-explorer'; +import type { IoHelper } from '../api-private'; + +export interface ExploreOptions { + readonly ioHelper: IoHelper; + readonly port?: number; +} + +export async function explore(options: ExploreOptions): Promise { + const server = await startWebServer({ port: options.port }); + await options.ioHelper.defaults.info(`CDK Explorer running at ${server.url}`); + + await new Promise((resolve) => { + const onSignal = () => { + process.removeListener('SIGINT', onSignal); + process.removeListener('SIGTERM', onSignal); + resolve(); + }; + process.once('SIGINT', onSignal); + process.once('SIGTERM', onSignal); + }); + + await server.stop(); + return 0; +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 86b637dda..0fc25c8fd 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -84,6 +84,7 @@ }, "dependencies": { "@aws-cdk/cdk-assets-lib": "^0.0.0", + "@aws-cdk/cdk-explorer": "^0.0.0", "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", diff --git a/packages/aws-cdk/test/cli/cli-arguments.test.ts b/packages/aws-cdk/test/cli/cli-arguments.test.ts index 5f81b67c4..920ec37ce 100644 --- a/packages/aws-cdk/test/cli/cli-arguments.test.ts +++ b/packages/aws-cdk/test/cli/cli-arguments.test.ts @@ -166,6 +166,7 @@ describe('config', () => { gc: expect.anything(), flags: expect.anything(), doctor: expect.anything(), + explore: expect.anything(), docs: expect.anything(), orphan: expect.anything(), refactor: expect.anything(), diff --git a/packages/aws-cdk/test/commands/explore.test.ts b/packages/aws-cdk/test/commands/explore.test.ts new file mode 100644 index 000000000..37eb43f80 --- /dev/null +++ b/packages/aws-cdk/test/commands/explore.test.ts @@ -0,0 +1,27 @@ +import { explore } from '../../lib/commands/explore'; + +describe('explore command', () => { + test('starts server and prints URL', async () => { + const messages: string[] = []; + const fakeIoHelper = { + defaults: { + info: async (msg: string) => { + messages.push(msg); + }, + }, + }; + + // Run explore in background, then immediately send SIGINT to unblock it + const resultPromise = explore({ ioHelper: fakeIoHelper as any }); + + // Give the server time to start, then signal exit + await new Promise((r) => setTimeout(r, 100)); + process.emit('SIGINT', 'SIGINT'); + + const exitCode = await resultPromise; + + expect(exitCode).toBe(0); + expect(messages).toHaveLength(1); + expect(messages[0]).toMatch(/CDK Explorer running at http:\/\/127\.0\.0\.1:\d+/); + }); +}); diff --git a/yarn.lock b/yarn.lock index 9f81f4f7c..7a2eece9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -157,7 +157,7 @@ __metadata: languageName: unknown linkType: soft -"@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer": +"@aws-cdk/cdk-explorer@npm:^0.0.0, @aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer": version: 0.0.0-use.local resolution: "@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer" dependencies: @@ -7841,6 +7841,7 @@ __metadata: resolution: "aws-cdk@workspace:packages/aws-cdk" dependencies: "@aws-cdk/cdk-assets-lib": "npm:^0.0.0" + "@aws-cdk/cdk-explorer": "npm:^0.0.0" "@aws-cdk/cli-plugin-contract": "npm:^0.0.0" "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" From fc29374e6409365ff124ee076c3ba5520f4a8cb3 Mon Sep 17 00:00:00 2001 From: megha-narayanan Date: Mon, 8 Jun 2026 09:57:27 -0400 Subject: [PATCH 04/28] chore: regenerate THIRD_PARTY_LICENSES after merging cdk-lsp --- packages/aws-cdk/THIRD_PARTY_LICENSES | 426 +++++++++++++++++++++++++- 1 file changed, 419 insertions(+), 7 deletions(-) diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index e480a38ea..0a3a5dc06 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -9682,7 +9682,7 @@ Apache License ---------------- -** @aws-sdk/lib-storage@3.1036.0 - https://www.npmjs.com/package/@aws-sdk/lib-storage/v/3.1036.0 | Apache-2.0 +** @aws-sdk/lib-storage@3.1058.0 - https://www.npmjs.com/package/@aws-sdk/lib-storage/v/3.1058.0 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -16530,7 +16530,7 @@ Apache License ---------------- -** @smithy/core@3.24.0 - https://www.npmjs.com/package/@smithy/core/v/3.24.0 | Apache-2.0 +** @smithy/core@3.24.2 - https://www.npmjs.com/package/@smithy/core/v/3.24.2 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -16736,7 +16736,7 @@ Apache License ---------------- -** @smithy/core@3.24.2 - https://www.npmjs.com/package/@smithy/core/v/3.24.2 | Apache-2.0 +** @smithy/core@3.24.3 - https://www.npmjs.com/package/@smithy/core/v/3.24.3 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -16942,7 +16942,213 @@ Apache License ---------------- -** @smithy/core@3.24.3 - https://www.npmjs.com/package/@smithy/core/v/3.24.3 | Apache-2.0 +** @smithy/core@3.24.5 - https://www.npmjs.com/package/@smithy/core/v/3.24.5 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---------------- + +** @smithy/core@3.24.6 - https://www.npmjs.com/package/@smithy/core/v/3.24.6 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -23721,6 +23927,212 @@ Apache License limitations under the License. +---------------- + +** @smithy/types@4.14.3 - https://www.npmjs.com/package/@smithy/types/v/4.14.3 | Apache-2.0 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ---------------- ** @smithy/url-parser@4.2.14 - https://www.npmjs.com/package/@smithy/url-parser/v/4.2.14 | Apache-2.0 @@ -25980,7 +26392,7 @@ Apache License ---------------- -** @smithy/util-retry@4.4.0 - https://www.npmjs.com/package/@smithy/util-retry/v/4.4.0 | Apache-2.0 +** @smithy/util-retry@4.4.6 - https://www.npmjs.com/package/@smithy/util-retry/v/4.4.6 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -28437,7 +28849,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** cdk-from-cfn@0.300.0 - https://www.npmjs.com/package/cdk-from-cfn/v/0.300.0 | MIT OR Apache-2.0 +** cdk-from-cfn@0.302.0 - https://www.npmjs.com/package/cdk-from-cfn/v/0.302.0 | MIT OR Apache-2.0 ---------------- @@ -32200,7 +32612,7 @@ SOFTWARE. ---------------- -** semver@7.8.0 - https://www.npmjs.com/package/semver/v/7.8.0 | ISC +** semver@7.8.1 - https://www.npmjs.com/package/semver/v/7.8.1 | ISC The ISC License Copyright (c) Isaac Z. Schlueter and Contributors From a9ece95e909b2730bb539d531de2b9de528138c3 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:20:02 -0400 Subject: [PATCH 05/28] feat: surface validation violations and CFN resources in LSP (#1592) - Adds an assembly reader that joins `cdk.out/`'s `tree.json` with each stack's manifest metadata into a `ConstructNode` tree carrying `logicalId`, CFN type, and source location. - LSP publishes CDK validation violations as `Diagnostic`s anchored to the construct's TypeScript source line, with rule-level severity. - LSP serves `CodeLens` entries above each construct creation site summarising the CFN resources it produces. - Source resolution covers `.ts` and `.js` (with sibling `.js.map`); non-TS apps degrade gracefully (no crash, no source-linked features). Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 9 +- .../@aws-cdk/cdk-explorer/.projen/deps.json | 15 + .../@aws-cdk/cdk-explorer/jest.config.json | 2 +- .../cdk-explorer/lib/core/assembly-reader.ts | 81 ++++ .../cdk-explorer/lib/core/source-resolver.ts | 139 ++++++ packages/@aws-cdk/cdk-explorer/lib/index.ts | 8 +- .../@aws-cdk/cdk-explorer/lib/lsp/codelens.ts | 80 ++++ .../cdk-explorer/lib/lsp/diagnostics.ts | 142 ++++++ .../@aws-cdk/cdk-explorer/lib/lsp/main.ts | 3 +- .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 108 ++++- packages/@aws-cdk/cdk-explorer/package.json | 3 + .../cdk-explorer/test/_fixtures/README.md | 33 ++ .../test/_fixtures/builders.test.ts | 227 ++++++++++ .../cdk-explorer/test/_fixtures/builders.ts | 404 ++++++++++++++++++ .../test/_fixtures/source-maps/sample.js | 8 + .../test/_fixtures/source-maps/sample.js.map | 1 + .../test/_fixtures/source-maps/sample.ts | 4 + .../test/core/assembly-reader.test.ts | 257 +++++++++++ .../test/core/source-resolver.test.ts | 124 ++++++ .../cdk-explorer/test/lsp/codelens.test.ts | 156 +++++++ .../cdk-explorer/test/lsp/diagnostics.test.ts | 177 ++++++++ .../cdk-explorer/test/lsp/server.test.ts | 272 ++++++------ .../@aws-cdk/cdk-explorer/tsconfig.dev.json | 1 + packages/@aws-cdk/cdk-explorer/tsconfig.json | 1 + .../cloud-assembly-api/lib/construct-tree.ts | 193 +++++++++ .../@aws-cdk/cloud-assembly-api/lib/index.ts | 1 + .../test/construct-tree.test.ts | 154 +++++++ .../lib/cloud-assembly/metadata-schema.ts | 7 + .../cloud-assembly-schema/lib/manifest.ts | 3 + .../lib/api/source-tracing/index.ts | 1 + .../private/stack-source-tracing.ts | 8 +- yarn.lock | 14 +- 32 files changed, 2491 insertions(+), 145 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/_fixtures/README.md create mode 100644 packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js create mode 100644 packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js.map create mode 100644 packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts create mode 100644 packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts create mode 100644 packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index 9be2bd57c..d600805f4 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1686,10 +1686,13 @@ const cdkExplorer = configureProject( 'vscode-languageserver-textdocument@^1', 'vscode-jsonrpc@^8', 'express@^4', + '@jridgewell/trace-mapping@^0.3', + 'convert-source-map@^2', ], devDeps: [ 'vscode-languageserver-protocol@^3', '@types/express@^4', + '@types/convert-source-map@^2', ], tsconfig: { compilerOptions: { @@ -1700,12 +1703,8 @@ const cdkExplorer = configureProject( jestConfig: { coverageThreshold: { statements: 80, - // The vscode-languageserver onExit handler unconditionally calls - // process.exit, which is not unit-testable and counts as one of - // very few functions in this small skeleton package. Lowered to - // 50% until the package grows; raise as more code is added. branches: 80, - functions: 50, + functions: 80, lines: 80, }, }, diff --git a/packages/@aws-cdk/cdk-explorer/.projen/deps.json b/packages/@aws-cdk/cdk-explorer/.projen/deps.json index 541e976b0..2c7be48f1 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/deps.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/deps.json @@ -9,6 +9,11 @@ "version": "^3", "type": "build" }, + { + "name": "@types/convert-source-map", + "version": "^2", + "type": "build" + }, { "name": "@types/express", "version": "^4", @@ -115,6 +120,16 @@ "name": "@aws-cdk/toolkit-lib", "type": "runtime" }, + { + "name": "@jridgewell/trace-mapping", + "version": "^0.3", + "type": "runtime" + }, + { + "name": "convert-source-map", + "version": "^2", + "type": "runtime" + }, { "name": "express", "version": "^4", diff --git a/packages/@aws-cdk/cdk-explorer/jest.config.json b/packages/@aws-cdk/cdk-explorer/jest.config.json index 4f6b37284..ac29819e4 100644 --- a/packages/@aws-cdk/cdk-explorer/jest.config.json +++ b/packages/@aws-cdk/cdk-explorer/jest.config.json @@ -10,7 +10,7 @@ "global": { "statements": 80, "branches": 80, - "functions": 50, + "functions": 80, "lines": 80 } }, diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts new file mode 100644 index 000000000..452267218 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts @@ -0,0 +1,81 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { buildConstructTree, CloudAssembly, type ConstructTreeNode } from '@aws-cdk/cloud-assembly-api'; +import { VALIDATION_REPORT_FILE, type PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; +import { findCreationStackTrace } from '@aws-cdk/toolkit-lib'; +import { SourceMapResolver, type SourceLocation } from './source-resolver'; + +/** + * A construct from the cloud assembly, decorated with the user source location + * where it was created. Extends the generic tree node from cloud-assembly-api + * with the source-map-resolved location the LSP/explorer surfaces. + */ +export interface ConstructNode extends ConstructTreeNode { + /** User source location where the construct was created; undefined for non-TS apps. */ + readonly sourceLocation?: SourceLocation; + readonly children: readonly ConstructNode[]; +} + +export interface AssemblyData { + readonly tree: readonly ConstructNode[]; + readonly violations?: PolicyValidationReportJson; + /** Set when validation-report.json fails to load. The tree still loads. */ + readonly violationsError?: string; + /** Non-fatal warnings collected while reading the assembly (e.g. unparseable source maps). */ + readonly warnings: readonly string[]; +} + +export type AssemblyReadResult = + | { readonly status: 'success'; readonly data: AssemblyData } + | { readonly status: 'not-found' } + | { readonly status: 'error'; readonly message: string }; + +/** + * Decorates the cloud assembly's construct tree with the source location of + * each node and attaches any policy-validation violations. Tree construction + * (tree.json + stack-metadata join) is delegated to buildConstructTree. + */ +export function readAssembly(assemblyDir: string): AssemblyReadResult { + const manifestPath = path.join(assemblyDir, 'manifest.json'); + if (!fs.existsSync(manifestPath)) { + return { status: 'not-found' }; + } + + try { + const assembly = new CloudAssembly(assemblyDir); + // One resolver per readAssembly call: caches parsed source maps across + // constructs, scoped so a fresh synth observes any moved/edited maps. + const sourceResolver = new SourceMapResolver(); + const tree = buildConstructTree(assembly, (fields, stack, constructPath) => ({ + ...fields, + sourceLocation: stack + ? sourceResolver.resolveFrames(findCreationStackTrace(stack, constructPath)) + : undefined, + })); + + let violations: PolicyValidationReportJson | undefined; + let violationsError: string | undefined; + try { + violations = loadViolations(assemblyDir); + } catch (err) { + violationsError = (err as Error).message; + } + + return { + status: 'success', + data: { tree, violations, violationsError, warnings: sourceResolver.warnings }, + }; + } catch (err) { + return { status: 'error', message: (err as Error).message }; + } +} + +/** Loads the policy-validation report from the assembly dir, if present. */ +function loadViolations(assemblyDir: string): PolicyValidationReportJson | undefined { + const reportPath = path.join(assemblyDir, VALIDATION_REPORT_FILE); + if (!fs.existsSync(reportPath)) return undefined; + // Read as data, not via Manifest.loadValidationReport: that loader's version-compat + // check throws on older aws-cdk-lib reports that omit `version`. We only consume + // pluginReports, which are version-independent across producer versions. + return JSON.parse(fs.readFileSync(reportPath, 'utf-8')) as PolicyValidationReportJson; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts b/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts new file mode 100644 index 000000000..4290f5160 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts @@ -0,0 +1,139 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath, pathToFileURL } from 'url'; +import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping'; +import * as convertSourceMap from 'convert-source-map'; + +/** + * Resolved source location for a construct, in user-space coordinates. + * Always 1-based line and column. Resolves to .ts when a sibling source + * map exists; otherwise to the .js (or whatever the trace produced). + */ +export interface SourceLocation { + readonly file: string; + readonly line: number; + readonly column: number; +} + +/** + * Resolves construct creation stack traces to user source locations, caching + * parsed source maps across calls. Source-map parsing is not free and one + * cdk.out tree asks for the same .js.map many times, so create one resolver + * per assembly read and reuse it for every construct. + */ +export class SourceMapResolver { + // Parsed source maps keyed by absolute .js path; null = known to have no map. + private readonly cache = new Map(); + private readonly collectedWarnings: string[] = []; + + /** Non-fatal warnings collected during resolution (e.g. an unparseable .js.map). */ + public get warnings(): readonly string[] { + return this.collectedWarnings; + } + + /** + * Resolve the user source location from a creation stack trace (the frames + * produced by toolkit-lib's findCreationStackTrace). Returns undefined when + * there's no trace (non-TS apps) or every frame is a skip-placeholder + * (framework-only call sites). + */ + public resolveFrames(frames: readonly string[] | undefined): SourceLocation | undefined { + if (!frames) return undefined; + + // aws-cdk-lib's renderCallStackJustMyCode (in node_modules/aws-cdk-lib/core/ + // lib/stack-trace.js) pre-filters node_modules/node:internal frames into + // skip-placeholder lines. Those don't match FRAME_RE, so the first frame + // that parses IS the user call site. + for (const frame of frames) { + const parsed = parseFrame(frame); + if (!parsed) continue; + if (!isSupportedSourceFile(parsed.file)) return undefined; + return this.mapJsToOriginalSource(parsed); + } + return undefined; + } + + /** + * Map a .js location to its original .ts via a sibling .js.map. Returns the + * input location unchanged when it isn't a .js file or has no usable map. + */ + private mapJsToOriginalSource(loc: SourceLocation): SourceLocation { + // Input is allow-listed to .ts/.tsx/.js by resolveFrames; .ts/.tsx are + // already source-space, only .js needs mapping back to its original. + if (!loc.file.endsWith('.js')) return loc; + + const tracer = this.loadTraceMap(loc.file); + if (!tracer) return loc; + + // trace-mapping uses 0-based columns, stack frames are 1-based. + const orig = originalPositionFor(tracer, { line: loc.line, column: loc.column - 1 }); + if (!orig.source || orig.line == null || orig.column == null) return loc; + + // orig.source is already resolved against the map's location (we pass the + // map URL when building the TraceMap), with any sourceRoot applied — so it's + // a file:// URL for local maps. Convert it back to a path. + const file = orig.source.startsWith('file://') ? fileURLToPath(orig.source) : orig.source; + return { file, line: orig.line, column: orig.column + 1 }; + } + + private loadTraceMap(jsFile: string): TraceMap | null { + const cached = this.cache.get(jsFile); + if (cached !== undefined) return cached; + + const tracer = this.readSourceMap(jsFile); + this.cache.set(jsFile, tracer); + return tracer; + } + + /** + * Load a .js file's source map. Honors the `//# sourceMappingURL=` directive, + * so it handles maps inlined as a `data:` URI as well as external maps under + * any filename (resolved relative to the .js). Returns null when the file has + * no map; warns when a referenced map exists but can't be read or parsed. + */ + private readSourceMap(jsFile: string): TraceMap | null { + let code: string; + try { + code = fs.readFileSync(jsFile, 'utf-8'); + } catch { + return null; // .js not present/readable -> treat as "no map" + } + + try { + // Inline maps live at the .js; external maps live at the referenced file. + // The map URL tells trace-mapping where `sources` (and sourceRoot) resolve. + let mapUrl = pathToFileURL(jsFile).href; + const converter = + convertSourceMap.fromSource(code) + ?? convertSourceMap.fromMapFileSource(code, (mapFile) => { + const mapPath = path.resolve(path.dirname(jsFile), mapFile); + mapUrl = pathToFileURL(mapPath).href; + return fs.readFileSync(mapPath, 'utf-8'); + }); + return converter ? new TraceMap(converter.toObject(), mapUrl) : null; + } catch (err) { + // A map was referenced but couldn't be read/parsed. Surface it once + // rather than letting a broken map masquerade as "no source map". + this.collectedWarnings.push(`Source map for ${jsFile} failed to load: ${(err as Error).message}`); + return null; + } + } +} +const SUPPORTED_SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js'] as const; + +function isSupportedSourceFile(file: string): boolean { + return SUPPORTED_SOURCE_EXTENSIONS.some((ext) => file.endsWith(ext)); +} + +// renderCallStackJustMyCode emits frames as " at (::)". +// Anchoring on "(" avoids capturing the leading "at " into the file group. +const FRAME_RE = /\(([^()\s][^()]*?):(\d+):(\d+)\)\s*$/; + +function parseFrame(frame: string): SourceLocation | undefined { + const m = FRAME_RE.exec(frame); + if (!m) return undefined; + const line = Number(m[2]); + const column = Number(m[3]); + if (!Number.isFinite(line) || !Number.isFinite(column)) return undefined; + return { file: m[1], line, column }; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/index.ts b/packages/@aws-cdk/cdk-explorer/lib/index.ts index 9e4838147..c0fd2de4e 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/index.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/index.ts @@ -1,4 +1,8 @@ export const VERSION = '0.0.0'; -export { startServer } from './lsp/server'; -export type { LspServerOptions } from './lsp/server'; +export { readAssembly } from './core/assembly-reader'; +export type { AssemblyData, AssemblyReadResult, ConstructNode } from './core/assembly-reader'; +export type { SourceLocation } from './core/source-resolver'; + +export { startServer, createLspHandlers } from './lsp/server'; +export type { LspHandlers, LspHandlerOptions, LspServerOptions } from './lsp/server'; diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts new file mode 100644 index 000000000..c0a980266 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts @@ -0,0 +1,80 @@ +import { pathToFileURL } from 'url'; +import type { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import { type CodeLens, type Range } from 'vscode-languageserver/node'; +import type { ConstructNode } from '../core/assembly-reader'; +import type { SourceLocation } from '../core/source-resolver'; + +/** + * Build CodeLens entries for a single source file. For every construct whose + * sourceLocation matches fileUri, group by line and emit one lens per line + * summarising the CFN resources produced there. + */ +export function codeLensesForFile(index: ConstructIndex, fileUri: string): CodeLens[] { + const matches = [...index] + .filter((node) => isResourceOnFile(node, fileUri)) + .map((node) => ({ + line: node.sourceLocation.line, + resource: { logicalId: node.logicalId, cfnType: node.type }, + })); + + // Multiple resources can map to one line when an L2 construct fans out + // (e.g. an L2 producing a primary resource + auxiliary resources). + // Empty command name = title-only lens; click does nothing for now. + return [...groupBy(matches, (m) => m.line)].map(([line, group]) => ({ + range: lineRange(line), + command: { title: titleFor(group.map((m) => m.resource)), command: '' }, + })); +} + +interface ResourceLensInfo { + readonly logicalId: string; + readonly cfnType: string; +} + +/** A construct that produces a CFN resource and carries a source location. */ +interface ResourceConstruct extends ConstructNode { + readonly sourceLocation: SourceLocation; + readonly logicalId: string; + readonly type: string; +} + +/** + * A node gets a lens only if it maps to a CFN resource (has logicalId + type) + * and has a source location in the requested file. Wrapper nodes and non-TS + * constructs are excluded. + */ +function isResourceOnFile(node: ConstructNode, fileUri: string): node is ResourceConstruct { + return ( + node.sourceLocation !== undefined && + node.logicalId !== undefined && + node.type !== undefined && + pathToFileURL(node.sourceLocation.file).toString() === fileUri + ); +} + +/** Group items by a derived key, preserving first-seen order. */ +export function groupBy(items: readonly T[], key: (item: T) => K): Map { + const out = new Map(); + for (const item of items) { + const k = key(item); + const list = out.get(k); + if (list) list.push(item); + else out.set(k, [item]); + } + return out; +} + +function lineRange(line1Based: number): Range { + // LSP positions are 0-based. The editor renders the lens above this line. + const line = Math.max(0, line1Based - 1); + return { start: { line, character: 0 }, end: { line, character: 0 } }; +} + +function titleFor(resources: readonly ResourceLensInfo[]): string { + if (resources.length === 1) { + const r = resources[0]; + return `Creates: ${r.cfnType} [logical: ${r.logicalId}]`; + } + const ids = resources.map((r) => r.logicalId).join(', '); + return `${resources.length} resources: ${ids}`; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts new file mode 100644 index 000000000..adfdb2350 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts @@ -0,0 +1,142 @@ +import { pathToFileURL } from 'url'; +import type { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import type { + PolicyValidationReportJson, + PolicyViolationJson, + PolicyViolationSeverity, +} from '@aws-cdk/cloud-assembly-schema'; +import { + type Diagnostic, + DiagnosticSeverity, + type Range, +} from 'vscode-languageserver/node'; +import { groupBy } from './codelens'; +import type { ConstructNode } from '../core/assembly-reader'; +import type { SourceLocation } from '../core/source-resolver'; + +export interface MapViolationsResult { + /** One entry per file URI, ready for connection.sendDiagnostics. */ + readonly byUri: Map; + /** Violations we couldn't anchor to a source location, with reason. */ + readonly dropped: ReadonlyArray<{ + readonly ruleName: string; + readonly constructPath: string; + readonly reason: string; + }>; +} + +/** + * Convert a validation report into LSP diagnostics keyed by file URI. + * Violations we can't anchor to a TypeScript source location are dropped + * (with a reason) rather than thrown. + */ +export function mapViolationsToDiagnostics( + violations: PolicyValidationReportJson | undefined, + index: ConstructIndex, +): MapViolationsResult { + const dropped: Array = []; + const located: Array<{ uri: string; diagnostic: Diagnostic }> = []; + + for (const { pluginName, violation, constructPath } of flattenTargets(violations)) { + const anchored = anchorViolation(constructPath, index); + if ('reason' in anchored) { + dropped.push({ ruleName: violation.ruleName, constructPath: constructPath ?? '', reason: anchored.reason }); + } else { + located.push({ uri: anchored.uri, diagnostic: buildDiagnostic(violation, anchored.range, pluginName) }); + } + } + + const byUri = new Map( + [...groupBy(located, (l) => l.uri)].map(([uri, items]) => [uri, items.map((i) => i.diagnostic)]), + ); + return { byUri, dropped }; +} + +interface ViolationTarget { + readonly pluginName: string; + readonly violation: PolicyViolationJson; + readonly constructPath: string | undefined; +} + +/** Flatten the report's plugin -> violation -> target nesting into a flat list. */ +function flattenTargets(report: PolicyValidationReportJson | undefined): ViolationTarget[] { + return (report?.pluginReports ?? []).flatMap((plugin) => + (plugin.violations ?? []).flatMap((violation) => + (violation.violatingConstructs ?? []).map((target) => ({ + pluginName: plugin.pluginName, + violation, + constructPath: target.constructPath, + })))); +} + +/** A construct's source location, or the reason it has none. */ +function resolveLocation( + constructPath: string | undefined, + index: ConstructIndex, +): SourceLocation | { readonly reason: string } { + if (!constructPath) return { reason: 'violation has no construct path' }; + const node = index.byPath(constructPath); + if (!node) return { reason: 'not found in the construct tree' }; + if (!node.sourceLocation) return { reason: 'no source location (non-TypeScript app or framework-only trace)' }; + return node.sourceLocation; +} + +/** Resolve a violation to a presentable anchor, or a reason it can't be shown. */ +function anchorViolation( + constructPath: string | undefined, + index: ConstructIndex, +): { readonly uri: string; readonly range: Range } | { readonly reason: string } { + const loc = resolveLocation(constructPath, index); + if ('reason' in loc) return loc; + + // We only surface diagnostics for TypeScript sources for now. + if (!isTypeScript(loc.file)) return { reason: `source file is not TypeScript: ${loc.file}` }; + + return { uri: pathToFileURL(loc.file).toString(), range: toRange(loc) }; +} + +function isTypeScript(file: string): boolean { + return file.endsWith('.ts') || file.endsWith('.tsx'); +} + +/** + * LSP range for a source location. Anchors at the resolved line/column, or at + * the top of the file when the file is known but the line/column aren't. LSP + * positions are 0-based; sourceLocation is 1-based. The end spans to + * Number.MAX_VALUE so the squiggle covers the rest of the line. + */ +function toRange(loc: SourceLocation): Range { + const hasLineCol = loc.line >= 1 && loc.column >= 1; + const line = hasLineCol ? loc.line - 1 : 0; + const character = hasLineCol ? loc.column - 1 : 0; + return { start: { line, character }, end: { line, character: Number.MAX_VALUE } }; +} + +function buildDiagnostic(violation: PolicyViolationJson, range: Range, pluginName: string): Diagnostic { + return { + range, + severity: severityFor(violation.severity), + code: violation.ruleName, + source: pluginName, + message: formatMessage(violation), + }; +} + +function severityFor(s: PolicyViolationSeverity | undefined): DiagnosticSeverity { + switch (s) { + case 'fatal': + case 'error': + return DiagnosticSeverity.Error; + case 'warning': + return DiagnosticSeverity.Warning; + case 'info': + case 'custom': + default: + return DiagnosticSeverity.Information; + } +} + +function formatMessage(v: PolicyViolationJson): string { + const head = v.description ?? v.ruleName; + return v.suggestedFix ? `${head}\n\nSuggested fix: ${v.suggestedFix}` : head; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts index 118ea0777..f1f09461f 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts @@ -6,6 +6,7 @@ try { writable: process.stdout, }); } catch (err) { - process.stderr.write(`CDK LSP startup fatal: ${err}\n`); + const e = err as Error; + process.stderr.write(`CDK LSP startup fatal: ${e.stack ?? e.message}\n`); process.exit(1); } diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index b0e4d764d..006188ac0 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -1,4 +1,6 @@ +import * as path from 'path'; import { fileURLToPath } from 'url'; +import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; import { StreamMessageReader, StreamMessageWriter, @@ -7,38 +9,112 @@ import { createConnection, ProposedFeatures, TextDocumentSyncKind, + type CodeLens, + type CodeLensParams, type DidSaveTextDocumentParams, + type Diagnostic, type InitializeParams, type InitializeResult, } from 'vscode-languageserver/node'; /* eslint-disable import/no-relative-packages */ +import { codeLensesForFile } from './codelens'; +import { mapViolationsToDiagnostics } from './diagnostics'; import { WATCH_EXCLUDE_DEFAULTS } from '../../../toolkit-lib/lib/actions/watch/private/helpers'; import { createIgnoreMatcher } from '../../../toolkit-lib/lib/util/glob-matcher'; +import { + readAssembly as defaultReadAssembly, + type AssemblyReadResult, + type ConstructNode, +} from '../core/assembly-reader'; -export interface LspServerOptions { - readonly readable: NodeJS.ReadableStream; - readonly writable: NodeJS.WritableStream; +export interface LspHandlerOptions { + /** Callback invoked on `didSave` for tracked source files. */ + readonly onSynthRequest?: (projectDir: string) => void; + /** Override readAssembly for tests. Defaults to reading /cdk.out. */ + readonly readAssembly?: (assemblyDir: string) => AssemblyReadResult; /** - * Callback invoked on `didSave` for tracked source files + * Sink for non-fatal messages. In production, the connection's console writes + * to the editor's Output panel; in tests, capture into an array. */ - readonly onSynthRequest?: (projectDir: string) => void; + readonly logger?: LogSink; + /** Receives diagnostics ready to be published to the editor. */ + readonly onPublishDiagnostics?: (uri: string, diagnostics: Diagnostic[]) => void; +} + +export interface LspServerOptions extends LspHandlerOptions { + readonly readable: NodeJS.ReadableStream; + readonly writable: NodeJS.WritableStream; } -interface LspHandlers { +/** Pure handler functions for LSP messages, extracted for direct unit testing. */ +export interface LspHandlers { onInitialize(params: InitializeParams): InitializeResult; onInitialized(): void; onDidSaveTextDocument(params: DidSaveTextDocumentParams): void; + onCodeLens(params: CodeLensParams): CodeLens[]; onShutdown(): void; } interface LogSink { + warn(message: string): void; error(message: string): void; } -function buildHandlers(onSynthRequest: (projectDir: string) => void, log: LogSink): LspHandlers { +const NOOP_LOGGER: LogSink = { + warn: () => { + }, + error: () => { + }, +}; + +/** + * Build the LSP message handlers as plain functions over closed-over state. + * No streams, no JSON-RPC, no framework — testable in isolation. + */ +export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers { + const onSynthRequest = options.onSynthRequest ?? (() => { + }); + const readAssembly = options.readAssembly ?? defaultReadAssembly; + const log = options.logger ?? NOOP_LOGGER; + const onPublishDiagnostics = options.onPublishDiagnostics ?? (() => { + }); + let applicationDir: string | undefined; let shutdownRequested = false; let shouldIgnore: (filePath: string) => boolean = () => false; + // Latest index from readAssembly, served to CodeLens without re-reading + // cdk.out. Refreshed on every onInitialized; cdk.out watcher is a future feature. + let cachedIndex: ConstructIndex = ConstructIndex.fromTree([]); + + function refreshFromAssembly(projectDir: string): void { + const assemblyDir = path.join(projectDir, 'cdk.out'); + const result = readAssembly(assemblyDir); + + if (result.status === 'error') { + log.error(`Failed to read cloud assembly: ${result.message}`); + return; + } + if (result.status === 'not-found') return; + + const { tree, violations, violationsError, warnings } = result.data; + for (const warning of warnings) { + log.warn(warning); + } + if (violationsError) { + log.warn(`validation-report.json failed to load: ${violationsError}`); + } + + cachedIndex = ConstructIndex.fromTree(tree); + + const { byUri, dropped } = mapViolationsToDiagnostics(violations, cachedIndex); + + for (const drop of dropped) { + log.warn(`Dropped diagnostic for '${drop.ruleName}' at '${drop.constructPath}': ${drop.reason}`); + } + for (const [uri, diagnostics] of byUri) { + onPublishDiagnostics(uri, diagnostics); + } + } return { onInitialize(params) { @@ -52,6 +128,8 @@ function buildHandlers(onSynthRequest: (projectDir: string) => void, log: LogSin change: TextDocumentSyncKind.None, save: { includeText: false }, }, + // Lens title is computed up-front; no resolve round-trip needed. + codeLensProvider: { resolveProvider: false }, }, }; }, @@ -71,6 +149,7 @@ function buildHandlers(onSynthRequest: (projectDir: string) => void, log: LogSin ], rootDir: projectDir, }); + refreshFromAssembly(projectDir); }, onDidSaveTextDocument(params) { if (shutdownRequested) return; @@ -80,9 +159,12 @@ function buildHandlers(onSynthRequest: (projectDir: string) => void, log: LogSin try { onSynthRequest(projectDir); } catch (err) { - log.error(`Synth request failed: ${err instanceof Error ? err.message : String(err)}`); + log.error(`Synth request failed: ${(err as Error).message}`); } }, + onCodeLens(params) { + return codeLensesForFile(cachedIndex, params.textDocument.uri); + }, onShutdown() { shutdownRequested = true; }, @@ -96,13 +178,19 @@ export function startServer(options: LspServerOptions): void { new StreamMessageWriter(options.writable), ); - const onSynthRequest = options.onSynthRequest ?? (() => { + const handlers = createLspHandlers({ + onSynthRequest: options.onSynthRequest, + readAssembly: options.readAssembly, + logger: connection.console, + onPublishDiagnostics: (uri, diagnostics) => { + void connection.sendDiagnostics({ uri, diagnostics }); + }, }); - const handlers = buildHandlers(onSynthRequest, connection.console); connection.onInitialize((params) => handlers.onInitialize(params)); connection.onInitialized(() => handlers.onInitialized()); connection.onDidSaveTextDocument((params) => handlers.onDidSaveTextDocument(params)); + connection.onCodeLens((params) => handlers.onCodeLens(params)); connection.onShutdown(() => handlers.onShutdown()); connection.onExit(() => process.exit(0)); diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index 7a75aadaa..d893c20ba 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -32,6 +32,7 @@ "devDependencies": { "@cdklabs/eslint-plugin": "^2.0.6", "@stylistic/eslint-plugin": "^3", + "@types/convert-source-map": "^2", "@types/express": "^4", "@types/jest": "^29.5.14", "@types/node": "^20", @@ -58,6 +59,8 @@ "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/toolkit-lib": "^0.0.0", + "@jridgewell/trace-mapping": "^0.3", + "convert-source-map": "^2", "express": "^4", "vscode-jsonrpc": "^8", "vscode-languageserver": "^9", diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/README.md b/packages/@aws-cdk/cdk-explorer/test/_fixtures/README.md new file mode 100644 index 000000000..9146924b0 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/README.md @@ -0,0 +1,33 @@ +# Test Fixtures + +These fixtures back the cdk-explorer unit tests. They come in two kinds. + +## Programmatic assemblies (`builders.ts`) + +Rather than checking in static `cdk.out/` directories, most tests construct +their cloud assembly in memory with `builders.ts` — helpers that write a +`manifest.json` + `tree.json` describing stacks, constructs, CFN resources, +and validation reports. This keeps each test's intent readable in TypeScript +and reduces the chance of the fixtures going stale as the cloud-assembly format evolves. The +builders only populate the fields `readAssembly` actually consumes, using the +metadata-key constants from `@aws-cdk/cloud-assembly-schema` so the keys stay +correct. `builders.test.ts` sanity-checks each builder against `readAssembly`. + +## source-maps/ + +`sample.ts` + `sample.js` + `sample.js.map` are real `tsc` output, used by +`source-resolver.test.ts` to verify `.js` → `.ts` resolution via +`@jridgewell/trace-mapping`. + +To regenerate (if the test starts pointing at the wrong line/col): + +```bash +cd packages/@aws-cdk/cdk-explorer +rm test/_fixtures/source-maps/sample.{js,js.map} +npx tsc --target ES2020 --module commonjs --sourceMap \ + --outDir test/_fixtures/source-maps \ + test/_fixtures/source-maps/sample.ts +``` + +If you edit `sample.ts`, also update the line/column expectations in +`source-resolver.test.ts`. diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts new file mode 100644 index 000000000..68f4e65c0 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts @@ -0,0 +1,227 @@ +// Sanity tests for the fixture builders. If these pass, every other test +// that uses the builders inherits a known-good baseline. +import { + buildFlatAssembly, + buildNestedAssembly, + buildNestedStackAssembly, + buildNonTypeScriptAssembly, + cleanupFixture, + withMalformedValidationReport, + withValidationReport, +} from './builders'; +import { readAssembly } from '../../lib'; + +describe('fixture builders', () => { + let dir: string | undefined; + + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + test('buildFlatAssembly produces a readAssembly-compatible directory', () => { + dir = buildFlatAssembly({ + stacks: [{ + id: 'Stack1', + resources: [{ + id: 'MyBucket', + logicalId: 'MyBucketF68F3FF0', + cfnType: 'AWS::S3::Bucket', + }], + }], + }); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error(`expected success, got ${result.status}: ${(result as any).message ?? ''}`); + + expect(result.data.tree).toHaveLength(1); + expect(result.data.tree[0].id).toBe('Stack1'); + }); + + test('buildFlatAssembly attaches logicalId and cfnType correctly', () => { + dir = buildFlatAssembly({ + stacks: [{ + id: 'Stack1', + resources: [{ + id: 'MyBucket', + logicalId: 'MyBucketF68F3FF0', + cfnType: 'AWS::S3::Bucket', + }], + }], + }); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + + const resource = result.data.tree[0].children[0].children[0]; + expect(resource.path).toBe('Stack1/MyBucket/Resource'); + expect(resource.logicalId).toBe('MyBucketF68F3FF0'); + expect(resource.type).toBe('AWS::S3::Bucket'); + // No creationTrace was set, so the resolver finds no frames -> undefined. + expect(resource.sourceLocation).toBeUndefined(); + }); + + test('buildFlatAssembly with creationTrace exposes sourceLocation', () => { + dir = buildFlatAssembly({ + stacks: [{ + id: 'Stack1', + resources: [{ + id: 'MyBucket', + logicalId: 'MyBucketF68F3FF0', + cfnType: 'AWS::S3::Bucket', + // Mirrors aws-cdk-lib's renderCallStackJustMyCode output: node_modules + // and node: frames are pre-filtered into skip-placeholder lines, + // so the first frame that parses is the user's call site. + creationTrace: [ + ' ...node_modules-aws-cdk-lib...', + ' at new MyStack (/project/lib/my-stack.ts:12:5)', + ], + }], + }], + }); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + + const resource = result.data.tree[0].children[0].children[0]; + expect(resource.sourceLocation).toEqual({ + file: '/project/lib/my-stack.ts', + line: 12, + column: 5, + }); + }); + + test('buildNestedAssembly produces a Stage-based assembly with correct tree', () => { + dir = buildNestedAssembly({ + stages: [{ + id: 'Prod', + stacks: [{ + id: 'Service', + resources: [{ + id: 'MyBucket', + logicalId: 'MyBucketABC', + cfnType: 'AWS::S3::Bucket', + }], + }], + }], + }); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + + expect(result.data.tree).toHaveLength(1); + + const stage = result.data.tree[0]; + expect(stage.id).toBe('Prod'); + + const stack = stage.children[0]; + expect(stack.path).toBe('Prod/Service'); + + const resource = stack.children[0].children[0]; + expect(resource.path).toBe('Prod/Service/MyBucket/Resource'); + expect(resource.logicalId).toBe('MyBucketABC'); + expect(resource.type).toBe('AWS::S3::Bucket'); + }); + + test('buildNestedStackAssembly enriches resources INSIDE a NestedStack via parent metadata', () => { + // aws-cdk-lib emits nested-stack-internal metadata into the parent's + // artifact metadata. Verifies buildNode's metadata inheritance handles + // this with no separate walker. + dir = buildNestedStackAssembly({ + parent: { + id: 'Parent', + resources: [{ + id: 'TopBucket', + logicalId: 'TopBucketAAA', + cfnType: 'AWS::S3::Bucket', + }], + nestedStacks: [{ + id: 'MyNestedStack', + resources: [{ + id: 'NestedBucket', + logicalId: 'NestedBucketBBB', + cfnType: 'AWS::S3::Bucket', + creationTrace: [ + ' ...node_modules-aws-cdk-lib...', + ' at new MyNestedStack (/project/lib/nested.ts:7:5)', + ], + }], + }], + }, + }); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + + const parent = result.data.tree[0]; + expect(parent.id).toBe('Parent'); + + const topBucket = parent.children.find((c) => c.id === 'TopBucket')!.children[0]; + expect(topBucket.logicalId).toBe('TopBucketAAA'); + expect(topBucket.type).toBe('AWS::S3::Bucket'); + + const nestedStack = parent.children.find((c) => c.id === 'MyNestedStack')!; + expect(nestedStack.path).toBe('Parent/MyNestedStack'); + + const nestedBucket = nestedStack.children[0].children[0]; + expect(nestedBucket.path).toBe('Parent/MyNestedStack/NestedBucket/Resource'); + expect(nestedBucket.logicalId).toBe('NestedBucketBBB'); + expect(nestedBucket.type).toBe('AWS::S3::Bucket'); + expect(nestedBucket.sourceLocation).toEqual({ + file: '/project/lib/nested.ts', + line: 7, + column: 5, + }); + }); + + test('buildNonTypeScriptAssembly returns success without crashing', () => { + dir = buildNonTypeScriptAssembly(); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + + expect(result.data.tree).toHaveLength(1); + const stack = result.data.tree[0]; + expect(stack.id).toBe('Stack1'); + // Non-TS apps emit no aws:cdk:logicalId metadata. + expect(stack.sourceLocation).toBeUndefined(); + }); + + test('withMalformedValidationReport surfaces the error without breaking the tree', () => { + dir = buildFlatAssembly({ + stacks: [{ id: 'Stack1', resources: [] }], + }); + withMalformedValidationReport(dir); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + + expect(result.data.tree).toHaveLength(1); + expect(result.data.violations).toBeUndefined(); + expect(result.data.violationsError).toBeTruthy(); + }); + + test('withValidationReport produces a parseable report', () => { + dir = buildFlatAssembly({ + stacks: [{ id: 'Stack1', resources: [] }], + }); + withValidationReport(dir, { + pluginReports: [{ + pluginName: 'test', + conclusion: 'failure', + violations: [{ + ruleName: 'no-bad-things', + description: 'no', + severity: 'error', + violatingConstructs: [{ constructPath: 'Stack1/x' }], + }], + }], + }); + + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + + expect(result.data.violationsError).toBeUndefined(); + expect(result.data.violations?.pluginReports[0].pluginName).toBe('test'); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts new file mode 100644 index 000000000..b0fd39679 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts @@ -0,0 +1,404 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { ArtifactMetadataEntryType, VALIDATION_REPORT_FILE } from '@aws-cdk/cloud-assembly-schema'; + +/** + * Programmatic fixture builders. Each builder writes a minimal cdk.out/ to a + * temp dir and returns its path; cleanupFixture removes it. CloudAssembly + * construction enforces the schema, which is what catches drift in builders. + * + * const dir = buildFlatAssembly({ stacks: [...] }); + * try { ... } finally { cleanupFixture(dir); } + */ + +/** + * Pinned schema revision written into fixture manifests. Intentionally a static + * literal (not Manifest.version()): a fixture frozen at a known revision doubles + * as a forward-compat test that the reader can still load older assemblies. + * Deriving the version would make the manifest claim "latest" while its contents + * stay frozen — i.e. lie about its shape if a future revision changes it. + */ +const ASSEMBLY_SCHEMA_VERSION = '53.0.0'; + +/** aws-cdk-lib version stamped into fixture tree.json constructInfo. */ +const CONSTRUCT_INFO_VERSION = '2.245.0'; + +/** tree.json schema version written into fixtures. */ +const TREE_SCHEMA_VERSION = 'tree-0.1'; + +export interface ResourceSpec { + /** Construct id under the stack (e.g. "MyBucket"). */ + readonly id: string; + /** CFN logical ID (e.g. "MyBucketF68F3FF0"). */ + readonly logicalId: string; + /** CFN resource type (e.g. "AWS::S3::Bucket"). */ + readonly cfnType: string; + /** Stack frames for the creation trace (omit for non-TS apps). */ + readonly creationTrace?: readonly string[]; +} + +export interface StackSpec { + /** Stack id. Becomes both artifact id and tree path. */ + readonly id: string; + readonly resources: readonly ResourceSpec[]; +} + +export interface FlatAssemblySpec { + readonly stacks: readonly StackSpec[]; +} + +export interface StageStackSpec { + /** Construct id under the Stage. Tree path = `/`. */ + readonly id: string; + readonly resources: readonly ResourceSpec[]; +} + +export interface StageSpec { + readonly id: string; + readonly stacks: readonly StageStackSpec[]; +} + +export interface NestedAssemblySpec { + readonly stages: readonly StageSpec[]; +} + +export interface NestedStackSpec { + /** Construct id under the parent stack (e.g. "MyNestedStack"). */ + readonly id: string; + /** Resources inside the nested stack. */ + readonly resources: readonly ResourceSpec[]; +} + +export interface NestedStackParentSpec { + /** Parent stack id. */ + readonly id: string; + /** Top-level resources of the parent stack. */ + readonly resources: readonly ResourceSpec[]; + /** NestedStack children of the parent stack. */ + readonly nestedStacks: readonly NestedStackSpec[]; +} + +/** Build a flat assembly (stacks directly under App, no Stages). */ +export function buildFlatAssembly(spec: FlatAssemblySpec): string { + const dir = mkAssemblyDir('flat'); + const artifacts: Record = { + Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + }; + + for (const stack of spec.stacks) { + artifacts[stack.id] = stackArtifact(stack); + writeTemplate(dir, stack); + } + + writeJson(path.join(dir, 'manifest.json'), { + version: ASSEMBLY_SCHEMA_VERSION, + artifacts, + }); + writeJson(path.join(dir, 'tree.json'), { + version: TREE_SCHEMA_VERSION, + tree: appNode(spec.stacks.map(stackTreeNode)), + }); + fs.writeFileSync(path.join(dir, 'cdk.out'), JSON.stringify({ version: ASSEMBLY_SCHEMA_VERSION })); + return dir; +} + +/** Build a Stage-based assembly (each stage produces a nested cloud-assembly). */ +export function buildNestedAssembly(spec: NestedAssemblySpec): string { + const rootDir = mkAssemblyDir('nested'); + const rootArtifacts: Record = { + Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + }; + + const treeChildren: Record = {}; + + for (const stage of spec.stages) { + const dirName = `assembly-${stage.id}`; + rootArtifacts[dirName] = { + type: 'cdk:cloud-assembly', + properties: { directoryName: dirName, displayName: stage.id }, + }; + + const stageDir = path.join(rootDir, dirName); + fs.mkdirSync(stageDir, { recursive: true }); + + const stageArtifacts: Record = {}; + for (const stack of stage.stacks) { + const artifactId = `${stage.id}${stack.id}Artifact`; + const constructPath = `${stage.id}/${stack.id}`; + stageArtifacts[artifactId] = { + type: 'aws:cloudformation:stack', + environment: 'aws://unknown-account/unknown-region', + properties: { templateFile: `${artifactId}.template.json` }, + // displayName is the construct path; the reader keys metadata by + // hierarchicalId, which falls back to displayName. + displayName: constructPath, + metadata: stackMetadata(stack.resources, `/${constructPath}`), + }; + writeTemplate(stageDir, { id: artifactId, resources: stack.resources }); + } + + writeJson(path.join(stageDir, 'manifest.json'), { + version: ASSEMBLY_SCHEMA_VERSION, + artifacts: stageArtifacts, + }); + fs.writeFileSync(path.join(stageDir, 'cdk.out'), JSON.stringify({ version: ASSEMBLY_SCHEMA_VERSION })); + + treeChildren[stage.id] = { + id: stage.id, + path: stage.id, + constructInfo: { fqn: 'aws-cdk-lib.Stage', version: CONSTRUCT_INFO_VERSION }, + children: Object.fromEntries(stage.stacks.map((s) => [s.id, { + id: s.id, + path: `${stage.id}/${s.id}`, + constructInfo: { fqn: 'aws-cdk-lib.Stack', version: CONSTRUCT_INFO_VERSION }, + children: resourcesToTreeChildren(`${stage.id}/${s.id}`, s.resources), + }])), + }; + } + + writeJson(path.join(rootDir, 'manifest.json'), { + version: ASSEMBLY_SCHEMA_VERSION, + artifacts: rootArtifacts, + }); + writeJson(path.join(rootDir, 'tree.json'), { + version: TREE_SCHEMA_VERSION, + tree: appNode(Object.values(treeChildren)), + }); + fs.writeFileSync(path.join(rootDir, 'cdk.out'), JSON.stringify({ version: ASSEMBLY_SCHEMA_VERSION })); + return rootDir; +} + +/** + * Build an assembly that mimics a parent stack with NestedStack children. + * aws-cdk-lib emits NO separate manifest artifact for nested stacks; their + * resources' metadata lives under the PARENT artifact, keyed by full + * construct path (e.g. /Parent/MyNestedStack/MyBucket/Resource). + */ +export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec }): string { + const dir = mkAssemblyDir('nestedstack'); + const { parent } = spec; + + const metadata: Record = {}; + for (const r of parent.resources) { + metadata[`/${parent.id}/${r.id}/Resource`] = [logicalIdEntry(r)]; + } + for (const ns of parent.nestedStacks) { + for (const r of ns.resources) { + metadata[`/${parent.id}/${ns.id}/${r.id}/Resource`] = [logicalIdEntry(r)]; + } + } + + writeJson(path.join(dir, 'manifest.json'), { + version: ASSEMBLY_SCHEMA_VERSION, + artifacts: { + Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + [parent.id]: { + type: 'aws:cloudformation:stack', + environment: 'aws://unknown-account/unknown-region', + properties: { templateFile: `${parent.id}.template.json` }, + displayName: parent.id, + metadata, + }, + }, + }); + + const parentChildren: Record = {}; + for (const r of parent.resources) { + parentChildren[r.id] = resourceTreeNode(parent.id, r); + } + for (const ns of parent.nestedStacks) { + parentChildren[ns.id] = { + id: ns.id, + path: `${parent.id}/${ns.id}`, + constructInfo: { fqn: 'aws-cdk-lib.NestedStack', version: CONSTRUCT_INFO_VERSION }, + children: Object.fromEntries( + ns.resources.map((r) => [r.id, resourceTreeNode(`${parent.id}/${ns.id}`, r)]), + ), + }; + } + + writeJson(path.join(dir, 'tree.json'), { + version: TREE_SCHEMA_VERSION, + tree: appNode([{ + id: parent.id, + path: parent.id, + constructInfo: { fqn: 'aws-cdk-lib.Stack', version: CONSTRUCT_INFO_VERSION }, + children: parentChildren, + }]), + }); + writeJson(path.join(dir, `${parent.id}.template.json`), { Resources: {} }); + fs.writeFileSync(path.join(dir, 'cdk.out'), JSON.stringify({ version: ASSEMBLY_SCHEMA_VERSION })); + return dir; +} + +/** Manifest + tree with no metadata, no traces — for non-TS app graceful-degradation tests. */ +export function buildNonTypeScriptAssembly(): string { + const dir = mkAssemblyDir('nonts'); + writeJson(path.join(dir, 'manifest.json'), { + version: ASSEMBLY_SCHEMA_VERSION, + artifacts: { + Stack1: { + type: 'aws:cloudformation:stack', + environment: 'aws://unknown-account/unknown-region', + properties: { templateFile: 'Stack1.template.json' }, + displayName: 'Stack1', + // No metadata: non-TS apps emit no aws:cdk:logicalId entries. + }, + Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + }, + }); + writeJson(path.join(dir, 'tree.json'), { + version: TREE_SCHEMA_VERSION, + tree: { + id: 'App', + path: '', + constructInfo: { fqn: 'aws-cdk-lib.App', version: CONSTRUCT_INFO_VERSION }, + children: { + Stack1: { + id: 'Stack1', + path: 'Stack1', + constructInfo: { fqn: 'aws-cdk-lib.Stack', version: CONSTRUCT_INFO_VERSION }, + }, + }, + }, + }); + writeJson(path.join(dir, 'Stack1.template.json'), { Resources: {} }); + fs.writeFileSync(path.join(dir, 'cdk.out'), JSON.stringify({ version: ASSEMBLY_SCHEMA_VERSION })); + return dir; +} + +/** Drop an unparseable validation-report.json into an existing fixture dir. */ +export function withMalformedValidationReport(dir: string): void { + fs.writeFileSync(path.join(dir, VALIDATION_REPORT_FILE), '{ "pluginReports": ['); +} + +/** + * Drop a well-formed but version-LESS validation-report.json: the legacy shape + * older aws-cdk-lib emits (no `version` field). The reader must still load it. + */ +export function withVersionlessValidationReport(dir: string): void { + writeJson(path.join(dir, VALIDATION_REPORT_FILE), { + title: 'Validation Report', + pluginReports: [], + }); +} + +/** Drop a well-formed validation-report.json into an existing fixture dir. */ +export function withValidationReport(dir: string, report: { + version?: string; + pluginReports: Array<{ + pluginName: string; + conclusion: 'success' | 'failure'; + violations: Array<{ + ruleName: string; + description: string; + severity: 'fatal' | 'error' | 'warning' | 'info' | 'custom'; + violatingConstructs: Array<{ + constructPath: string; + cloudFormationResource?: { templatePath: string; logicalId: string }; + }>; + }>; + }>; +}): void { + writeJson(path.join(dir, VALIDATION_REPORT_FILE), { + version: report.version ?? '1.0.0', + ...report, + }); +} + +/** Delete a fixture dir; safe on undefined / missing dirs. */ +export function cleanupFixture(dir: string | undefined): void { + if (!dir) return; + fs.rmSync(dir, { recursive: true, force: true }); +} + +// ---------- internals ---------- + +function mkAssemblyDir(prefix: string): string { + const base = fs.mkdtempSync(path.join(os.tmpdir(), `cdk-explorer-${prefix}-`)); + return path.join(base, 'cdk.out'); +} + +function writeJson(filePath: string, data: unknown): void { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} + +function appNode(children: readonly unknown[]): unknown { + const childrenMap: Record = {}; + for (const child of children) { + const c = child as { id: string }; + childrenMap[c.id] = child; + } + return { + id: 'App', + path: '', + constructInfo: { fqn: 'aws-cdk-lib.App', version: CONSTRUCT_INFO_VERSION }, + children: childrenMap, + }; +} + +function stackTreeNode(stack: StackSpec): unknown { + return { + id: stack.id, + path: stack.id, + constructInfo: { fqn: 'aws-cdk-lib.Stack', version: CONSTRUCT_INFO_VERSION }, + children: Object.fromEntries( + stack.resources.map((r) => [r.id, resourceTreeNode(stack.id, r)]), + ), + }; +} + +function resourcesToTreeChildren(stackPath: string, resources: readonly ResourceSpec[]): Record { + return Object.fromEntries(resources.map((r) => [r.id, resourceTreeNode(stackPath, r)])); +} + +// Mirrors aws-cdk-lib's L2 construct -> CfnResource child shape. +function resourceTreeNode(parentPath: string, r: ResourceSpec): unknown { + return { + id: r.id, + path: `${parentPath}/${r.id}`, + children: { + Resource: { + id: 'Resource', + path: `${parentPath}/${r.id}/Resource`, + attributes: { 'aws:cdk:cloudformation:type': r.cfnType }, + }, + }, + }; +} + +function stackArtifact(stack: StackSpec): unknown { + return { + type: 'aws:cloudformation:stack', + environment: 'aws://unknown-account/unknown-region', + properties: { templateFile: `${stack.id}.template.json` }, + displayName: stack.id, + metadata: stackMetadata(stack.resources, `/${stack.id}`), + }; +} + +function stackMetadata(resources: readonly ResourceSpec[], pathPrefix: string): Record { + const metadata: Record = {}; + for (const r of resources) { + metadata[`${pathPrefix}/${r.id}/Resource`] = [logicalIdEntry(r)]; + } + return metadata; +} + +function logicalIdEntry(r: ResourceSpec): Record { + const entry: Record = { type: ArtifactMetadataEntryType.LOGICAL_ID, data: r.logicalId }; + if (r.creationTrace && r.creationTrace.length > 0) { + entry.trace = [...r.creationTrace]; + } + return entry; +} + +function writeTemplate(dir: string, stack: { id: string; resources: readonly ResourceSpec[] }): void { + const resources: Record = {}; + for (const r of stack.resources) { + resources[r.logicalId] = { Type: r.cfnType, Properties: {} }; + } + writeJson(path.join(dir, `${stack.id}.template.json`), { Resources: resources }); +} diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js b/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js new file mode 100644 index 000000000..e09b2c495 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.greet = greet; +// Original TypeScript file used to test source-map resolution. +function greet(name) { + return `Hello, ${name}`; +} +//# sourceMappingURL=sample.js.map \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js.map b/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js.map new file mode 100644 index 000000000..a52bfd314 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sample.js","sourceRoot":"","sources":["sample.ts"],"names":[],"mappings":";;AACA,sBAEC;AAHD,+DAA+D;AAC/D,SAAgB,KAAK,CAAC,IAAY;IAChC,OAAO,UAAU,IAAI,EAAE,CAAC;AAC1B,CAAC"} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.ts new file mode 100644 index 000000000..1c0f121ec --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/source-maps/sample.ts @@ -0,0 +1,4 @@ +// Original TypeScript file used to test source-map resolution. +export function greet(name: string): string { + return `Hello, ${name}`; +} diff --git a/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts new file mode 100644 index 000000000..f400d8c8f --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts @@ -0,0 +1,257 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { readAssembly, type AssemblyData, type AssemblyReadResult, type ConstructNode } from '../../lib'; +import { + buildFlatAssembly, + buildNestedAssembly, + buildNonTypeScriptAssembly, + cleanupFixture, + withMalformedValidationReport, + withValidationReport, + withVersionlessValidationReport, +} from '../_fixtures/builders'; + +/** Assert that readAssembly succeeded and return the typed data. */ +function expectSuccess(result: AssemblyReadResult): AssemblyData { + expect(result.status).toBe('success'); + // Cast is safe because expect() above would have failed the test on mismatch. + return (result as Extract).data; +} + +describe('readAssembly', () => { + let dir: string | undefined; + + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + test('returns not-found for nonexistent directory', () => { + expect(readAssembly('/nonexistent/path').status).toBe('not-found'); + }); + + test('returns not-found for directory without manifest.json', () => { + const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-empty-')); + try { + expect(readAssembly(empty).status).toBe('not-found'); + } finally { + fs.rmSync(empty, { recursive: true, force: true }); + } + }); + + test('parses tree from a flat assembly', () => { + dir = buildFlatAssembly({ + stacks: [ + { id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }, + { id: 'Stack2', resources: [{ id: 'MyQueue', logicalId: 'MyQueueE6CA6235', cfnType: 'AWS::SQS::Queue' }] }, + ], + }); + + const data = expectSuccess(readAssembly(dir)); + + expect(data.tree).toHaveLength(2); + expect(data.tree.map((n) => n.id).sort()).toEqual(['Stack1', 'Stack2']); + }); + + test('enriches resource nodes with logicalId and cfnType', () => { + dir = buildFlatAssembly({ + stacks: [{ + id: 'Stack1', + resources: [{ + id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket', + }], + }], + }); + const data = expectSuccess(readAssembly(dir)); + + const resource = findNode(data.tree, 'Stack1/MyBucket/Resource')!; + expect(resource.logicalId).toBe('MyBucketF68F3FF0'); + expect(resource.type).toBe('AWS::S3::Bucket'); + }); + + test('non-resource constructs (L2 wrappers) have no logicalId or type', () => { + dir = buildFlatAssembly({ + stacks: [{ + id: 'Stack1', + resources: [{ + id: 'MyBucket', + logicalId: 'MyBucketF68F3FF0', + cfnType: 'AWS::S3::Bucket', + }], + }], + }); + const data = expectSuccess(readAssembly(dir)); + + const wrapper = findNode(data.tree, 'Stack1/MyBucket')!; + expect(wrapper.logicalId).toBeUndefined(); + expect(wrapper.type).toBeUndefined(); + }); + + test('children are arrays', () => { + dir = buildFlatAssembly({ + stacks: [{ id: 'Stack1', resources: [{ id: 'X', logicalId: 'X', cfnType: 'AWS::S3::Bucket' }] }], + }); + const data = expectSuccess(readAssembly(dir)); + + expect(Array.isArray(data.tree)).toBe(true); + expect(Array.isArray(data.tree[0].children)).toBe(true); + }); + + test('returns error for malformed manifest', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-malformed-')); + fs.writeFileSync(path.join(tmpDir, 'manifest.json'), 'not json{{{'); + try { + const result = readAssembly(tmpDir); + expect(result.status).toBe('error'); + if (result.status === 'error') expect(result.message).toBeTruthy(); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); +}); + +describe('readAssembly with violations', () => { + let dir: string | undefined; + + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + test('parses validation report when present', () => { + dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); + withValidationReport(dir, { + pluginReports: [{ + pluginName: 'no-public-buckets-plugin', + conclusion: 'failure', + violations: [{ + ruleName: 'no-public-buckets', + description: 'S3 must not be public', + severity: 'error', + violatingConstructs: [{ + constructPath: 'Stack1/MyBucket', + cloudFormationResource: { templatePath: 'Stack1.template.json', logicalId: 'B1' }, + }], + }], + }], + }); + + const data = expectSuccess(readAssembly(dir)); + + expect(data.violations).toBeDefined(); + expect(data.violations!.pluginReports[0].conclusion).toBe('failure'); + expect(data.violations!.pluginReports[0].violations[0].ruleName).toBe('no-public-buckets'); + expect(data.violationsError).toBeUndefined(); + }); + + test('returns undefined violations when report file is absent', () => { + dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); + const data = expectSuccess(readAssembly(dir)); + + expect(data.violations).toBeUndefined(); + expect(data.violationsError).toBeUndefined(); + }); + + test('malformed validation report does not crash the tree read', () => { + dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); + withMalformedValidationReport(dir); + + const data = expectSuccess(readAssembly(dir)); + + expect(data.tree).toHaveLength(1); + expect(data.violations).toBeUndefined(); + expect(data.violationsError).toBeTruthy(); + }); + + test('loads a version-less validation report (legacy aws-cdk-lib shape)', () => { + dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); + withVersionlessValidationReport(dir); + + const data = expectSuccess(readAssembly(dir)); + + expect(data.tree).toHaveLength(1); + expect(data.violations).toBeDefined(); + expect(data.violationsError).toBeUndefined(); + }); +}); + +describe('readAssembly with Stage-based (nested-assembly) apps', () => { + let dir: string | undefined; + + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + test('preserves Stage grouping node in the tree', () => { + dir = buildNestedAssembly({ + stages: [{ id: 'Prod', stacks: [{ id: 'Service', resources: [] }] }], + }); + const data = expectSuccess(readAssembly(dir)); + + const stage = data.tree.find((n) => n.id === 'Prod')!; + expect(stage.path).toBe('Prod'); + }); + + test('resources inside a Stage stack get logicalId and cfnType', () => { + dir = buildNestedAssembly({ + stages: [{ + id: 'Prod', + stacks: [{ + id: 'Service', + resources: [{ id: 'MyBucket', logicalId: 'MyBucketABC', cfnType: 'AWS::S3::Bucket' }], + }], + }], + }); + const data = expectSuccess(readAssembly(dir)); + + const resource = findNode(data.tree, 'Prod/Service/MyBucket/Resource')!; + expect(resource.logicalId).toBe('MyBucketABC'); + expect(resource.type).toBe('AWS::S3::Bucket'); + }); + + test('multi-stage apps route metadata to the correct nested assembly', () => { + // Two stages, same construct ids — proves we don't cross-contaminate + // metadata from one stage's nested-assembly manifest to another's. + dir = buildNestedAssembly({ + stages: [ + { id: 'Prod', stacks: [{ id: 'Service', resources: [{ id: 'X', logicalId: 'ProdX', cfnType: 'AWS::S3::Bucket' }] }] }, + { id: 'Staging', stacks: [{ id: 'Service', resources: [{ id: 'X', logicalId: 'StagingX', cfnType: 'AWS::S3::Bucket' }] }] }, + ], + }); + const data = expectSuccess(readAssembly(dir)); + + expect(findNode(data.tree, 'Prod/Service/X/Resource')!.logicalId).toBe('ProdX'); + expect(findNode(data.tree, 'Staging/Service/X/Resource')!.logicalId).toBe('StagingX'); + }); +}); + +describe('readAssembly graceful degradation', () => { + let dir: string | undefined; + + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + test('non-TypeScript app returns success with no source enrichment, no crash', () => { + dir = buildNonTypeScriptAssembly(); + + const data = expectSuccess(readAssembly(dir)); + + expect(data.tree).toHaveLength(1); + const stack = data.tree[0]; + expect(stack.id).toBe('Stack1'); + expect(stack.sourceLocation).toBeUndefined(); + }); +}); + +function findNode(nodes: readonly ConstructNode[], targetPath: string): ConstructNode | undefined { + for (const node of nodes) { + if (node.path === targetPath) return node; + const found = findNode(node.children, targetPath); + if (found) return found; + } + return undefined; +} diff --git a/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts new file mode 100644 index 000000000..644e082b9 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts @@ -0,0 +1,124 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { SourceMapResolver } from '../../lib/core/source-resolver'; + +const SOURCE_MAPS_DIR = path.join(__dirname, '..', '_fixtures', 'source-maps'); + +let resolver: SourceMapResolver; + +beforeEach(() => { + // Fresh resolver (and cache) per test so source-map parses don't bleed between cases. + resolver = new SourceMapResolver(); +}); + +// NOTE: choosing WHICH metadata entry's frames to use (LOGICAL_ID.trace vs +// aws:cdk:creationStack) is toolkit-lib's findCreationStackTrace; this resolver +// only turns the chosen frames into a user source location. +describe('SourceMapResolver.resolveFrames', () => { + test('returns undefined for no frames', () => { + expect(resolver.resolveFrames(undefined)).toBeUndefined(); + }); + + test('returns undefined when all frames are skip-placeholders', () => { + // aws-cdk-lib's renderCallStackJustMyCode emits these for filtered frames + // (e.g. node_modules and node: internals). They have no parens and no + // :line:col, so they don't match FRAME_RE. + const frames = [ + ' ...node_modules-aws-cdk-lib...', + ' ...node internals...', + ' (no user code in 10 frames, use --stack-trace-limit to capture more)', + ]; + expect(resolver.resolveFrames(frames)).toBeUndefined(); + }); + + test('skips skip-placeholder frames and picks the first parseable user frame', () => { + const frames = [ + ' ...node_modules-aws-cdk-lib...', + ' at new MyStack (/project/lib/my-stack.ts:42:7)', + ' at Object. (/project/bin/app.ts:8:1)', + ]; + expect(resolver.resolveFrames(frames)).toEqual({ + file: '/project/lib/my-stack.ts', + line: 42, + column: 7, + }); + }); + + test('returns undefined for a non-TypeScript (host-language) frame', () => { + expect(resolver.resolveFrames([' at (/project/app/my_stack.py:42:5)'])).toBeUndefined(); + }); +}); + +describe('source-map resolution', () => { + const SAMPLE_JS = path.join(SOURCE_MAPS_DIR, 'sample.js'); + const SAMPLE_MAP = path.join(SOURCE_MAPS_DIR, 'sample.js.map'); + + const tmpDirs: string[] = []; + afterEach(() => tmpDirs.forEach((d) => fs.rmSync(d, { recursive: true, force: true }))); + + // sample.js body without its trailing sourceMappingURL comment, so each test + // can attach its own (inline or external) form. greet() stays on line 5. + const sampleBody = (): string => + fs.readFileSync(SAMPLE_JS, 'utf-8').replace(/\/\/# sourceMappingURL=.*$/m, '').trimEnd(); + const tmpDir = (): string => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'srcmap-')); + tmpDirs.push(dir); + return dir; + }; + + test('resolves .js to .ts using sibling .js.map', () => { + // sample.js line 5: `function greet(name) {` + const result = resolver.resolveFrames([` at greet (${SAMPLE_JS}:5:10)`]); + expect(result).toBeDefined(); + expect(result!.file).toContain('sample.ts'); + expect(result!.line).toBe(2); // ts line 2 = `export function greet` + }); + + test('falls back to .js location when no .js.map exists', () => { + expect(resolver.resolveFrames([' at someFn (/tmp/no-map-here.js:1:1)'])).toEqual({ + file: '/tmp/no-map-here.js', line: 1, column: 1, + }); + }); + + test('returns .ts location unchanged (no source-map needed)', () => { + expect(resolver.resolveFrames([' at someFn (/project/lib/foo.ts:3:2)'])).toEqual({ + file: '/project/lib/foo.ts', line: 3, column: 2, + }); + }); + + test('resolves an inline (data: URI) source map', () => { + const b64 = Buffer.from(fs.readFileSync(SAMPLE_MAP, 'utf-8'), 'utf-8').toString('base64'); + const dir = tmpDir(); + const jsPath = path.join(dir, 'sample.js'); + fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=data:application/json;base64,${b64}\n`); + + const result = resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + expect(result!.file).toContain('sample.ts'); + expect(result!.line).toBe(2); + }); + + test('resolves an external map referenced under a non-default filename', () => { + const dir = tmpDir(); + fs.copyFileSync(SAMPLE_MAP, path.join(dir, 'renamed.map')); + const jsPath = path.join(dir, 'sample.js'); + fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=renamed.map\n`); + + const result = resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + expect(result!.file).toContain('sample.ts'); + expect(result!.line).toBe(2); + }); + + test('applies sourceRoot, resolving sources relative to the map', () => { + const dir = tmpDir(); + const map = JSON.parse(fs.readFileSync(SAMPLE_MAP, 'utf-8')); + map.sourceRoot = 'nested/'; + fs.writeFileSync(path.join(dir, 'renamed.map'), JSON.stringify(map)); + const jsPath = path.join(dir, 'sample.js'); + fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=renamed.map\n`); + + const result = resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + expect(result!.file).toBe(path.join(dir, 'nested', 'sample.ts')); + expect(result!.line).toBe(2); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts new file mode 100644 index 000000000..79d762e04 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts @@ -0,0 +1,156 @@ +import { pathToFileURL } from 'url'; +import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import type { ConstructNode } from '../../lib'; +import { codeLensesForFile } from '../../lib/lsp/codelens'; + +const FILE = '/p/lib/stack.ts'; +const URI = pathToFileURL(FILE).toString(); +const OTHER_URI = pathToFileURL('/p/lib/other.ts').toString(); + +const node = (overrides: Partial & { path: string }): ConstructNode => ({ + id: overrides.path.split('/').pop() ?? overrides.path, + children: [], + ...overrides, +}); + +describe('codeLensesForFile', () => { + test('returns no lenses when tree is empty', () => { + expect(codeLensesForFile(ConstructIndex.fromTree([]), URI)).toEqual([]); + }); + + test('returns no lenses for non-resource wrapper nodes (no logicalId)', () => { + // L2 wrapper: has sourceLocation but no logicalId/cfnType — they live on + // its `Resource` child. Wrappers shouldn't get their own lens. + const tree = [node({ + path: 'Stack1/MyBucket', + sourceLocation: { file: FILE, line: 12, column: 5 }, + // logicalId/type intentionally omitted + })]; + expect(codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); + }); + + test('emits one lens per resource on its source line', () => { + const tree = [node({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucketF68F3FF0', + type: 'AWS::S3::Bucket', + sourceLocation: { file: FILE, line: 12, column: 5 }, + })]; + + const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + expect(lenses).toHaveLength(1); + expect(lenses[0].range).toEqual({ + start: { line: 11, character: 0 }, + end: { line: 11, character: 0 }, + }); + expect(lenses[0].command?.title).toBe('Creates: AWS::S3::Bucket [logical: MyBucketF68F3FF0]'); + }); + + test('groups multiple resources on the same source line into one lens', () => { + // L2 like Bucket can produce Bucket + BucketPolicy + Key, all anchored + // to the same `new s3.Bucket(...)` line. One lens, listing all. + const tree = [ + node({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'BucketABC', + type: 'AWS::S3::Bucket', + sourceLocation: { file: FILE, line: 12, column: 5 }, + }), + node({ + path: 'Stack1/MyBucket/Policy', + logicalId: 'BucketPolicyDEF', + type: 'AWS::S3::BucketPolicy', + sourceLocation: { file: FILE, line: 12, column: 5 }, + }), + node({ + path: 'Stack1/MyBucket/Key', + logicalId: 'KeyGHI', + type: 'AWS::KMS::Key', + sourceLocation: { file: FILE, line: 12, column: 5 }, + }), + ]; + + const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + expect(lenses).toHaveLength(1); + expect(lenses[0].command?.title).toBe('3 resources: BucketABC, BucketPolicyDEF, KeyGHI'); + }); + + test('emits separate lenses for resources on different lines', () => { + const tree = [ + node({ + path: 'Stack1/A', + logicalId: 'A1', + type: 'AWS::S3::Bucket', + sourceLocation: { file: FILE, line: 10, column: 1 }, + }), + node({ + path: 'Stack1/B', + logicalId: 'B1', + type: 'AWS::SQS::Queue', + sourceLocation: { file: FILE, line: 20, column: 1 }, + }), + ]; + + const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + expect(lenses).toHaveLength(2); + expect(lenses.map((l) => l.range.start.line).sort((a, b) => a - b)).toEqual([9, 19]); + }); + + test('filters out resources from other files', () => { + const tree = [ + node({ + path: 'Stack1/A', + logicalId: 'A1', + type: 'AWS::S3::Bucket', + sourceLocation: { file: FILE, line: 10, column: 1 }, + }), + node({ + path: 'Stack1/B', + logicalId: 'B1', + type: 'AWS::SQS::Queue', + sourceLocation: { file: '/p/lib/other.ts', line: 5, column: 1 }, + }), + ]; + + expect(codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toHaveLength(1); + expect(codeLensesForFile(ConstructIndex.fromTree(tree), OTHER_URI)).toHaveLength(1); + }); + + test('walks descendants — finds resources nested under wrappers', () => { + const tree = [ + node({ + path: 'Stack1', + sourceLocation: { file: FILE, line: 1, column: 1 }, + children: [ + node({ + path: 'Stack1/MyBucket', + sourceLocation: { file: FILE, line: 12, column: 5 }, + children: [ + node({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucketABC', + type: 'AWS::S3::Bucket', + sourceLocation: { file: FILE, line: 12, column: 5 }, + }), + ], + }), + ], + }), + ]; + + const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + expect(lenses).toHaveLength(1); + expect(lenses[0].command?.title).toContain('AWS::S3::Bucket'); + }); + + test('omits resources without sourceLocation (non-TS apps)', () => { + const tree = [node({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucketF68F3FF0', + type: 'AWS::S3::Bucket', + // sourceLocation omitted — non-TS app + })]; + + expect(codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts new file mode 100644 index 000000000..27ae54670 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts @@ -0,0 +1,177 @@ +import { pathToFileURL } from 'url'; +import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import type { PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; +import { DiagnosticSeverity } from 'vscode-languageserver/node'; +import type { ConstructNode } from '../../lib'; +import { mapViolationsToDiagnostics } from '../../lib/lsp/diagnostics'; + +const nodeWithLocation = (path: string, file: string, line: number, column: number): ConstructNode => ({ + path, + id: path.split('/').pop() ?? path, + children: [], + sourceLocation: { file, line, column }, +}); + +const nodeNoLocation = (path: string): ConstructNode => ({ + path, + id: path.split('/').pop() ?? path, + children: [], +}); + +const indexOf = (...nodes: ConstructNode[]): ConstructIndex => ConstructIndex.fromTree(nodes); + +const reportWith = (...violations: Array<{ + ruleName: string; + description?: string; + severity: 'fatal' | 'error' | 'warning' | 'info' | 'custom'; + suggestedFix?: string; + paths: string[]; +}>): PolicyValidationReportJson => ({ + version: '1.0.0', + pluginReports: [{ + pluginName: 'test-plugin', + conclusion: 'failure', + violations: violations.map((v) => ({ + ruleName: v.ruleName, + description: v.description ?? v.ruleName, + severity: v.severity, + suggestedFix: v.suggestedFix, + violatingConstructs: v.paths.map((p) => ({ constructPath: p })), + })), + }], +}); + +describe('mapViolationsToDiagnostics', () => { + test('returns empty maps when violations is undefined', () => { + const { byUri, dropped } = mapViolationsToDiagnostics(undefined, indexOf()); + expect(byUri.size).toBe(0); + expect(dropped).toHaveLength(0); + }); + + test('emits one diagnostic per violating construct, grouped by URI', () => { + const tree = indexOf(nodeWithLocation('S/MyBucket', '/p/lib/s.ts', 12, 5)); + const report = reportWith({ + ruleName: 'no-public-buckets', + description: 'S3 must not be public', + severity: 'error', + paths: ['S/MyBucket'], + }); + + const { byUri, dropped } = mapViolationsToDiagnostics(report, tree); + + const expectedUri = pathToFileURL('/p/lib/s.ts').toString(); + expect(byUri.get(expectedUri)).toHaveLength(1); + expect(dropped).toHaveLength(0); + + const diag = byUri.get(expectedUri)![0]; + expect(diag.severity).toBe(DiagnosticSeverity.Error); + expect(diag.code).toBe('no-public-buckets'); + expect(diag.source).toBe('test-plugin'); + // Range starts at construct creation (1-based -> 0-based) and extends + // to LSP's end-of-line sentinel so the squiggle is visible. + expect(diag.range).toEqual({ + start: { line: 11, character: 4 }, + end: { line: 11, character: Number.MAX_VALUE }, + }); + }); + + test('groups multiple violations on the same file under one URI', () => { + const tree = indexOf( + nodeWithLocation('S/A', '/p/lib/s.ts', 10, 1), + nodeWithLocation('S/B', '/p/lib/s.ts', 20, 1), + ); + const report = reportWith( + { ruleName: 'r1', severity: 'error', paths: ['S/A'] }, + { ruleName: 'r2', severity: 'warning', paths: ['S/B'] }, + ); + + const { byUri } = mapViolationsToDiagnostics(report, tree); + const uri = pathToFileURL('/p/lib/s.ts').toString(); + expect(byUri.get(uri)).toHaveLength(2); + }); + + test('maps severity correctly', () => { + const tree = indexOf( + nodeWithLocation('S/F', '/x.ts', 1, 1), + nodeWithLocation('S/E', '/x.ts', 2, 1), + nodeWithLocation('S/W', '/x.ts', 3, 1), + nodeWithLocation('S/I', '/x.ts', 4, 1), + nodeWithLocation('S/C', '/x.ts', 5, 1), + ); + const report = reportWith( + { ruleName: 'fatal', severity: 'fatal', paths: ['S/F'] }, + { ruleName: 'error', severity: 'error', paths: ['S/E'] }, + { ruleName: 'warning', severity: 'warning', paths: ['S/W'] }, + { ruleName: 'info', severity: 'info', paths: ['S/I'] }, + { ruleName: 'custom', severity: 'custom', paths: ['S/C'] }, + ); + + const { byUri } = mapViolationsToDiagnostics(report, tree); + const diags = byUri.get(pathToFileURL('/x.ts').toString())!; + const byCode = new Map(diags.map((d) => [d.code, d.severity])); + expect(byCode.get('fatal')).toBe(DiagnosticSeverity.Error); + expect(byCode.get('error')).toBe(DiagnosticSeverity.Error); + expect(byCode.get('warning')).toBe(DiagnosticSeverity.Warning); + expect(byCode.get('info')).toBe(DiagnosticSeverity.Information); + expect(byCode.get('custom')).toBe(DiagnosticSeverity.Information); + }); + + test('appends suggestedFix to the message when present', () => { + const tree = indexOf(nodeWithLocation('S/X', '/x.ts', 1, 1)); + const report = reportWith({ + ruleName: 'r', + description: 'do not do that', + severity: 'error', + suggestedFix: 'do this instead', + paths: ['S/X'], + }); + + const { byUri } = mapViolationsToDiagnostics(report, tree); + const diag = byUri.get(pathToFileURL('/x.ts').toString())![0]; + expect(diag.message).toContain('do not do that'); + expect(diag.message).toContain('Suggested fix: do this instead'); + }); + + test('drops violations whose constructPath is unknown', () => { + const tree = indexOf(); + const report = reportWith({ ruleName: 'r', severity: 'error', paths: ['S/Missing'] }); + const { byUri, dropped } = mapViolationsToDiagnostics(report, tree); + + expect(byUri.size).toBe(0); + expect(dropped).toHaveLength(1); + expect(dropped[0]).toMatchObject({ + ruleName: 'r', + constructPath: 'S/Missing', + reason: expect.stringContaining('construct tree'), + }); + }); + + test('drops violations whose construct has no sourceLocation (non-TS app)', () => { + const tree = indexOf(nodeNoLocation('S/MyBucket')); + const report = reportWith({ ruleName: 'r', severity: 'error', paths: ['S/MyBucket'] }); + + const { byUri, dropped } = mapViolationsToDiagnostics(report, tree); + expect(byUri.size).toBe(0); + expect(dropped).toHaveLength(1); + expect(dropped[0].reason).toContain('no source location'); + }); + + test('drops violations whose source file is not TypeScript', () => { + const tree = indexOf(nodeWithLocation('S/X', '/p/lib/x.py', 1, 1)); + const report = reportWith({ ruleName: 'r', severity: 'error', paths: ['S/X'] }); + + const { byUri, dropped } = mapViolationsToDiagnostics(report, tree); + expect(byUri.size).toBe(0); + expect(dropped[0].reason).toContain('not TypeScript'); + }); + + test('anchors at the top of the file when line/column are unknown', () => { + const tree = indexOf(nodeWithLocation('S/X', '/p/lib/stack.ts', 0, 0)); + const report = reportWith({ ruleName: 'r', severity: 'error', paths: ['S/X'] }); + + const { byUri, dropped } = mapViolationsToDiagnostics(report, tree); + expect(dropped).toHaveLength(0); + const diags = [...byUri.values()][0]; + expect(diags[0].range.start).toEqual({ line: 0, character: 0 }); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index e3860667c..6b2c31ad3 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -1,99 +1,43 @@ -import { PassThrough } from 'stream'; -import type { MessageConnection } from 'vscode-jsonrpc/node'; -import { - createMessageConnection, - StreamMessageReader, - StreamMessageWriter, -} from 'vscode-jsonrpc/node'; -import { startServer, type LspServerOptions } from '../../lib/lsp/server'; - -interface TestClient { - connection: MessageConnection; - serverIn: PassThrough; - serverOut: PassThrough; +import { pathToFileURL } from 'url'; +import type { Diagnostic } from 'vscode-languageserver/node'; +import type { AssemblyReadResult } from '../../lib'; +import { createLspHandlers, type LspHandlerOptions, type LspHandlers } from '../../lib/lsp/server'; + +interface CapturedClient { + handlers: LspHandlers; + published: Array<{ uri: string; diagnostics: Diagnostic[] }>; + log: { warn: jest.Mock; error: jest.Mock }; } -function createTestClient(opts?: Partial>): TestClient { - const serverIn = new PassThrough(); - const serverOut = new PassThrough(); - - startServer({ - readable: serverIn, - writable: serverOut, +function createTestClient(opts?: Partial>): CapturedClient { + const published: Array<{ uri: string; diagnostics: Diagnostic[] }> = []; + const log = { warn: jest.fn(), error: jest.fn() }; + const handlers = createLspHandlers({ onSynthRequest: opts?.onSynthRequest, + // Default to "no assembly" so tests that don't care about diagnostics + // don't need a fake fixture. Tests that do care override this. + readAssembly: opts?.readAssembly ?? (() => ({ status: 'not-found' })), + logger: log, + onPublishDiagnostics: (uri, diagnostics) => published.push({ uri, diagnostics }), }); - - const connection = createMessageConnection( - new StreamMessageReader(serverOut), - new StreamMessageWriter(serverIn), - ); - connection.listen(); - - return { connection, serverIn, serverOut }; + return { handlers, published, log }; } -async function initializeClient(client: TestClient, options?: Record): Promise { - // processId: null prevents vscode-languageserver from registering an - // undisposable parent-process watchdog timer that hangs jest. - await client.connection.sendRequest('initialize', { +function initializeClient(client: CapturedClient, options?: Record): void { + client.handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: options ?? {}, }); - await client.connection.sendNotification('initialized'); -} - -interface LogMessage { - type: number; - message: string; -} - -interface Deferred { - promise: Promise; - resolve(value: T): void; -} - -function deferred(): Deferred { - let resolve!: (value: T) => void; - const promise = new Promise((res) => { - resolve = res; - }); - return { promise, resolve }; -} - -// didSave is a fire-and-forget notification. Tests gate on a deferred that -// the relevant callback (onSynthRequest, or window/logMessage) resolves — -// no setImmediate / setTimeout polling. -const TIMEOUT_MS = 1000; - -async function withTimeout(promise: Promise, label: string): Promise { - let timer: NodeJS.Timeout | undefined; - const timeout = new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error(`Timed out waiting for ${label}`)), TIMEOUT_MS); - }); - try { - return await Promise.race([promise, timeout]); - } finally { - if (timer) clearTimeout(timer); - } + client.handlers.onInitialized(); } describe('LSP Server', () => { - let testClient: TestClient; - - afterEach(() => { - if (testClient) { - testClient.connection.dispose(); - testClient.serverIn.end(); - testClient.serverOut.end(); - } - }); - - test('responds to initialize with capabilities', async () => { - testClient = createTestClient(); + test('responds to initialize with capabilities', () => { + const client = createTestClient(); - const result = await testClient.connection.sendRequest('initialize', { + const result = client.handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, @@ -107,93 +51,177 @@ describe('LSP Server', () => { change: 0, save: { includeText: false }, }, + codeLensProvider: { resolveProvider: false }, }, }); }); - test('didSave triggers onSynthRequest for source files', async () => { - const seen = deferred(); - testClient = createTestClient({ - onSynthRequest: (dir) => seen.resolve(dir), + test('didSave triggers onSynthRequest for source files', () => { + const synthRequests: string[] = []; + const client = createTestClient({ + onSynthRequest: (dir) => synthRequests.push(dir), }); - await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + initializeClient(client, { applicationDir: '/tmp/test-project' }); - await testClient.connection.sendNotification('textDocument/didSave', { + client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, }); - await expect(withTimeout(seen.promise, 'onSynthRequest')).resolves.toBe('/tmp/test-project'); + expect(synthRequests).toEqual(['/tmp/test-project']); }); - test('didSave does not trigger for ignored files', async () => { + test('didSave does not trigger for ignored files', () => { const synthRequests: string[] = []; - testClient = createTestClient({ + const client = createTestClient({ onSynthRequest: (dir) => synthRequests.push(dir), }); - await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + initializeClient(client, { applicationDir: '/tmp/test-project' }); - await testClient.connection.sendNotification('textDocument/didSave', { + client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/node_modules/foo/index.ts' }, }); - await testClient.connection.sendNotification('textDocument/didSave', { + client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/cdk.out/tree.json' }, }); - // Round-trip a request to drain the server's notification queue past the - // two didSaves, then assert the synth callback was never invoked. - await testClient.connection.sendRequest('shutdown'); expect(synthRequests).toEqual([]); }); - test('didSave is a no-op when onSynthRequest is not provided', async () => { - testClient = createTestClient(); - await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + test('didSave does not throw without onSynthRequest configured', () => { + const client = createTestClient(); + initializeClient(client, { applicationDir: '/tmp/test-project' }); - await testClient.connection.sendNotification('textDocument/didSave', { + expect(() => client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, - }); + })).not.toThrow(); - // Server stays responsive after didSave with no callback configured. - await testClient.connection.sendRequest('shutdown'); + // Server should still be responsive after didSave with no callback + expect(() => client.handlers.onShutdown()).not.toThrow(); }); - test('didSave is ignored after shutdown', async () => { + test('shutdown completes without error', () => { + const client = createTestClient(); + initializeClient(client); + + expect(() => client.handlers.onShutdown()).not.toThrow(); + }); + + test('didSave is ignored after shutdown', () => { const synthRequests: string[] = []; - testClient = createTestClient({ + const client = createTestClient({ onSynthRequest: (dir) => synthRequests.push(dir), }); - await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + initializeClient(client, { applicationDir: '/tmp/test-project' }); - await testClient.connection.sendRequest('shutdown'); + client.handlers.onShutdown(); - await testClient.connection.sendNotification('textDocument/didSave', { + client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, }); - // Best-effort drain: the post-shutdown didSave is a notification, no - // request to await. Re-issuing shutdown rides the same queue and acks - // only after the prior didSave has been processed. - await testClient.connection.sendRequest('shutdown'); expect(synthRequests).toEqual([]); }); - test('onSynthRequest errors are surfaced via window/logMessage', async () => { - const logged = deferred(); - testClient = createTestClient({ + test('onSynthRequest errors are caught gracefully', () => { + const client = createTestClient({ onSynthRequest: () => { throw new Error('synth failed'); }, }); - testClient.connection.onNotification('window/logMessage', (params: LogMessage) => { - if (params.message.includes('synth failed')) logged.resolve(params); - }); - await initializeClient(testClient, { applicationDir: '/tmp/test-project' }); + initializeClient(client, { applicationDir: '/tmp/test-project' }); - await testClient.connection.sendNotification('textDocument/didSave', { + expect(() => client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, + })).not.toThrow(); + + // Server should still be responsive after the error + expect(() => client.handlers.onShutdown()).not.toThrow(); + }); + + test('publishes diagnostics on initialized when assembly has violations', () => { + const client = createTestClient({ + readAssembly: () => ({ + status: 'success', + data: { + warnings: [], + tree: [{ + path: 'Stack1', + id: 'Stack1', + children: [{ + path: 'Stack1/MyBucket', + id: 'MyBucket', + children: [], + sourceLocation: { file: '/p/lib/stack.ts', line: 12, column: 5 }, + }], + }], + violations: { + version: '1.0.0', + pluginReports: [{ + pluginName: 'test-plugin', + conclusion: 'failure', + violations: [{ + ruleName: 'no-public-buckets', + description: 'no public buckets', + severity: 'error', + violatingConstructs: [{ constructPath: 'Stack1/MyBucket' }], + }], + }], + }, + }, + }), + }); + + initializeClient(client, { applicationDir: '/p' }); + + expect(client.published).toHaveLength(1); + expect(client.published[0].uri).toContain('stack.ts'); + expect(client.published[0].diagnostics).toHaveLength(1); + }); + + test('responds to codeLens with resources for the requested file', () => { + const stackTs = '/p/lib/stack.ts'; + const stackUri = pathToFileURL(stackTs).toString(); + + const client = createTestClient({ + readAssembly: (): AssemblyReadResult => ({ + status: 'success', + data: { + warnings: [], + tree: [{ + path: 'Stack1', + id: 'Stack1', + children: [{ + path: 'Stack1/MyBucket/Resource', + id: 'Resource', + logicalId: 'MyBucketABC', + type: 'AWS::S3::Bucket', + sourceLocation: { file: stackTs, line: 12, column: 5 }, + children: [], + }], + }], + }, + }), + }); + + initializeClient(client, { applicationDir: '/p' }); + + const lenses = client.handlers.onCodeLens({ + textDocument: { uri: stackUri }, }); - const message = await withTimeout(logged.promise, 'window/logMessage'); - expect(message.message).toContain('Synth request failed: synth failed'); + expect(lenses).toHaveLength(1); + expect(lenses[0].range.start.line).toBe(11); // 1-based 12 -> 0-based 11 + expect(lenses[0].command?.title).toContain('AWS::S3::Bucket'); + expect(lenses[0].command?.title).toContain('MyBucketABC'); + }); + + test('publishes nothing when assembly is not-found (pre-synth)', () => { + const client = createTestClient({ + readAssembly: () => ({ status: 'not-found' }), + }); + + initializeClient(client, { applicationDir: '/p' }); + + expect(client.published).toHaveLength(0); }); }); diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json index f07ed00a5..ac3e8501d 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json @@ -25,6 +25,7 @@ "stripInternal": true, "target": "ES2020", "types": [ + "convert-source-map", "express", "jest", "node" diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.json b/packages/@aws-cdk/cdk-explorer/tsconfig.json index 3cb3b5bcd..7324429b1 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.json @@ -27,6 +27,7 @@ "stripInternal": true, "target": "ES2020", "types": [ + "convert-source-map", "express", "jest", "node" diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts new file mode 100644 index 000000000..3bf1c4a19 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts @@ -0,0 +1,193 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { ArtifactMetadataEntryType, CFN_RESOURCE_TYPE_ATTRIBUTE, type MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; +import type { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; +import type { CloudAssembly } from './cloud-assembly'; + +/** Construct ids that aws-cdk-lib injects into the tree but aren't user constructs. */ +const CDK_INTERNAL_IDS = new Set(['Tree', 'CDKMetadata', 'BootstrapVersion', 'CheckBootstrapVersion']); + +/** + * A construct from tree.json joined with the CloudFormation metadata of the + * stack it belongs to: the construct path/id plus, for CFN resources, the + * logical ID and resource type. Readers that need more (for example a source + * location) extend this and build the richer tree via {@link buildConstructTree}. + */ +export interface ConstructTreeNode { + readonly path: string; + readonly id: string; + /** CFN resource type (e.g. "AWS::S3::Bucket"), if this construct is a CFN resource. */ + readonly type?: string; + /** CFN logical ID, if this construct maps to a CFN resource. */ + readonly logicalId?: string; + readonly children: readonly ConstructTreeNode[]; +} + +/** + * The generic fields {@link buildConstructTree} computes for each node before a + * consumer decorates it. `children` are already the decorated node type. + */ +export interface ConstructTreeNodeFields { + readonly path: string; + readonly id: string; + readonly type?: string; + readonly logicalId?: string; + readonly children: readonly T[]; +} + +/** + * Produces a node of type `T` from the generic fields, the owning stack + * artifact (if any), and the construct path. Readers use the stack + path to + * attach extra data (for example a source location traced from the stack). + */ +export type ConstructNodeDecorator = ( + fields: ConstructTreeNodeFields, + stack: CloudFormationStackArtifact | undefined, + constructPath: string, +) => T; + +/** + * A pre-built index over a construct tree. Walks the tree once and exposes + * O(1) path lookup plus pre-order iteration over every node, so callers never + * re-implement the recursive descent over `children`. + * + * Generic over the concrete node type, so callers that store richer nodes get + * those richer nodes back from `byPath()` and iteration. + */ +export class ConstructIndex implements Iterable { + /** Build an index from the roots of a construct tree. */ + public static fromTree(tree: readonly T[]): ConstructIndex { + const byPath = new Map(); + // Pre-order insertion: iteration order below relies on the Map preserving it. + visit(tree, (node) => byPath.set(node.path, node)); + return new ConstructIndex(byPath); + } + + private constructor(private readonly byPathMap: Map) { + } + + /** The node at a given construct path, or undefined if absent. */ + public byPath(constructPath: string): T | undefined { + return this.byPathMap.get(constructPath); + } + + /** Number of nodes in the tree. */ + public get size(): number { + return this.byPathMap.size; + } + + /** Pre-order iteration over every node in the tree. */ + public [Symbol.iterator](): Iterator { + return this.byPathMap.values(); + } +} + +/** The single recursive descent over a construct tree. */ +function visit(nodes: readonly T[], fn: (node: T) => void): void { + for (const node of nodes) { + fn(node); + // In a homogeneous tree the children are the same concrete node type. + if (node.children.length > 0) visit(node.children as readonly T[], fn); + } +} + +/** + * Read a cloud assembly's tree.json and join it with each stack's manifest + * metadata to produce a construct tree where every CFN resource carries its + * logicalId and CFN type. + * + * Supports: + * - Flat assemblies (stacks directly under App). + * - Stage-based apps: each Stage produces a nested cloud-assembly artifact + * under assembly-STAGE/. stacksRecursively descends into them. + * - NestedStack contents: aws-cdk-lib emits the per-resource metadata for + * nested-stack constructs into the PARENT stack's artifact metadata, + * keyed by full construct path. + * + * The `decorate` callback turns each node's generic fields and its metadata + * entries into the concrete node type (e.g. attaching a resolved source location). + */ +export function buildConstructTree( + assembly: CloudAssembly, + decorate: ConstructNodeDecorator, +): T[] { + const treeArtifact = assembly.tree(); + if (!treeArtifact) return []; + + const rawTree = loadTree(path.join(assembly.directory, treeArtifact.file)); + if (!rawTree) return []; + + const stackIndex = buildStackIndex(assembly.stacksRecursively); + return Object.values(rawTree.children ?? {}) + .filter((child) => !isCdkInternal(child.id)) + .map((child) => buildNode(child, stackIndex, undefined, decorate)); +} + +/** + * Raw tree.json node. The root tree.json holds the FULL hierarchy including + * Stage children; nested-assembly subdirs don't shard tree.json. + */ +interface RawTreeNode { + readonly id: string; + readonly path: string; + readonly children?: { [key: string]: RawTreeNode }; + readonly attributes?: { [key: string]: unknown }; + readonly constructInfo?: { readonly fqn: string; readonly version: string }; +} + +/** A stack artifact plus its metadata Map, keyed (in the index) by construct path. */ +interface StackMetadata { + readonly stack: CloudFormationStackArtifact; + readonly metadata: Map; +} +type StackMetadataIndex = Map; + +function loadTree(treePath: string): RawTreeNode | undefined { + if (!fs.existsSync(treePath)) return undefined; + const content = JSON.parse(fs.readFileSync(treePath, 'utf-8')); + return content.tree; +} + +/** + * Index a stack's metadata by its tree-path key. Uses hierarchicalId, which + * resolves to stack.node.path for everything aws-cdk-lib emits. Caches the + * metadata Map because stack.metadata re-reads from disk on each access. + */ +function buildStackIndex(stacks: CloudFormationStackArtifact[]): StackMetadataIndex { + const index: StackMetadataIndex = new Map(); + for (const stack of stacks) { + index.set(stack.hierarchicalId, { stack, metadata: new Map(Object.entries(stack.metadata)) }); + } + return index; +} + +function buildNode( + raw: RawTreeNode, + stackIndex: StackMetadataIndex, + inheritedStack: StackMetadata | undefined, + decorate: ConstructNodeDecorator, +): T { + // When a node IS a stack, switch to that stack. Otherwise inherit the + // parent's: this routes NestedStack children to the parent's metadata, + // since aws-cdk-lib emits their entries there. + const owner = stackIndex.get(raw.path) ?? inheritedStack; + + // Metadata keys carry a leading "/", construct paths in tree.json don't. + const entries = owner?.metadata.get('/' + raw.path) ?? []; + + const logicalIdEntry = entries.find((e) => e.type === ArtifactMetadataEntryType.LOGICAL_ID); + const logicalId = typeof logicalIdEntry?.data === 'string' ? logicalIdEntry.data : undefined; + + const cfnTypeRaw = raw.attributes?.[CFN_RESOURCE_TYPE_ATTRIBUTE]; + const cfnType = typeof cfnTypeRaw === 'string' ? cfnTypeRaw : undefined; + + const children = Object.values(raw.children ?? {}) + .filter((child) => !isCdkInternal(child.id)) + .map((child) => buildNode(child, stackIndex, owner, decorate)); + + return decorate({ path: raw.path, id: raw.id, type: cfnType, logicalId, children }, owner?.stack, raw.path); +} + +function isCdkInternal(id: string): boolean { + return CDK_INTERNAL_IDS.has(id); +} diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/index.ts b/packages/@aws-cdk/cloud-assembly-api/lib/index.ts index f32343237..736566961 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/index.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/index.ts @@ -19,3 +19,4 @@ export * from './metadata'; export * from './placeholders'; export * from './environment'; export * from './bootstrap'; +export * from './construct-tree'; diff --git a/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts b/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts new file mode 100644 index 000000000..0ca226bec --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts @@ -0,0 +1,154 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { buildConstructTree, CloudAssembly, ConstructIndex, type ConstructTreeNode } from '../lib'; +import { rimraf } from './util'; + +const node = (nodePath: string, children: ConstructTreeNode[] = []): ConstructTreeNode => ({ + path: nodePath, + id: nodePath.split('/').pop() ?? nodePath, + children, +}); + +describe('ConstructIndex', () => { + test('empty tree has size 0 and no nodes', () => { + const index = ConstructIndex.fromTree([]); + expect(index.size).toBe(0); + expect([...index]).toEqual([]); + }); + + test('byPath looks up nodes in a flat tree', () => { + const index = ConstructIndex.fromTree([node('Stack1'), node('Stack2')]); + expect(index.byPath('Stack1')!.path).toBe('Stack1'); + expect(index.byPath('Stack2')!.path).toBe('Stack2'); + expect(index.byPath('Missing')).toBeUndefined(); + }); + + test('byPath indexes deep descendants', () => { + const tree = [ + node('Stack1', [ + node('Stack1/MyBucket', [ + node('Stack1/MyBucket/Resource'), + ]), + ]), + ]; + const index = ConstructIndex.fromTree(tree); + expect(index.byPath('Stack1/MyBucket/Resource')).toBeDefined(); + expect(index.size).toBe(3); + }); + + test('iterates every node in pre-order', () => { + const tree = [ + node('Stack1', [ + node('Stack1/A'), + node('Stack1/B', [node('Stack1/B/Resource')]), + ]), + ]; + const paths = [...ConstructIndex.fromTree(tree)].map((n) => n.path); + expect(paths).toEqual(['Stack1', 'Stack1/A', 'Stack1/B', 'Stack1/B/Resource']); + }); + + test('preserves the concrete node type through byPath and iteration', () => { + interface Rich extends ConstructTreeNode { + readonly children: readonly Rich[]; + readonly label: string; + } + const rich = (richPath: string, label: string): Rich => ({ path: richPath, id: richPath, label, children: [] }); + const index = ConstructIndex.fromTree([rich('A', 'alpha')]); + expect(index.byPath('A')?.label).toBe('alpha'); + expect([...index][0].label).toBe('alpha'); + }); +}); + +describe('buildConstructTree', () => { + let dir: string; + afterEach(() => dir && rimraf(dir)); + + // Deliberately non-default so the tests prove we read the filename from the + // manifest's tree artifact rather than assuming "tree.json". + const TREE_FILE = 'foo.tree.json'; + + function writeAssembly(opts: { withTree: boolean }): string { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'caa-tree-')); + const artifacts: Record = { + MyStack: { + type: 'aws:cloudformation:stack', + environment: 'aws://111/us-east-1', + properties: { templateFile: 'template.json' }, + metadata: { + '/MyStack/Bucket/Resource': [{ type: 'aws:cdk:logicalId', data: 'BucketABC' }], + }, + }, + }; + if (opts.withTree) { + artifacts.Tree = { type: 'cdk:tree', properties: { file: TREE_FILE } }; + } + fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify({ version: '0.0.0', artifacts })); + fs.writeFileSync(path.join(dir, 'template.json'), '{}'); + if (opts.withTree) { + const tree = { + version: 'tree-0.1', + tree: { + id: 'App', + path: '', + children: { + MyStack: { + id: 'MyStack', + path: 'MyStack', + children: { + Bucket: { + id: 'Bucket', + path: 'MyStack/Bucket', + children: { + Resource: { + id: 'Resource', + path: 'MyStack/Bucket/Resource', + attributes: { 'aws:cdk:cloudformation:type': 'AWS::S3::Bucket' }, + }, + }, + }, + // Internal node that must be filtered out. + CDKMetadata: { id: 'CDKMetadata', path: 'MyStack/CDKMetadata' }, + }, + }, + }, + }, + }; + fs.writeFileSync(path.join(dir, TREE_FILE), JSON.stringify(tree)); + } + return dir; + } + + test('reads the tree filename from the manifest and joins logicalId + CFN type', () => { + const assembly = new CloudAssembly(writeAssembly({ withTree: true })); + const tree = buildConstructTree(assembly, (fields) => fields); + + const resource = ConstructIndex.fromTree(tree).byPath('MyStack/Bucket/Resource'); + expect(resource?.type).toBe('AWS::S3::Bucket'); + expect(resource?.logicalId).toBe('BucketABC'); + }); + + test('filters cdk-internal nodes (e.g. CDKMetadata)', () => { + const assembly = new CloudAssembly(writeAssembly({ withTree: true })); + const paths = [...ConstructIndex.fromTree(buildConstructTree(assembly, (f) => f))].map((n) => n.path); + expect(paths).toContain('MyStack/Bucket'); + expect(paths).not.toContain('MyStack/CDKMetadata'); + }); + + test('passes the owning stack and construct path to the decorate callback', () => { + const assembly = new CloudAssembly(writeAssembly({ withTree: true })); + const tree = buildConstructTree(assembly, (fields, stack, constructPath) => ({ + ...fields, + stackId: stack?.id, + decoratedPath: constructPath, + })); + const resource = ConstructIndex.fromTree(tree).byPath('MyStack/Bucket/Resource'); + expect((resource as any)?.decoratedPath).toBe('MyStack/Bucket/Resource'); + expect((resource as any)?.stackId).toBe('MyStack'); + }); + + test('returns an empty tree when the assembly has no tree artifact', () => { + const assembly = new CloudAssembly(writeAssembly({ withTree: false })); + expect(buildConstructTree(assembly, (f) => f)).toEqual([]); + }); +}); diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts index 785137fba..0e2c681e8 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts @@ -353,6 +353,13 @@ export enum ArtifactMetadataEntryType { PROPERTY_ASSIGNMENT = 'aws:cdk:propertyAssignment', } +/** + * tree.json node attribute carrying a construct's CloudFormation resource type + * (e.g. "AWS::S3::Bucket"). This is a construct-tree node attribute, distinct + * from the manifest metadata entries in {@link ArtifactMetadataEntryType}. + */ +export const CFN_RESOURCE_TYPE_ATTRIBUTE = 'aws:cdk:cloudformation:type'; + /** * A metadata entry in a cloud assembly artifact. */ diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index 048c7020a..be5acf996 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -14,6 +14,9 @@ import type * as integ from './integ-tests'; // see exec.ts#createAssembly export const VERSION_MISMATCH: string = 'Cloud assembly schema version mismatch'; +/** Canonical filename of the policy-validation report emitted into a cloud assembly. */ +export const VALIDATION_REPORT_FILE = 'validation-report.json'; + /** * CLI version is created at build and release time * diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/index.ts b/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/index.ts index fcb073fef..46e5f1757 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/index.ts @@ -1 +1,2 @@ export * from './types'; +export { findCreationStackTrace, findMutationStackTraces } from './private/stack-source-tracing'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/private/stack-source-tracing.ts b/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/private/stack-source-tracing.ts index a28a989f4..380b12887 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/private/stack-source-tracing.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/source-tracing/private/stack-source-tracing.ts @@ -79,7 +79,7 @@ export class StackArtifactSourceTracer implements ISourceTracer { * * Stack's currently don't emit any stack traces. */ -function findCreationStackTrace(stack: cxapi.CloudFormationStackArtifact, constructPath: string): string[] | undefined { +export function findCreationStackTrace(stack: cxapi.CloudFormationStackArtifact, constructPath: string): string[] | undefined { const candidates = [ // logical ID traces ...resourceMetadata(stack, constructPath, cxschema.ArtifactMetadataEntryType.LOGICAL_ID).flatMap((m) => { @@ -106,7 +106,11 @@ function findCreationStackTrace(stack: cxapi.CloudFormationStackArtifact, constr * These are all places in the code where a property's value is overwritten after * construct creation. */ -function findMutationStackTraces(stack: cxapi.CloudFormationStackArtifact, constructPath: string, propertyName: string): string[][] | undefined { +export function findMutationStackTraces( + stack: cxapi.CloudFormationStackArtifact, + constructPath: string, + propertyName: string, +): string[][] | undefined { const assignments = resourceMetadata(stack, constructPath, 'aws:cdk:propertyAssignment'); return assignments.flatMap((m) => { diff --git a/yarn.lock b/yarn.lock index 18014ba0b..73edb75c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -179,13 +179,16 @@ __metadata: "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/toolkit-lib": "npm:^0.0.0" "@cdklabs/eslint-plugin": "npm:^2.0.6" + "@jridgewell/trace-mapping": "npm:^0.3" "@stylistic/eslint-plugin": "npm:^3" + "@types/convert-source-map": "npm:^2" "@types/express": "npm:^4" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^20" "@typescript-eslint/eslint-plugin": "npm:^8" "@typescript-eslint/parser": "npm:^8" constructs: "npm:^10.0.0" + convert-source-map: "npm:^2" eslint: "npm:^9" eslint-config-prettier: "npm:^10.1.8" eslint-import-resolver-typescript: "npm:^4.4.4" @@ -4802,7 +4805,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3, @jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -6675,6 +6678,13 @@ __metadata: languageName: node linkType: hard +"@types/convert-source-map@npm:^2": + version: 2.0.3 + resolution: "@types/convert-source-map@npm:2.0.3" + checksum: 10c0/43dd8ccad61489c245342220db74c1baf3b75586074f99609943fe1bdecf7d5dcff0acd038cb0063dd7533a90cc980101d5899afa70a638883752ad8d66de20b + languageName: node + linkType: hard + "@types/cors@npm:^2.8.6": version: 2.8.19 resolution: "@types/cors@npm:2.8.19" @@ -9560,7 +9570,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^2.0.0": +"convert-source-map@npm:^2, convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b From 151119b876ff6712cb1bcaddabba23361fdac729 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:25:30 -0400 Subject: [PATCH 06/28] feat: explorer codelens nav + template resource locations (#1617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on #1592, which surfaced display-only CodeLens entries for the CFN resources each construct produces. This PR makes them **clickable**. Selecting a lens jumps to the resource's definition in the synthesized CloudFormation template. Features: - **Clickable navigation** — each lens carries an `openResource` command that opens the resource's template file at its logical-ID line. - **Multi-resource picker** — constructs producing several resources show a QuickPick; single-resource constructs open directly. - **Positional `templateFile` resolution (cloud-assembly-api)** — `buildConstructTree` threads the owning template through the tree, switching at NestedStack boundaries. - **Clearer titles** — `Creates AWS::S3::Bucket`, or `Creates 3 resources: AWS::S3::Bucket, AWS::S3::BucketPolicy, AWS::KMS::Key`. ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../@aws-cdk/cdk-explorer/lib/lsp/codelens.ts | 61 ++++++-- .../cdk-explorer/lib/lsp/template-locator.ts | 52 +++++++ packages/@aws-cdk/cdk-explorer/package.json | 8 +- .../cdk-explorer/test/_fixtures/builders.ts | 132 +++++++++++++---- .../test/core/assembly-reader.test.ts | 135 +++++++++++++++++ .../cdk-explorer/test/lsp/codelens.test.ts | 92 +++++++++++- .../cdk-explorer/test/lsp/server.test.ts | 3 +- .../test/lsp/template-locator.test.ts | 103 +++++++++++++ .../cloud-assembly-api/lib/construct-tree.ts | 127 ++++++++++++++-- .../test/construct-tree.test.ts | 137 ++++++++++++++++++ yarn.lock | 8 +- 11 files changed, 790 insertions(+), 68 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts index c0a980266..bd07de5c0 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts @@ -1,9 +1,13 @@ import { pathToFileURL } from 'url'; import type { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; -import { type CodeLens, type Range } from 'vscode-languageserver/node'; +import { type CodeLens, type Command, type Range } from 'vscode-languageserver/node'; +import { resourceTarget, type ResourceTarget } from './template-locator'; import type { ConstructNode } from '../core/assembly-reader'; import type { SourceLocation } from '../core/source-resolver'; +/** Command the client registers to open a resource in its template. */ +export const OPEN_RESOURCE_COMMAND = 'cdkExplorer.openResource'; + /** * Build CodeLens entries for a single source file. For every construct whose * sourceLocation matches fileUri, group by line and emit one lens per line @@ -12,23 +16,41 @@ import type { SourceLocation } from '../core/source-resolver'; export function codeLensesForFile(index: ConstructIndex, fileUri: string): CodeLens[] { const matches = [...index] .filter((node) => isResourceOnFile(node, fileUri)) - .map((node) => ({ - line: node.sourceLocation.line, - resource: { logicalId: node.logicalId, cfnType: node.type }, - })); + .map((node) => ({ line: node.sourceLocation.line, node })); // Multiple resources can map to one line when an L2 construct fans out // (e.g. an L2 producing a primary resource + auxiliary resources). - // Empty command name = title-only lens; click does nothing for now. return [...groupBy(matches, (m) => m.line)].map(([line, group]) => ({ range: lineRange(line), - command: { title: titleFor(group.map((m) => m.resource)), command: '' }, + command: commandFor(group.map((m) => m.node)), })); } -interface ResourceLensInfo { - readonly logicalId: string; - readonly cfnType: string; +/** + * One selectable resource on a lens, shaped as a VS Code QuickPick item so the + * client renders it directly: `label` is the CFN type, `description` the + * developer-facing construct name. + */ +interface ResourceChoice { + readonly label: string; + readonly description: string; + readonly target: ResourceTarget; +} + +/** + * Builds the lens command for one line's resources: resolvable choices the + * client opens directly (one) or via a picker (several). Unresolvable resources + * are dropped; a line where none resolve stays title-only. + */ +function commandFor(nodes: readonly ResourceConstruct[]): Command { + const title = titleFor(nodes); + const choices = nodes + .map((node) => ({ label: node.type, description: friendlyName(node.path), target: resourceTarget(node) })) + .filter((choice): choice is ResourceChoice => choice.target !== undefined); + if (choices.length === 0) { + return { title, command: '' }; + } + return { title, command: OPEN_RESOURCE_COMMAND, arguments: [choices] }; } /** A construct that produces a CFN resource and carries a source location. */ @@ -70,11 +92,20 @@ function lineRange(line1Based: number): Range { return { start: { line, character: 0 }, end: { line, character: 0 } }; } -function titleFor(resources: readonly ResourceLensInfo[]): string { +function titleFor(resources: readonly ResourceConstruct[]): string { if (resources.length === 1) { - const r = resources[0]; - return `Creates: ${r.cfnType} [logical: ${r.logicalId}]`; + return `Creates ${resources[0].type}`; + } + const types = resources.map((r) => r.type).join(', '); + return `Creates ${resources.length} resources: ${types}`; +} + +/** Developer-facing construct name: the construct path without the synthetic CfnResource leaf. */ +function friendlyName(constructPath: string): string { + const segments = constructPath.split('/'); + const leaf = segments[segments.length - 1]; + if (segments.length > 1 && (leaf === 'Resource' || leaf === 'Default')) { + segments.pop(); } - const ids = resources.map((r) => r.logicalId).join(', '); - return `${resources.length} resources: ${ids}`; + return segments.join('/'); } diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts new file mode 100644 index 000000000..10ab1f6ce --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts @@ -0,0 +1,52 @@ +import * as fs from 'fs'; +import { pathToFileURL } from 'url'; +import { type Position, type Range } from 'vscode-languageserver/node'; + +/** + * 0-based position of a resource's logical-ID key in its synthesized template. + * A logical ID only appears as a `"":` key in its own definition (every + * Ref/GetAtt/DependsOn occurrence is a string value, never followed by `:`), so + * anchoring on the key selects the definition without a JSON parse. Assumes + * pretty-printed templates (one key per line). Undefined if not found. + */ +function findLogicalIdPosition(templateText: string, logicalId: string): Position | undefined { + const key = `"${logicalId}"`; + const lines = templateText.split('\n'); + for (let line = 0; line < lines.length; line++) { + const trimmed = lines[line].trimStart(); + if (trimmed.startsWith(key) && trimmed.slice(key.length).trimStart().startsWith(':')) { + return { line, character: lines[line].length - trimmed.length }; + } + } + return undefined; +} + +/** An editor navigation target: the template file and the position to reveal. */ +export interface ResourceTarget { + readonly uri: string; + readonly range: Range; +} + +/** + * Resolves a construct node to its CFN resource location for an LSP "go to"; + * undefined when not navigable (no template, unreadable, or id not found). + */ +export function resourceTarget(node: { templateFile?: string; logicalId: string }): ResourceTarget | undefined { + if (node.templateFile === undefined) { + return undefined; + } + let templateText: string; + try { + templateText = fs.readFileSync(node.templateFile, 'utf-8'); + } catch { + return undefined; + } + const position = findLogicalIdPosition(templateText, node.logicalId); + if (position === undefined) { + return undefined; + } + return { + uri: pathToFileURL(node.templateFile).toString(), + range: { start: position, end: position }, + }; +} diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index d893c20ba..40ac05126 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -30,7 +30,7 @@ "organization": true }, "devDependencies": { - "@cdklabs/eslint-plugin": "^2.0.6", + "@cdklabs/eslint-plugin": "^2.0.8", "@stylistic/eslint-plugin": "^3", "@types/convert-source-map": "^2", "@types/express": "^4", @@ -41,16 +41,16 @@ "constructs": "^10.0.0", "eslint": "^9", "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.4", + "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest": "^29.15.2", "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-prettier": "^4.2.5", "jest": "^29.7.0", "jest-junit": "^16", - "nx": "^22.7.2", + "nx": "^22.7.5", "prettier": "^2.8", - "projen": "^0.99.63", + "projen": "^0.99.70", "ts-jest": "^29.4.11", "typescript": "5.9", "vscode-languageserver-protocol": "^3" diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts index b0fd39679..931bf9637 100644 --- a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts @@ -46,6 +46,8 @@ export interface StackSpec { export interface FlatAssemblySpec { readonly stacks: readonly StackSpec[]; + /** Emit `aws:cdk:path` on template resources (default true); false simulates `--no-path-metadata`. */ + readonly pathMetadata?: boolean; } export interface StageStackSpec { @@ -68,6 +70,8 @@ export interface NestedStackSpec { readonly id: string; /** Resources inside the nested stack. */ readonly resources: readonly ResourceSpec[]; + /** Nested stacks inside this nested stack (for nested-in-nested fixtures). */ + readonly nestedStacks?: readonly NestedStackSpec[]; } export interface NestedStackParentSpec { @@ -88,7 +92,7 @@ export function buildFlatAssembly(spec: FlatAssemblySpec): string { for (const stack of spec.stacks) { artifacts[stack.id] = stackArtifact(stack); - writeTemplate(dir, stack); + writeTemplate(dir, stack.id, stack.resources, stack.id, spec.pathMetadata ?? true); } writeJson(path.join(dir, 'manifest.json'), { @@ -135,7 +139,7 @@ export function buildNestedAssembly(spec: NestedAssemblySpec): string { displayName: constructPath, metadata: stackMetadata(stack.resources, `/${constructPath}`), }; - writeTemplate(stageDir, { id: artifactId, resources: stack.resources }); + writeTemplate(stageDir, artifactId, stack.resources, constructPath); } writeJson(path.join(stageDir, 'manifest.json'), { @@ -179,14 +183,23 @@ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec } const dir = mkAssemblyDir('nestedstack'); const { parent } = spec; + // All nested resources' metadata lives under the PARENT artifact, keyed by + // full construct path (any depth), mirroring aws-cdk-lib. const metadata: Record = {}; + const parentChildren: Record = {}; + const parentResources: Record = {}; + for (const r of parent.resources) { metadata[`/${parent.id}/${r.id}/Resource`] = [logicalIdEntry(r)]; + parentChildren[r.id] = resourceTreeNode(parent.id, r); + parentResources[r.logicalId] = { + Type: r.cfnType, + Metadata: { 'aws:cdk:path': `${parent.id}/${r.id}/Resource` }, + Properties: {}, + }; } for (const ns of parent.nestedStacks) { - for (const r of ns.resources) { - metadata[`/${parent.id}/${ns.id}/${r.id}/Resource`] = [logicalIdEntry(r)]; - } + emitNestedStack(dir, metadata, parentChildren, parentResources, parent.id, ns); } writeJson(path.join(dir, 'manifest.json'), { @@ -202,22 +215,6 @@ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec } }, }, }); - - const parentChildren: Record = {}; - for (const r of parent.resources) { - parentChildren[r.id] = resourceTreeNode(parent.id, r); - } - for (const ns of parent.nestedStacks) { - parentChildren[ns.id] = { - id: ns.id, - path: `${parent.id}/${ns.id}`, - constructInfo: { fqn: 'aws-cdk-lib.NestedStack', version: CONSTRUCT_INFO_VERSION }, - children: Object.fromEntries( - ns.resources.map((r) => [r.id, resourceTreeNode(`${parent.id}/${ns.id}`, r)]), - ), - }; - } - writeJson(path.join(dir, 'tree.json'), { version: TREE_SCHEMA_VERSION, tree: appNode([{ @@ -227,11 +224,80 @@ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec } children: parentChildren, }]), }); - writeJson(path.join(dir, `${parent.id}.template.json`), { Resources: {} }); + writeJson(path.join(dir, `${parent.id}.template.json`), { Resources: parentResources }); fs.writeFileSync(path.join(dir, 'cdk.out'), JSON.stringify({ version: ASSEMBLY_SCHEMA_VERSION })); return dir; } +/** + * Writes one nested stack's template (its resources plus an + * AWS::CloudFormation::Stack + aws:asset:path per child nested stack) and + * recurses for those children. Accumulates every resource's logical-ID + * metadata into the parent artifact's metadata map, keyed by full construct + * path. Returns the tree node and the nested template filename. + */ +function emitNestedStack( + dir: string, + metadata: Record, + parentChildren: Record, + parentResources: Record, + parentPath: string, + ns: NestedStackSpec, +): void { + const nsPath = `${parentPath}/${ns.id}`; + const templateFile = `${nsPath.replace(/\//g, '')}.nested.template.json`; + const cfnStackLogicalId = `${ns.id}NestedStackResource`; + + // Parent template: the AWS::CloudFormation::Stack resource pointing at the + // nested template via aws:asset:path. + parentResources[cfnStackLogicalId] = { + Type: 'AWS::CloudFormation::Stack', + Metadata: { 'aws:asset:path': templateFile }, + Properties: {}, + }; + // Tree: mirror aws-cdk-lib -- the CfnStack resource lives in a SIBLING + // `.NestedStack` wrapper (not under the NestedStack construct), and its + // logical ID is emitted into the owning stack's metadata. + const resourcePath = `${nsPath}.NestedStack/${ns.id}.NestedStackResource`; + parentChildren[`${ns.id}.NestedStack`] = { + id: `${ns.id}.NestedStack`, + path: `${nsPath}.NestedStack`, + constructInfo: { fqn: 'constructs.Construct', version: CONSTRUCT_INFO_VERSION }, + children: { + [`${ns.id}.NestedStackResource`]: { + id: `${ns.id}.NestedStackResource`, + path: resourcePath, + attributes: { 'aws:cdk:cloudformation:type': 'AWS::CloudFormation::Stack' }, + }, + }, + }; + metadata[`/${resourcePath}`] = [{ type: ArtifactMetadataEntryType.LOGICAL_ID, data: cfnStackLogicalId }]; + + // The nested stack's own construct subtree + template. + const nsChildren: Record = {}; + const nsResources: Record = {}; + for (const r of ns.resources) { + metadata[`/${nsPath}/${r.id}/Resource`] = [logicalIdEntry(r)]; + nsChildren[r.id] = resourceTreeNode(nsPath, r); + nsResources[r.logicalId] = { + Type: r.cfnType, + Metadata: { 'aws:cdk:path': `${nsPath}/${r.id}/Resource` }, + Properties: {}, + }; + } + for (const child of ns.nestedStacks ?? []) { + emitNestedStack(dir, metadata, nsChildren, nsResources, nsPath, child); + } + writeJson(path.join(dir, templateFile), { Resources: nsResources }); + + parentChildren[ns.id] = { + id: ns.id, + path: nsPath, + constructInfo: { fqn: 'aws-cdk-lib.NestedStack', version: CONSTRUCT_INFO_VERSION }, + children: nsChildren, + }; +} + /** Manifest + tree with no metadata, no traces — for non-TS app graceful-degradation tests. */ export function buildNonTypeScriptAssembly(): string { const dir = mkAssemblyDir('nonts'); @@ -395,10 +461,22 @@ function logicalIdEntry(r: ResourceSpec): Record { return entry; } -function writeTemplate(dir: string, stack: { id: string; resources: readonly ResourceSpec[] }): void { - const resources: Record = {}; - for (const r of stack.resources) { - resources[r.logicalId] = { Type: r.cfnType, Properties: {} }; +function writeTemplate( + dir: string, + fileBaseId: string, + resources: readonly ResourceSpec[], + constructPathPrefix: string, + pathMetadata = true, +): void { + const out: Record = {}; + for (const r of resources) { + out[r.logicalId] = { + Type: r.cfnType, + // aws:cdk:path mirrors real synth output (on by default), giving each + // resource its globally-unique construct path for collision-free lookup. + ...(pathMetadata ? { Metadata: { 'aws:cdk:path': `${constructPathPrefix}/${r.id}/Resource` } } : {}), + Properties: {}, + }; } - writeJson(path.join(dir, `${stack.id}.template.json`), { Resources: resources }); + writeJson(path.join(dir, `${fileBaseId}.template.json`), { Resources: out }); } diff --git a/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts index f400d8c8f..e9c0eff91 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts @@ -5,6 +5,7 @@ import { readAssembly, type AssemblyData, type AssemblyReadResult, type Construc import { buildFlatAssembly, buildNestedAssembly, + buildNestedStackAssembly, buildNonTypeScriptAssembly, cleanupFixture, withMalformedValidationReport, @@ -247,6 +248,140 @@ describe('readAssembly graceful degradation', () => { }); }); +describe('readAssembly resource templateFile', () => { + let dir: string | undefined; + + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + test('sets templateFile to the resource\'s own stack template, none on wrappers', () => { + dir = buildFlatAssembly({ + stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], + }); + const data = expectSuccess(readAssembly(dir)); + + expect(findNode(data.tree, 'Stack1/MyBucket/Resource')!.templateFile) + .toBe(path.join(dir!, 'Stack1.template.json')); + // L2 wrapper is not a CFN resource -> no template. + expect(findNode(data.tree, 'Stack1/MyBucket')!.templateFile).toBeUndefined(); + }); + + test('resolves nested-stack resources to the nested template, not the parent', () => { + dir = buildNestedStackAssembly({ + parent: { + id: 'Parent', + resources: [{ id: 'TopBucket', logicalId: 'TopBucketABC', cfnType: 'AWS::S3::Bucket' }], + nestedStacks: [{ + id: 'MyNested', + resources: [{ id: 'NestedQueue', logicalId: 'NestedQueueXYZ', cfnType: 'AWS::SQS::Queue' }], + }], + }, + }); + const data = expectSuccess(readAssembly(dir)); + + // Top-level resource lives in the parent template. + expect(findNode(data.tree, 'Parent/TopBucket/Resource')!.templateFile) + .toBe(path.join(dir!, 'Parent.template.json')); + // The resource inside the NestedStack lives in the nested template. + expect(findNode(data.tree, 'Parent/MyNested/NestedQueue/Resource')!.templateFile) + .toBe(path.join(dir!, 'ParentMyNested.nested.template.json')); + }); + + test('keeps templates distinct when two stacks share a logical id (stack-relative ids)', () => { + // Same-shape stacks produce the SAME stack-relative logicalId in different + // templates; each resource must resolve to its OWN stack's template. + dir = buildFlatAssembly({ + stacks: [ + { id: 'Prod', resources: [{ id: 'Data', logicalId: 'DataX', cfnType: 'AWS::S3::Bucket' }] }, + { id: 'Dev', resources: [{ id: 'Data', logicalId: 'DataX', cfnType: 'AWS::S3::Bucket' }] }, + ], + }); + const data = expectSuccess(readAssembly(dir)); + + expect(findNode(data.tree, 'Prod/Data/Resource')!.templateFile).toBe(path.join(dir!, 'Prod.template.json')); + expect(findNode(data.tree, 'Dev/Data/Resource')!.templateFile).toBe(path.join(dir!, 'Dev.template.json')); + }); + + test('resolves a parent resource and its nested-stack twin that share a logical id', () => { + // A NestedStack resets the logical-ID namespace, so a parent resource and a + // resource in its own nested stack can share an id. The globally-unique + // construct path (aws:cdk:path) disambiguates them. + dir = buildNestedStackAssembly({ + parent: { + id: 'Parent', + resources: [{ id: 'Data', logicalId: 'DataX', cfnType: 'AWS::S3::Bucket' }], + nestedStacks: [{ + id: 'Nested', + resources: [{ id: 'Data', logicalId: 'DataX', cfnType: 'AWS::S3::Bucket' }], + }], + }, + }); + const data = expectSuccess(readAssembly(dir)); + + expect(findNode(data.tree, 'Parent/Data/Resource')!.templateFile) + .toBe(path.join(dir!, 'Parent.template.json')); + expect(findNode(data.tree, 'Parent/Nested/Data/Resource')!.templateFile) + .toBe(path.join(dir!, 'ParentNested.nested.template.json')); + }); + + test('resolves a resource in a doubly-nested stack to the innermost template', () => { + dir = buildNestedStackAssembly({ + parent: { + id: 'Parent', + resources: [], + nestedStacks: [{ + id: 'Outer', + resources: [{ id: 'OuterFn', logicalId: 'OuterFnABC', cfnType: 'AWS::Lambda::Function' }], + nestedStacks: [{ + id: 'Inner', + resources: [{ id: 'InnerQueue', logicalId: 'InnerQueueXYZ', cfnType: 'AWS::SQS::Queue' }], + }], + }], + }, + }); + const data = expectSuccess(readAssembly(dir)); + + expect(findNode(data.tree, 'Parent/Outer/OuterFn/Resource')!.templateFile) + .toBe(path.join(dir!, 'ParentOuter.nested.template.json')); + expect(findNode(data.tree, 'Parent/Outer/Inner/InnerQueue/Resource')!.templateFile) + .toBe(path.join(dir!, 'ParentOuterInner.nested.template.json')); + }); + + test('skips an unreadable nested template instead of failing the whole read', () => { + dir = buildNestedStackAssembly({ + parent: { + id: 'Parent', + resources: [{ id: 'TopBucket', logicalId: 'TopBucketABC', cfnType: 'AWS::S3::Bucket' }], + nestedStacks: [{ + id: 'Nested', + resources: [{ id: 'NestedQueue', logicalId: 'NestedQueueXYZ', cfnType: 'AWS::SQS::Queue' }], + }], + }, + }); + fs.rmSync(path.join(dir, 'ParentNested.nested.template.json')); + // Still a success: the missing nested template degrades only its subtree. + const data = expectSuccess(readAssembly(dir)); + expect(findNode(data.tree, 'Parent/TopBucket/Resource')!.templateFile) + .toBe(path.join(dir!, 'Parent.template.json')); + expect(findNode(data.tree, 'Parent/Nested/NestedQueue/Resource')!.templateFile).toBeUndefined(); + }); + + test('resolves templateFile without path metadata (positional, not id-based)', () => { + // Resolution threads the template down the construct tree, so it works + // regardless of --no-path-metadata (no reliance on aws:cdk:path). + dir = buildFlatAssembly({ + pathMetadata: false, + stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], + }); + const data = expectSuccess(readAssembly(dir)); + + expect(findNode(data.tree, 'Stack1/MyBucket/Resource')!.templateFile) + .toBe(path.join(dir!, 'Stack1.template.json')); + }); +}); + function findNode(nodes: readonly ConstructNode[], targetPath: string): ConstructNode | undefined { for (const node of nodes) { if (node.path === targetPath) return node; diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts index 79d762e04..f7c997a0f 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts @@ -1,7 +1,10 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { pathToFileURL } from 'url'; import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; import type { ConstructNode } from '../../lib'; -import { codeLensesForFile } from '../../lib/lsp/codelens'; +import { codeLensesForFile, OPEN_RESOURCE_COMMAND } from '../../lib/lsp/codelens'; const FILE = '/p/lib/stack.ts'; const URI = pathToFileURL(FILE).toString(); @@ -43,7 +46,7 @@ describe('codeLensesForFile', () => { start: { line: 11, character: 0 }, end: { line: 11, character: 0 }, }); - expect(lenses[0].command?.title).toBe('Creates: AWS::S3::Bucket [logical: MyBucketF68F3FF0]'); + expect(lenses[0].command?.title).toBe('Creates AWS::S3::Bucket'); }); test('groups multiple resources on the same source line into one lens', () => { @@ -72,7 +75,7 @@ describe('codeLensesForFile', () => { const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); expect(lenses).toHaveLength(1); - expect(lenses[0].command?.title).toBe('3 resources: BucketABC, BucketPolicyDEF, KeyGHI'); + expect(lenses[0].command?.title).toBe('Creates 3 resources: AWS::S3::Bucket, AWS::S3::BucketPolicy, AWS::KMS::Key'); }); test('emits separate lenses for resources on different lines', () => { @@ -153,4 +156,87 @@ describe('codeLensesForFile', () => { expect(codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); }); + + test('a single resource with a resolvable template gets a clickable openResource command', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codelens-')); + const templateFile = path.join(dir, 'Stack1.template.json'); + fs.writeFileSync(templateFile, JSON.stringify({ Resources: { MyBucketF68F3FF0: { Type: 'AWS::S3::Bucket' } } }, null, 2)); + try { + const tree = [node({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucketF68F3FF0', + type: 'AWS::S3::Bucket', + templateFile, + sourceLocation: { file: FILE, line: 12, column: 5 }, + })]; + + const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); + expect(lens.command?.arguments).toEqual([[{ + label: 'AWS::S3::Bucket', + description: 'Stack1/MyBucket', + target: { + uri: pathToFileURL(templateFile).toString(), + range: { start: { line: 2, character: 4 }, end: { line: 2, character: 4 } }, + }, + }]]); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + test('a multi-resource line carries all resolvable resources as choices', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codelens-')); + const templateFile = path.join(dir, 'Stack1.template.json'); + fs.writeFileSync(templateFile, JSON.stringify({ + Resources: { B1: { Type: 'AWS::S3::Bucket' }, B2: { Type: 'AWS::S3::BucketPolicy' } }, + }, null, 2)); + try { + const tree = [ + node({ path: 'Stack1/B/Resource', logicalId: 'B1', type: 'AWS::S3::Bucket', templateFile, sourceLocation: { file: FILE, line: 12, column: 5 } }), + node({ path: 'Stack1/B/Policy', logicalId: 'B2', type: 'AWS::S3::BucketPolicy', templateFile, sourceLocation: { file: FILE, line: 12, column: 5 } }), + ]; + + const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + const uri = pathToFileURL(templateFile).toString(); + expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); + expect(lens.command?.arguments).toEqual([[ + { label: 'AWS::S3::Bucket', description: 'Stack1/B', target: { uri, range: { start: { line: 2, character: 4 }, end: { line: 2, character: 4 } } } }, + { label: 'AWS::S3::BucketPolicy', description: 'Stack1/B/Policy', target: { uri, range: { start: { line: 5, character: 4 }, end: { line: 5, character: 4 } } } }, + ]]); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + test('a multi-resource line with no resolvable templates stays title-only', () => { + const tree = [ + node({ path: 'Stack1/B/Resource', logicalId: 'B1', type: 'AWS::S3::Bucket', templateFile: '/no/such.json', sourceLocation: { file: FILE, line: 12, column: 5 } }), + node({ path: 'Stack1/B/Policy', logicalId: 'B2', type: 'AWS::S3::BucketPolicy', templateFile: '/no/such.json', sourceLocation: { file: FILE, line: 12, column: 5 } }), + ]; + + const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + expect(lens.command?.command).toBe(''); + expect(lens.command?.arguments).toBeUndefined(); + }); + + test('a single resource whose id is missing from the template degrades to title-only', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codelens-')); + const templateFile = path.join(dir, 'Stack1.template.json'); + fs.writeFileSync(templateFile, JSON.stringify({ Resources: { SomethingElse: { Type: 'AWS::S3::Bucket' } } }, null, 2)); + try { + const tree = [node({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucketF68F3FF0', + type: 'AWS::S3::Bucket', + templateFile, + sourceLocation: { file: FILE, line: 12, column: 5 }, + })]; + + const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + expect(lens.command?.command).toBe(''); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index 6b2c31ad3..9112068bf 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -211,8 +211,7 @@ describe('LSP Server', () => { expect(lenses).toHaveLength(1); expect(lenses[0].range.start.line).toBe(11); // 1-based 12 -> 0-based 11 - expect(lenses[0].command?.title).toContain('AWS::S3::Bucket'); - expect(lenses[0].command?.title).toContain('MyBucketABC'); + expect(lenses[0].command?.title).toBe('Creates AWS::S3::Bucket'); }); test('publishes nothing when assembly is not-found (pre-synth)', () => { diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts new file mode 100644 index 000000000..c664e1463 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts @@ -0,0 +1,103 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { pathToFileURL } from 'url'; +import { readAssembly, type ConstructNode } from '../../lib'; +import { resourceTarget } from '../../lib/lsp/template-locator'; +import { buildFlatAssembly, cleanupFixture } from '../_fixtures/builders'; + +// A synthesized-style template where MyBucketF68F3FF0 is defined once and also +// referenced (Ref + DependsOn) -- the references must not be matched. +const TEMPLATE = [ + '{', + ' "Resources": {', + ' "MyBucketF68F3FF0": {', + ' "Type": "AWS::S3::Bucket"', + ' },', + ' "MyPolicy3A1B2C3D": {', + ' "Type": "AWS::IAM::Policy",', + ' "Properties": {', + ' "Bucket": { "Ref": "MyBucketF68F3FF0" }', + ' },', + ' "DependsOn": [ "MyBucketF68F3FF0" ]', + ' }', + ' }', + '}', +].join('\n'); + +describe('resourceTarget', () => { + let dir: string | undefined; + + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + const find = (nodes: readonly ConstructNode[], targetPath: string): ConstructNode | undefined => { + for (const node of nodes) { + if (node.path === targetPath) return node; + const hit = find(node.children, targetPath); + if (hit) return hit; + } + return undefined; + }; + + test('resolves a resource node to its template uri and key position', () => { + dir = buildFlatAssembly({ + stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], + }); + const result = readAssembly(dir); + if (result.status !== 'success') throw new Error('expected success'); + const node = find(result.data.tree, 'Stack1/MyBucket/Resource')!; + + const target = resourceTarget(node); + expect(target?.uri).toBe(pathToFileURL(path.join(dir!, 'Stack1.template.json')).toString()); + // 2-space indented template: the key sits on line 2, opening quote at char 4. + expect(target?.range).toEqual({ start: { line: 2, character: 4 }, end: { line: 2, character: 4 } }); + }); + + test('returns undefined for a node without a resolved templateFile', () => { + expect(resourceTarget({ templateFile: undefined, logicalId: 'MyBucketF68F3FF0' })).toBeUndefined(); + }); + + test('returns undefined when the logical id is not in the resolved template', () => { + dir = buildFlatAssembly({ + stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], + }); + expect(resourceTarget({ templateFile: path.join(dir, 'Stack1.template.json'), logicalId: 'GhostId' })).toBeUndefined(); + }); + + test('returns undefined (does not throw) when the template can no longer be read', () => { + expect(resourceTarget({ templateFile: '/no/such/template.json', logicalId: 'MyBucketF68F3FF0' })).toBeUndefined(); + }); + + test('resolves to the definition key, not Ref/DependsOn occurrences of the same id', () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'locator-')); + const file = path.join(dir, 'T.template.json'); + fs.writeFileSync(file, TEMPLATE); + // MyBucketF68F3FF0 is defined on line 2 and also referenced (Ref line 8, + // DependsOn line 10); only the definition is a `"":` key, so line 2 wins. + expect(resourceTarget({ templateFile: file, logicalId: 'MyBucketF68F3FF0' })?.range.start) + .toEqual({ line: 2, character: 4 }); + }); + + test('does not match a logical id that is a prefix of a longer key', () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'locator-')); + const file = path.join(dir, 'T.template.json'); + fs.writeFileSync(file, [ + '{', + ' "Resources": {', + ' "MyBucketF68F3FF0": {', + ' "Type": "AWS::S3::Bucket"', + ' },', + ' "MyBucket": {', + ' "Type": "AWS::S3::Bucket"', + ' }', + ' }', + '}', + ].join('\n')); + // The closing quote in the match key stops "MyBucket" matching "MyBucketF68F3FF0": + expect(resourceTarget({ templateFile: file, logicalId: 'MyBucket' })?.range.start) + .toEqual({ line: 5, character: 4 }); + }); +}); diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts index 3bf1c4a19..9ad422b95 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { ArtifactMetadataEntryType, CFN_RESOURCE_TYPE_ATTRIBUTE, type MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; import type { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; +import { ASSET_RESOURCE_METADATA_PATH_KEY } from './assets'; import type { CloudAssembly } from './cloud-assembly'; /** Construct ids that aws-cdk-lib injects into the tree but aren't user constructs. */ @@ -20,6 +21,12 @@ export interface ConstructTreeNode { readonly type?: string; /** CFN logical ID, if this construct maps to a CFN resource. */ readonly logicalId?: string; + /** + * Absolute path to the `*.template.json` that declares this construct's CFN + * resource -- the nested template for resources inside a NestedStack. Set + * only for CFN resources whose template is resolvable. + */ + readonly templateFile?: string; readonly children: readonly ConstructTreeNode[]; } @@ -32,6 +39,7 @@ export interface ConstructTreeNodeFields { readonly id: string; readonly type?: string; readonly logicalId?: string; + readonly templateFile?: string; readonly children: readonly T[]; } @@ -118,11 +126,42 @@ export function buildConstructTree( if (!rawTree) return []; const stackIndex = buildStackIndex(assembly.stacksRecursively); + const ctx: WalkContext = { stackIndex, decorate, templateCache: new Map() }; return Object.values(rawTree.children ?? {}) .filter((child) => !isCdkInternal(child.id)) - .map((child) => buildNode(child, stackIndex, undefined, decorate)); + .map((child) => buildNode(child, ctx, undefined, NO_TEMPLATE)); +} + +/** Shared state for a single {@link buildConstructTree} walk. */ +interface WalkContext { + readonly stackIndex: StackMetadataIndex; + readonly decorate: ConstructNodeDecorator; + /** Parsed nested templates, cached by absolute path. */ + readonly templateCache: Map; +} + +/** The slice of a CloudFormation template the tree walk reads. */ +interface CfnTemplate { + readonly Resources?: Record; +} +interface CfnResource { + readonly Type?: string; + readonly Metadata?: Record; } +/** + * The active CloudFormation template threaded down the tree walk: a resolved + * `{ file, template }` pair, or both `undefined` when there's no resolvable + * template (the root, or a nested stack that isn't CDK-resolvable). Never a + * partial mix. + */ +type TemplateScope = + | { readonly file: string; readonly template: CfnTemplate } + | { readonly file: undefined; readonly template: undefined }; + +/** Scope for nodes with no resolvable template: the root seed and unresolvable nested stacks. */ +const NO_TEMPLATE: TemplateScope = { file: undefined, template: undefined }; + /** * Raw tree.json node. The root tree.json holds the FULL hierarchy including * Stage children; nested-assembly subdirs don't shard tree.json. @@ -132,7 +171,6 @@ interface RawTreeNode { readonly path: string; readonly children?: { [key: string]: RawTreeNode }; readonly attributes?: { [key: string]: unknown }; - readonly constructInfo?: { readonly fqn: string; readonly version: string }; } /** A stack artifact plus its metadata Map, keyed (in the index) by construct path. */ @@ -163,29 +201,92 @@ function buildStackIndex(stacks: CloudFormationStackArtifact[]): StackMetadataIn function buildNode( raw: RawTreeNode, - stackIndex: StackMetadataIndex, + ctx: WalkContext, inheritedStack: StackMetadata | undefined, - decorate: ConstructNodeDecorator, + inherited: TemplateScope, ): T { - // When a node IS a stack, switch to that stack. Otherwise inherit the - // parent's: this routes NestedStack children to the parent's metadata, - // since aws-cdk-lib emits their entries there. - const owner = stackIndex.get(raw.path) ?? inheritedStack; + const stackHere = ctx.stackIndex.get(raw.path); + const owner = stackHere ?? inheritedStack; + // A stack node switches to its own template via the artifact's cached getter, + // which throws on a missing/corrupt top-level template -- a real assembly error + // we surface rather than swallow. Other nodes inherit the active scope. + const scope: TemplateScope = stackHere + ? { file: stackHere.stack.templateFullPath, template: stackHere.stack.template as CfnTemplate } + : inherited; // Metadata keys carry a leading "/", construct paths in tree.json don't. const entries = owner?.metadata.get('/' + raw.path) ?? []; - - const logicalIdEntry = entries.find((e) => e.type === ArtifactMetadataEntryType.LOGICAL_ID); - const logicalId = typeof logicalIdEntry?.data === 'string' ? logicalIdEntry.data : undefined; + const logicalId = logicalIdFromEntries(entries); const cfnTypeRaw = raw.attributes?.[CFN_RESOURCE_TYPE_ATTRIBUTE]; const cfnType = typeof cfnTypeRaw === 'string' ? cfnTypeRaw : undefined; + // A NestedStack switches its subtree to the nested scope; other nodes inherit. const children = Object.values(raw.children ?? {}) .filter((child) => !isCdkInternal(child.id)) - .map((child) => buildNode(child, stackIndex, owner, decorate)); + .map((child) => buildNode(child, ctx, owner, nestedBoundary(child, raw, owner, scope.template, ctx) ?? scope)); + + // Only CFN resources (those with a logical ID) carry a templateFile. + const nodeTemplateFile = logicalId !== undefined ? scope.file : undefined; + return ctx.decorate( + { path: raw.path, id: raw.id, type: cfnType, logicalId, templateFile: nodeTemplateFile, children }, + owner?.stack, + raw.path, + ); +} + +function logicalIdFromEntries(entries: MetadataEntry[]): string | undefined { + const entry = entries.find((e) => e.type === ArtifactMetadataEntryType.LOGICAL_ID); + return typeof entry?.data === 'string' ? entry.data : undefined; +} + +/** Parses a template file (cached by absolute path); undefined when missing/unparseable. */ +function loadTemplate(absPath: string, cache: Map): CfnTemplate | undefined { + if (cache.has(absPath)) return cache.get(absPath); + let template: CfnTemplate | undefined; + try { + template = JSON.parse(fs.readFileSync(absPath, 'utf-8')) as CfnTemplate; + } catch { + template = undefined; // missing/unparseable: that subtree just won't resolve a templateFile + } + cache.set(absPath, template); + return template; +} + +/** + * If `child` is a NestedStack, resolves the template scope for its subtree; + * otherwise returns `undefined` (caller keeps the active scope). + * + * Detection keys off the sibling `AWS::CloudFormation::Stack` that aws-cdk-lib + * emits at `/.NestedStack/.NestedStackResource` (whose + * `aws:asset:path` points at the nested template), NOT the construct's fqn -- a + * jsii-published NestedStack subclass has an fqn that doesn't end in + * ".NestedStack", but the sibling is named the same regardless. Returns + * NO_TEMPLATE (not the parent's) when unresolvable, since the nested stack's + * resources don't live in the parent template. + */ +function nestedBoundary( + child: RawTreeNode, + parent: RawTreeNode, + owner: StackMetadata | undefined, + currentTemplate: CfnTemplate | undefined, + ctx: WalkContext, +): TemplateScope | undefined { + const resourceNode = parent.children?.[`${child.id}.NestedStack`]?.children?.[`${child.id}.NestedStackResource`]; + // No sibling -> not a nested stack. A real nested stack always lives under a + // stack, so `owner` is defined here; the check also narrows it. + if (!resourceNode || !owner) return undefined; - return decorate({ path: raw.path, id: raw.id, type: cfnType, logicalId, children }, owner?.stack, raw.path); + const logicalId = logicalIdFromEntries(owner.metadata.get('/' + resourceNode.path) ?? []); + const assetPath = logicalId !== undefined + ? currentTemplate?.Resources?.[logicalId]?.Metadata?.[ASSET_RESOURCE_METADATA_PATH_KEY] + : undefined; + if (typeof assetPath !== 'string') return NO_TEMPLATE; + // Asset path is relative to the OWNER stack's assembly dir (the Stage + // sub-assembly for staged stacks), not necessarily the root assembly. + const file = path.join(path.dirname(owner.stack.templateFullPath), assetPath); + const template = loadTemplate(file, ctx.templateCache); + return template ? { file, template } : NO_TEMPLATE; } function isCdkInternal(id: string): boolean { diff --git a/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts b/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts index 0ca226bec..2643f746d 100644 --- a/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts +++ b/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts @@ -152,3 +152,140 @@ describe('buildConstructTree', () => { expect(buildConstructTree(assembly, (f) => f)).toEqual([]); }); }); + +describe('buildConstructTree -- nested stacks', () => { + let dir: string; + afterEach(() => dir && rimraf(dir)); + + const TREE_FILE = 'tree.json'; + + interface NestedOpts { + /** Logical ID of the bucket inside the nested stack (parent bucket is always 'ParentBucket'). */ + readonly nestedBucketLogicalId?: string; + /** false simulates --no-asset-metadata: the CfnStack carries no aws:asset:path. */ + readonly withAssetMetadata?: boolean; + /** false simulates a missing/unstaged asset: the nested template file isn't written. */ + readonly writeNestedTemplate?: boolean; + } + + /** + * Emits the real aws-cdk-lib NestedStack topology: a `Nested` construct, its + * sibling `Nested.NestedStack/Nested.NestedStackResource` AWS::CloudFormation::Stack + * (carrying aws:asset:path), and a bucket in both the parent and nested stacks. + * The `Nested` node deliberately carries a jsii fqn that does NOT end in + * ".NestedStack" -- so every test here also proves detection is by the sibling, + * not the fqn (P2b). + */ + function writeNestedAssembly(opts: NestedOpts = {}): string { + const withAsset = opts.withAssetMetadata ?? true; + const writeNested = opts.writeNestedTemplate ?? true; + const nestedBucketLid = opts.nestedBucketLogicalId ?? 'NestedBucket'; + + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'caa-nested-')); + + const nestedStackResource: Record = { Type: 'AWS::CloudFormation::Stack' }; + if (withAsset) nestedStackResource.Metadata = { 'aws:asset:path': 'nested.template.json' }; + const parentTemplate = { Resources: { ParentBucket: { Type: 'AWS::S3::Bucket' }, NestedStackRes: nestedStackResource } }; + const nestedTemplate = { Resources: { [nestedBucketLid]: { Type: 'AWS::S3::Bucket' } } }; + + const artifacts = { + MyStack: { + type: 'aws:cloudformation:stack', + environment: 'aws://111/us-east-1', + properties: { templateFile: 'template.json' }, + metadata: { + '/MyStack/Bucket/Resource': [{ type: 'aws:cdk:logicalId', data: 'ParentBucket' }], + '/MyStack/Nested/Bucket/Resource': [{ type: 'aws:cdk:logicalId', data: nestedBucketLid }], + '/MyStack/Nested.NestedStack/Nested.NestedStackResource': [{ type: 'aws:cdk:logicalId', data: 'NestedStackRes' }], + }, + }, + Tree: { type: 'cdk:tree', properties: { file: TREE_FILE } }, + }; + + const tree = { + version: 'tree-0.1', + tree: { + id: 'App', + path: '', + children: { + MyStack: { + id: 'MyStack', + path: 'MyStack', + children: { + 'Bucket': { + id: 'Bucket', + path: 'MyStack/Bucket', + children: { Resource: { id: 'Resource', path: 'MyStack/Bucket/Resource', attributes: { 'aws:cdk:cloudformation:type': 'AWS::S3::Bucket' } } }, + }, + 'Nested': { + id: 'Nested', + path: 'MyStack/Nested', + // jsii fqn intentionally NOT ending in ".NestedStack" (P2b regression). + constructInfo: { fqn: 'my-lib.DatabaseNestedStack', version: '1.0.0' }, + children: { + Bucket: { + id: 'Bucket', + path: 'MyStack/Nested/Bucket', + children: { Resource: { id: 'Resource', path: 'MyStack/Nested/Bucket/Resource', attributes: { 'aws:cdk:cloudformation:type': 'AWS::S3::Bucket' } } }, + }, + }, + }, + 'Nested.NestedStack': { + id: 'Nested.NestedStack', + path: 'MyStack/Nested.NestedStack', + children: { + 'Nested.NestedStackResource': { + id: 'Nested.NestedStackResource', + path: 'MyStack/Nested.NestedStack/Nested.NestedStackResource', + attributes: { 'aws:cdk:cloudformation:type': 'AWS::CloudFormation::Stack' }, + }, + }, + }, + }, + }, + }, + }, + }; + + fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify({ version: '0.0.0', artifacts })); + fs.writeFileSync(path.join(dir, 'template.json'), JSON.stringify(parentTemplate)); + if (writeNested) fs.writeFileSync(path.join(dir, 'nested.template.json'), JSON.stringify(nestedTemplate)); + fs.writeFileSync(path.join(dir, TREE_FILE), JSON.stringify(tree)); + return dir; + } + + const templateFileOf = (assemblyDir: string, nodePath: string): string | undefined => + ConstructIndex.fromTree(buildConstructTree(new CloudAssembly(assemblyDir), (f) => f)).byPath(nodePath)?.templateFile; + + test('resolves nested resources to the nested template and parent resources to the parent template', () => { + const d = writeNestedAssembly(); + expect(templateFileOf(d, 'MyStack/Bucket/Resource')).toBe(path.join(d, 'template.json')); + expect(templateFileOf(d, 'MyStack/Nested/Bucket/Resource')).toBe(path.join(d, 'nested.template.json')); + }); + + test('resolves parent and nested twins (same logical ID) to their own templates', () => { + // NestedStack resets the logical-ID namespace, so both buckets are "ParentBucket". + // Positional threading must still send each to its own template. + const d = writeNestedAssembly({ nestedBucketLogicalId: 'ParentBucket' }); + expect(templateFileOf(d, 'MyStack/Bucket/Resource')).toBe(path.join(d, 'template.json')); + expect(templateFileOf(d, 'MyStack/Nested/Bucket/Resource')).toBe(path.join(d, 'nested.template.json')); + }); + + test('detects the NestedStack by its sibling resource, not the construct fqn (jsii subclass)', () => { + // The fixture's Nested node fqn is "my-lib.DatabaseNestedStack" (does not end in + // ".NestedStack"); a suffix gate would miss it and mis-inherit the parent template. + const d = writeNestedAssembly(); + expect(templateFileOf(d, 'MyStack/Nested/Bucket/Resource')).toBe(path.join(d, 'nested.template.json')); + }); + + test('yields no templateFile when the nested template is missing/unreadable', () => { + const d = writeNestedAssembly({ writeNestedTemplate: false }); + expect(templateFileOf(d, 'MyStack/Nested/Bucket/Resource')).toBeUndefined(); + expect(templateFileOf(d, 'MyStack/Bucket/Resource')).toBe(path.join(d, 'template.json')); + }); + + test('yields no templateFile when asset metadata is absent (--no-asset-metadata)', () => { + const d = writeNestedAssembly({ withAssetMetadata: false }); + expect(templateFileOf(d, 'MyStack/Nested/Bucket/Resource')).toBeUndefined(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 73edb75c3..4a352da58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -178,7 +178,7 @@ __metadata: "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/toolkit-lib": "npm:^0.0.0" - "@cdklabs/eslint-plugin": "npm:^2.0.6" + "@cdklabs/eslint-plugin": "npm:^2.0.8" "@jridgewell/trace-mapping": "npm:^0.3" "@stylistic/eslint-plugin": "npm:^3" "@types/convert-source-map": "npm:^2" @@ -191,7 +191,7 @@ __metadata: convert-source-map: "npm:^2" eslint: "npm:^9" eslint-config-prettier: "npm:^10.1.8" - eslint-import-resolver-typescript: "npm:^4.4.4" + eslint-import-resolver-typescript: "npm:^4.4.5" eslint-plugin-import: "npm:^2.32.0" eslint-plugin-jest: "npm:^29.15.2" eslint-plugin-jsdoc: "npm:^62.9.0" @@ -199,9 +199,9 @@ __metadata: express: "npm:^4" jest: "npm:^29.7.0" jest-junit: "npm:^16" - nx: "npm:^22.7.2" + nx: "npm:^22.7.5" prettier: "npm:^2.8" - projen: "npm:^0.99.63" + projen: "npm:^0.99.70" ts-jest: "npm:^29.4.11" typescript: "npm:5.9" vscode-jsonrpc: "npm:^8" From e5fb0c3f03fc17be2e1fb52095a04da43eb861b3 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:22:55 -0400 Subject: [PATCH 07/28] feat: template ranges + two-way template/source navigation (#1624) - Computes character ranges for CloudFormation resource blocks in synthesized templates and uses them for navigation in both directions: - - CodeLens "go to" now selects the whole resource block (was a zero-width cursor). - New go-to-definition from a synthesized template back to the construct's source. A note on the JSON parser dependency Computing character ranges needs a position-aware JSON parser, since JSON.parse discards offsets. The natural choice is jsonc-parser (what the VS Code JSON language service uses). We could not use it here: its UMD entry loads internal modules through a parameter-shadowed require("./impl/...") that esbuild (used by node-backpack to bundle the CLI) cannot statically trace, so the bundled `cdk/cdk-assets` binaries fail with `Cannot find module './impl/format'`. Known, still-open: [microsoft/node-jsonc-parser#57](https://github.com/microsoft/node-jsonc-parser/issues/57), [evanw/esbuild#1619](https://github.com/evanw/esbuild/issues/1619). Its ESM build bundles fine, but the esbuild mainFields workaround isn't exposed by node-backpack, and importing the ESM build directly breaks our CommonJS tests; marking it external isn't appropriate for a self-contained CLI. So we use [json-source-map](https://www.npmjs.com/package/json-source-map), a single-file CommonJS module that bundles cleanly. ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 2 + .../cdk-explorer/lib/lsp/positions.ts | 18 ++ .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 30 +++ .../cdk-explorer/lib/lsp/template-locator.ts | 70 ++++--- .../cdk-explorer/test/lsp/codelens.test.ts | 44 +++-- .../cdk-explorer/test/lsp/server.test.ts | 65 ++++++- .../test/lsp/template-locator.test.ts | 178 +++++++++++++----- .../cloud-assembly-api/.projen/deps.json | 10 + .../@aws-cdk/cloud-assembly-api/lib/index.ts | 1 + .../cloud-assembly-api/lib/template-ranges.ts | 76 ++++++++ .../@aws-cdk/cloud-assembly-api/package.json | 2 + .../test/template-ranges.test.ts | 113 +++++++++++ .../cloud-assembly-api/tsconfig.dev.json | 1 + .../@aws-cdk/cloud-assembly-api/tsconfig.json | 1 + .../integ-runner/THIRD_PARTY_LICENSES | 26 +++ packages/aws-cdk/THIRD_PARTY_LICENSES | 26 +++ packages/cdk-assets/THIRD_PARTY_LICENSES | 26 +++ yarn.lock | 16 ++ 18 files changed, 622 insertions(+), 83 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/positions.ts create mode 100644 packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts create mode 100644 packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index d600805f4..61cdd08bf 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -580,8 +580,10 @@ const cloudAssemblyApi = configureProject( srcdir: 'lib', devDeps: [ cloudAssemblySchema.customizeReference({ versionType: 'exact' }), + '@types/json-source-map@^0.6.0', ], deps: [ + 'json-source-map@^0.6.1', 'jsonschema@^1.5.0', 'semver', ], diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/positions.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/positions.ts new file mode 100644 index 000000000..a36151aec --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/positions.ts @@ -0,0 +1,18 @@ +import type { OffsetRange } from '@aws-cdk/cloud-assembly-api'; +import { type Position, type Range } from 'vscode-languageserver/node'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +// Character offsets (from the core range resolver) and LSP positions are two +// coordinate systems over the same text. These convert between them at the LSP +// boundary, using TextDocument so UTF-16 column counting matches the protocol. + +/** Convert character offsets into an LSP range (0-based lines, UTF-16 columns). */ +export function offsetsToRange(text: string, offsets: OffsetRange): Range { + const doc = TextDocument.create('', 'json', 0, text); + return { start: doc.positionAt(offsets.start), end: doc.positionAt(offsets.end) }; +} + +/** Convert an LSP position into a 0-based character offset. */ +export function offsetAtPosition(text: string, position: Position): number { + return TextDocument.create('', 'json', 0, text).offsetAt(position); +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 006188ac0..8d081b6f5 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; @@ -11,14 +12,18 @@ import { TextDocumentSyncKind, type CodeLens, type CodeLensParams, + type DefinitionParams, type DidSaveTextDocumentParams, type Diagnostic, type InitializeParams, type InitializeResult, + type Location, } from 'vscode-languageserver/node'; /* eslint-disable import/no-relative-packages */ import { codeLensesForFile } from './codelens'; import { mapViolationsToDiagnostics } from './diagnostics'; +import { offsetAtPosition } from './positions'; +import { sourceTargetAtTemplateOffset } from './template-locator'; import { WATCH_EXCLUDE_DEFAULTS } from '../../../toolkit-lib/lib/actions/watch/private/helpers'; import { createIgnoreMatcher } from '../../../toolkit-lib/lib/util/glob-matcher'; import { @@ -52,6 +57,7 @@ export interface LspHandlers { onInitialized(): void; onDidSaveTextDocument(params: DidSaveTextDocumentParams): void; onCodeLens(params: CodeLensParams): CodeLens[]; + onDefinition(params: DefinitionParams): Location | undefined; onShutdown(): void; } @@ -130,6 +136,8 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers }, // Lens title is computed up-front; no resolve round-trip needed. codeLensProvider: { resolveProvider: false }, + // Go-to-definition from a synthesized template back to construct source. + definitionProvider: true, }, }; }, @@ -165,6 +173,27 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers onCodeLens(params) { return codeLensesForFile(cachedIndex, params.textDocument.uri); }, + onDefinition(params) { + // Only synthesized templates link back to source, and only file: URIs are + // readable. Check the scheme before fileURLToPath, which throws on other + // schemes (untitled:, git:, diff views). + const uri = params.textDocument.uri; + if (!uri.startsWith('file:') || !uri.endsWith('.template.json')) { + return undefined; + } + const filePath = fileURLToPath(uri); + let templateText: string; + try { + templateText = fs.readFileSync(filePath, 'utf-8'); + } catch { + return undefined; + } + // Offsets come from current disk text; the owner is looked up in the index + // built at startup. If the template was re-synthesized since, a missing + // match degrades to undefined rather than navigating to the wrong place. + const offset = offsetAtPosition(templateText, params.position); + return sourceTargetAtTemplateOffset(cachedIndex, filePath, templateText, offset); + }, onShutdown() { shutdownRequested = true; }, @@ -191,6 +220,7 @@ export function startServer(options: LspServerOptions): void { connection.onInitialized(() => handlers.onInitialized()); connection.onDidSaveTextDocument((params) => handlers.onDidSaveTextDocument(params)); connection.onCodeLens((params) => handlers.onCodeLens(params)); + connection.onDefinition((params) => handlers.onDefinition(params)); connection.onShutdown(() => handlers.onShutdown()); connection.onExit(() => process.exit(0)); diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts index 10ab1f6ce..d97518ee8 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts @@ -1,35 +1,27 @@ import * as fs from 'fs'; import { pathToFileURL } from 'url'; -import { type Position, type Range } from 'vscode-languageserver/node'; +import { resolveLogicalIdAtOffset, resolveResourceRange, type ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import { type Location, type Range } from 'vscode-languageserver/node'; +import { offsetsToRange } from './positions'; +import type { ConstructNode } from '../core/assembly-reader'; /** - * 0-based position of a resource's logical-ID key in its synthesized template. - * A logical ID only appears as a `"":` key in its own definition (every - * Ref/GetAtt/DependsOn occurrence is a string value, never followed by `:`), so - * anchoring on the key selects the definition without a JSON parse. Assumes - * pretty-printed templates (one key per line). Undefined if not found. + * An editor navigation target: the template file and the range to reveal. + * Structurally an LSP `Location`, but kept as a named type because it is + * serialized into the `openResource` CodeLens command's QuickPick arguments + * (see codelens.ts), where it reads as a domain target rather than a protocol type. */ -function findLogicalIdPosition(templateText: string, logicalId: string): Position | undefined { - const key = `"${logicalId}"`; - const lines = templateText.split('\n'); - for (let line = 0; line < lines.length; line++) { - const trimmed = lines[line].trimStart(); - if (trimmed.startsWith(key) && trimmed.slice(key.length).trimStart().startsWith(':')) { - return { line, character: lines[line].length - trimmed.length }; - } - } - return undefined; -} - -/** An editor navigation target: the template file and the position to reveal. */ export interface ResourceTarget { readonly uri: string; readonly range: Range; } /** - * Resolves a construct node to its CFN resource location for an LSP "go to"; - * undefined when not navigable (no template, unreadable, or id not found). + * Resolves a construct node to a CFN resource location for an LSP "go to": the + * range of the resource's block in its synthesized template. + * + * Returns `undefined` when the node is not navigable: no resolved template, the + * template cannot be read, it does not parse, or the logical id is absent. */ export function resourceTarget(node: { templateFile?: string; logicalId: string }): ResourceTarget | undefined { if (node.templateFile === undefined) { @@ -41,12 +33,44 @@ export function resourceTarget(node: { templateFile?: string; logicalId: string } catch { return undefined; } - const position = findLogicalIdPosition(templateText, node.logicalId); - if (position === undefined) { + const block = resolveResourceRange(templateText, node.logicalId); + if (block === undefined) { return undefined; } return { uri: pathToFileURL(node.templateFile).toString(), + range: offsetsToRange(templateText, block), + }; +} + +/** + * Reverse navigation: resolve a character offset inside a synthesized template to + * the source location of the construct that produced the enclosing resource. + * + * Finds the resource's logical id at `offset`, looks up the owning construct in + * `index` (matched by both `templateFile` and `logicalId`, since logical ids are + * only unique within a template), and returns its source location as a zero-width + * range. Undefined when the offset is not inside a resource, no construct owns it, + * or the construct has no source location (for example a non-TypeScript app). + */ +export function sourceTargetAtTemplateOffset( + index: ConstructIndex, + templateFile: string, + templateText: string, + offset: number, +): Location | undefined { + const logicalId = resolveLogicalIdAtOffset(templateText, offset); + if (logicalId === undefined) { + return undefined; + } + const owner = [...index].find((node) => node.logicalId === logicalId && node.templateFile === templateFile); + if (owner?.sourceLocation === undefined) { + return undefined; + } + // SourceLocation is 1-based; LSP positions are 0-based. + const position = { line: owner.sourceLocation.line - 1, character: owner.sourceLocation.column - 1 }; + return { + uri: pathToFileURL(owner.sourceLocation.file).toString(), range: { start: position, end: position }, }; } diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts index f7c997a0f..c33e0c597 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts @@ -16,6 +16,13 @@ const node = (overrides: Partial & { path: string }): ConstructNo ...overrides, }); +/** Shape of one resource choice carried in the openResource command arguments. */ +interface CommandChoice { + label: string; + description: string; + target: { uri: string; range: { start: unknown; end: unknown } }; +} + describe('codeLensesForFile', () => { test('returns no lenses when tree is empty', () => { expect(codeLensesForFile(ConstructIndex.fromTree([]), URI)).toEqual([]); @@ -115,8 +122,15 @@ describe('codeLensesForFile', () => { }), ]; - expect(codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toHaveLength(1); - expect(codeLensesForFile(ConstructIndex.fromTree(tree), OTHER_URI)).toHaveLength(1); + const index = ConstructIndex.fromTree(tree); + // Each query returns only the resource defined in that file, which proves the + // URI filter selects by file rather than returning everything for any query. + const onThisFile = codeLensesForFile(index, URI); + expect(onThisFile).toHaveLength(1); + expect(onThisFile[0].command?.title).toBe('Creates AWS::S3::Bucket'); + const onOtherFile = codeLensesForFile(index, OTHER_URI); + expect(onOtherFile).toHaveLength(1); + expect(onOtherFile[0].command?.title).toBe('Creates AWS::SQS::Queue'); }); test('walks descendants — finds resources nested under wrappers', () => { @@ -172,14 +186,15 @@ describe('codeLensesForFile', () => { const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); - expect(lens.command?.arguments).toEqual([[{ + const choices = (lens.command!.arguments as CommandChoice[][])[0]; + expect(choices).toHaveLength(1); + expect(choices[0]).toMatchObject({ label: 'AWS::S3::Bucket', description: 'Stack1/MyBucket', - target: { - uri: pathToFileURL(templateFile).toString(), - range: { start: { line: 2, character: 4 }, end: { line: 2, character: 4 } }, - }, - }]]); + target: { uri: pathToFileURL(templateFile).toString() }, + }); + // The target carries a real (non-zero-width) block range; exact offsets are covered in template-locator.test. + expect(choices[0].target.range.start).not.toEqual(choices[0].target.range.end); } finally { fs.rmSync(dir, { recursive: true, force: true }); } @@ -200,10 +215,15 @@ describe('codeLensesForFile', () => { const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; const uri = pathToFileURL(templateFile).toString(); expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); - expect(lens.command?.arguments).toEqual([[ - { label: 'AWS::S3::Bucket', description: 'Stack1/B', target: { uri, range: { start: { line: 2, character: 4 }, end: { line: 2, character: 4 } } } }, - { label: 'AWS::S3::BucketPolicy', description: 'Stack1/B/Policy', target: { uri, range: { start: { line: 5, character: 4 }, end: { line: 5, character: 4 } } } }, - ]]); + const choices = (lens.command!.arguments as CommandChoice[][])[0]; + expect(choices).toMatchObject([ + { label: 'AWS::S3::Bucket', description: 'Stack1/B', target: { uri } }, + { label: 'AWS::S3::BucketPolicy', description: 'Stack1/B/Policy', target: { uri } }, + ]); + // Each carries a real span, and the two resources resolve to distinct blocks. + expect(choices[0].target.range.start).not.toEqual(choices[0].target.range.end); + expect(choices[1].target.range.start).not.toEqual(choices[1].target.range.end); + expect(choices[0].target.range).not.toEqual(choices[1].target.range); } finally { fs.rmSync(dir, { recursive: true, force: true }); } diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index 9112068bf..51a4e31ea 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -1,5 +1,9 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { pathToFileURL } from 'url'; import type { Diagnostic } from 'vscode-languageserver/node'; +import { TextDocument } from 'vscode-languageserver-textdocument'; import type { AssemblyReadResult } from '../../lib'; import { createLspHandlers, type LspHandlerOptions, type LspHandlers } from '../../lib/lsp/server'; @@ -34,7 +38,7 @@ function initializeClient(client: CapturedClient, options?: Record { - test('responds to initialize with capabilities', () => { + test('initialize advertises codeLens, definition, and save-sync capabilities', () => { const client = createTestClient(); const result = client.handlers.onInitialize({ @@ -52,6 +56,7 @@ describe('LSP Server', () => { save: { includeText: false }, }, codeLensProvider: { resolveProvider: false }, + definitionProvider: true, }, }); }); @@ -223,4 +228,62 @@ describe('LSP Server', () => { expect(client.published).toHaveLength(0); }); + + test('onDefinition resolves a template position back to construct source', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'server-def-')); + const templateFile = path.join(dir, 'Stack1.template.json'); + const text = JSON.stringify({ Resources: { MyBucket: { Type: 'AWS::S3::Bucket' } } }, undefined, 1); + fs.writeFileSync(templateFile, text); + try { + const client = createTestClient({ + readAssembly: () => ({ + status: 'success', + data: { + tree: [{ + path: 'Stack1/MyBucket/Resource', + id: 'Resource', + logicalId: 'MyBucket', + type: 'AWS::S3::Bucket', + templateFile, + sourceLocation: { file: '/p/lib/stack.ts', line: 5, column: 3 }, + children: [], + }], + violations: [], + warnings: [], + }, + }), + }); + initializeClient(client, { applicationDir: dir }); + + const uri = pathToFileURL(templateFile).toString(); + const position = TextDocument.create(uri, 'json', 0, text).positionAt(text.indexOf('AWS::S3::Bucket')); + const target = client.handlers.onDefinition({ textDocument: { uri }, position }); + + expect(target?.uri).toBe(pathToFileURL('/p/lib/stack.ts').toString()); + expect(target?.range.start).toEqual({ line: 4, character: 2 }); // 1-based (5,3) -> 0-based + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + test('onDefinition returns undefined for a non-template document', () => { + const client = createTestClient(); + initializeClient(client, { applicationDir: '/p' }); + const target = client.handlers.onDefinition({ + textDocument: { uri: pathToFileURL('/p/lib/stack.ts').toString() }, + position: { line: 0, character: 0 }, + }); + expect(target).toBeUndefined(); + }); + + test('onDefinition returns undefined (does not throw) for a non-file URI', () => { + const client = createTestClient(); + initializeClient(client, { applicationDir: '/p' }); + expect( + client.handlers.onDefinition({ + textDocument: { uri: 'untitled:Untitled-1' }, + position: { line: 0, character: 0 }, + }), + ).toBeUndefined(); + }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts index c664e1463..80a4cd461 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts @@ -2,28 +2,43 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { pathToFileURL } from 'url'; +import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import { type Range } from 'vscode-languageserver/node'; +import { TextDocument } from 'vscode-languageserver-textdocument'; import { readAssembly, type ConstructNode } from '../../lib'; -import { resourceTarget } from '../../lib/lsp/template-locator'; +import { resourceTarget, sourceTargetAtTemplateOffset } from '../../lib/lsp/template-locator'; import { buildFlatAssembly, cleanupFixture } from '../_fixtures/builders'; // A synthesized-style template where MyBucketF68F3FF0 is defined once and also // referenced (Ref + DependsOn) -- the references must not be matched. -const TEMPLATE = [ - '{', - ' "Resources": {', - ' "MyBucketF68F3FF0": {', - ' "Type": "AWS::S3::Bucket"', - ' },', - ' "MyPolicy3A1B2C3D": {', - ' "Type": "AWS::IAM::Policy",', - ' "Properties": {', - ' "Bucket": { "Ref": "MyBucketF68F3FF0" }', - ' },', - ' "DependsOn": [ "MyBucketF68F3FF0" ]', - ' }', - ' }', - '}', -].join('\n'); +const TEMPLATE = JSON.stringify( + { + Resources: { + MyBucketF68F3FF0: { Type: 'AWS::S3::Bucket' }, + MyPolicy3A1B2C3D: { + Type: 'AWS::IAM::Policy', + Properties: { Bucket: { Ref: 'MyBucketF68F3FF0' } }, + DependsOn: ['MyBucketF68F3FF0'], + }, + }, + }, + undefined, + 1, +); + +/** Re-parse the substring that a returned range selects, so assertions need no hand-computed offsets. */ +function sliceParse(text: string, range: Range): unknown { + const doc = TextDocument.create('', 'json', 0, text); + return JSON.parse(text.slice(doc.offsetAt(range.start), doc.offsetAt(range.end))); +} + +/** Write a template to a throwaway file and return its path. */ +function writeTemplate(contents: string): { dir: string; file: string } { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'locator-')); + const file = path.join(dir, 'T.template.json'); + fs.writeFileSync(file, contents); + return { dir, file }; +} describe('resourceTarget', () => { let dir: string | undefined; @@ -42,18 +57,21 @@ describe('resourceTarget', () => { return undefined; }; - test('resolves a resource node to its template uri and key position', () => { + test('resolves a resource node to its template uri and block range', () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], }); const result = readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); const node = find(result.data.tree, 'Stack1/MyBucket/Resource')!; + const templateFile = path.join(dir!, 'Stack1.template.json'); - const target = resourceTarget(node); - expect(target?.uri).toBe(pathToFileURL(path.join(dir!, 'Stack1.template.json')).toString()); - // 2-space indented template: the key sits on line 2, opening quote at char 4. - expect(target?.range).toEqual({ start: { line: 2, character: 4 }, end: { line: 2, character: 4 } }); + const target = resourceTarget(node)!; + expect(target.uri).toBe(pathToFileURL(templateFile).toString()); + // The range spans the resource block (not a zero-width cursor) and re-parses to it. + const text = fs.readFileSync(templateFile, 'utf-8'); + expect(target.range.start).not.toEqual(target.range.end); + expect(sliceParse(text, target.range)).toEqual(JSON.parse(text).Resources.MyBucketF68F3FF0); }); test('returns undefined for a node without a resolved templateFile', () => { @@ -71,33 +89,99 @@ describe('resourceTarget', () => { expect(resourceTarget({ templateFile: '/no/such/template.json', logicalId: 'MyBucketF68F3FF0' })).toBeUndefined(); }); - test('resolves to the definition key, not Ref/DependsOn occurrences of the same id', () => { - dir = fs.mkdtempSync(path.join(os.tmpdir(), 'locator-')); - const file = path.join(dir, 'T.template.json'); - fs.writeFileSync(file, TEMPLATE); - // MyBucketF68F3FF0 is defined on line 2 and also referenced (Ref line 8, - // DependsOn line 10); only the definition is a `"":` key, so line 2 wins. - expect(resourceTarget({ templateFile: file, logicalId: 'MyBucketF68F3FF0' })?.range.start) - .toEqual({ line: 2, character: 4 }); + test('resolves the definition block, not Ref/DependsOn occurrences of the same id', () => { + const written = writeTemplate(TEMPLATE); + dir = written.dir; + // MyBucketF68F3FF0 is defined once and also referenced; the block must be the definition. + const target = resourceTarget({ templateFile: written.file, logicalId: 'MyBucketF68F3FF0' })!; + expect(sliceParse(TEMPLATE, target.range)).toEqual({ Type: 'AWS::S3::Bucket' }); }); test('does not match a logical id that is a prefix of a longer key', () => { - dir = fs.mkdtempSync(path.join(os.tmpdir(), 'locator-')); - const file = path.join(dir, 'T.template.json'); - fs.writeFileSync(file, [ - '{', - ' "Resources": {', - ' "MyBucketF68F3FF0": {', - ' "Type": "AWS::S3::Bucket"', - ' },', - ' "MyBucket": {', - ' "Type": "AWS::S3::Bucket"', - ' }', - ' }', - '}', - ].join('\n')); - // The closing quote in the match key stops "MyBucket" matching "MyBucketF68F3FF0": - expect(resourceTarget({ templateFile: file, logicalId: 'MyBucket' })?.range.start) - .toEqual({ line: 5, character: 4 }); + const contents = JSON.stringify( + { + Resources: { + MyBucketF68F3FF0: { Type: 'AWS::S3::Bucket' }, + MyBucket: { Type: 'AWS::S3::Bucket', Properties: { BucketName: 'short' } }, + }, + }, + undefined, + 1, + ); + const written = writeTemplate(contents); + dir = written.dir; + // The shorter id must resolve to its own (distinct) block, not the longer-named sibling. + const target = resourceTarget({ templateFile: written.file, logicalId: 'MyBucket' })!; + expect(sliceParse(contents, target.range)).toEqual({ Type: 'AWS::S3::Bucket', Properties: { BucketName: 'short' } }); + }); +}); + +describe('sourceTargetAtTemplateOffset', () => { + let dir: string | undefined; + afterEach(() => { + cleanupFixture(dir); + dir = undefined; + }); + + const indexWith = (overrides: Partial & { path: string }) => + ConstructIndex.fromTree([{ id: overrides.path.split('/').pop()!, children: [], ...overrides }]); + + test('resolves a template offset back to the construct source location', () => { + const contents = JSON.stringify( + { Resources: { MyBucket: { Type: 'AWS::S3::Bucket', Properties: { BucketName: 'b' } } } }, + undefined, + 1, + ); + const written = writeTemplate(contents); + dir = written.dir; + const index = indexWith({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucket', + type: 'AWS::S3::Bucket', + templateFile: written.file, + sourceLocation: { file: '/p/lib/stack.ts', line: 5, column: 3 }, + }); + + const offset = contents.indexOf('AWS::S3::Bucket'); // inside the MyBucket block + const target = sourceTargetAtTemplateOffset(index, written.file, contents, offset)!; + expect(target.uri).toBe(pathToFileURL('/p/lib/stack.ts').toString()); + // 1-based source location (5, 3) maps to a 0-based LSP position (4, 2). + expect(target.range).toEqual({ start: { line: 4, character: 2 }, end: { line: 4, character: 2 } }); + }); + + test('returns undefined for an offset outside any resource block', () => { + const contents = JSON.stringify({ Resources: { MyBucket: { Type: 'AWS::S3::Bucket' } } }, undefined, 1); + const index = indexWith({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucket', + templateFile: '/x/T.template.json', + sourceLocation: { file: '/p/lib/stack.ts', line: 5, column: 3 }, + }); + expect(sourceTargetAtTemplateOffset(index, '/x/T.template.json', contents, 0)).toBeUndefined(); + }); + + test('returns undefined when the owning construct has no source location', () => { + const contents = JSON.stringify({ Resources: { MyBucket: { Type: 'AWS::S3::Bucket' } } }, undefined, 1); + const offset = contents.indexOf('AWS::S3::Bucket'); + const index = indexWith({ path: 'Stack1/MyBucket/Resource', logicalId: 'MyBucket', templateFile: '/x/T.template.json' }); + expect(sourceTargetAtTemplateOffset(index, '/x/T.template.json', contents, offset)).toBeUndefined(); + }); + + test('matches on templateFile, not logical id alone (cross-template collision)', () => { + const contents = JSON.stringify({ Resources: { MyBucket: { Type: 'AWS::S3::Bucket' } } }, undefined, 1); + const written = writeTemplate(contents); + dir = written.dir; + // Two constructs share the logical id 'MyBucket' across different templates; + // logical ids are only unique within a template, so the lookup must also key + // on templateFile. + const index = ConstructIndex.fromTree([ + { path: 'StackA/MyBucket/Resource', id: 'Resource', logicalId: 'MyBucket', templateFile: written.file, sourceLocation: { file: '/p/a.ts', line: 2, column: 1 }, children: [] }, + { path: 'StackB/MyBucket/Resource', id: 'Resource', logicalId: 'MyBucket', templateFile: '/other/Stack2.template.json', sourceLocation: { file: '/p/b.ts', line: 9, column: 1 }, children: [] }, + ]); + + const target = sourceTargetAtTemplateOffset(index, written.file, contents, contents.indexOf('AWS::S3::Bucket'))!; + // Resolves to template A's construct (a.ts), not B's, despite the shared id. + expect(target.uri).toBe(pathToFileURL('/p/a.ts').toString()); + expect(target.range.start).toEqual({ line: 1, character: 0 }); }); }); diff --git a/packages/@aws-cdk/cloud-assembly-api/.projen/deps.json b/packages/@aws-cdk/cloud-assembly-api/.projen/deps.json index b13424894..3dc0e5f7b 100644 --- a/packages/@aws-cdk/cloud-assembly-api/.projen/deps.json +++ b/packages/@aws-cdk/cloud-assembly-api/.projen/deps.json @@ -17,6 +17,11 @@ "name": "@types/jest", "type": "build" }, + { + "name": "@types/json-source-map", + "version": "^0.6.0", + "type": "build" + }, { "name": "@types/node", "version": "^20", @@ -110,6 +115,11 @@ "name": "@aws-cdk/cloud-assembly-schema", "type": "peer" }, + { + "name": "json-source-map", + "version": "^0.6.1", + "type": "runtime" + }, { "name": "jsonschema", "version": "^1.5.0", diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/index.ts b/packages/@aws-cdk/cloud-assembly-api/lib/index.ts index 736566961..6466dcc30 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/index.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/index.ts @@ -20,3 +20,4 @@ export * from './placeholders'; export * from './environment'; export * from './bootstrap'; export * from './construct-tree'; +export * from './template-ranges'; diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts b/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts new file mode 100644 index 000000000..73793b754 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts @@ -0,0 +1,76 @@ +import { parse, type Pointers } from 'json-source-map'; + +/** + * A character-offset range into a template's text, as a half-open interval + * `[start, end)`. Framework-neutral on purpose: consumers map offsets to their + * own position model (for example the LSP via `TextDocument.positionAt`), which + * keeps this package free of editor types and of the UTF-16 column subtlety. + */ +export interface OffsetRange { + /** Start offset, a 0-based character index, inclusive. */ + readonly start: number; + /** End offset, a 0-based character index, exclusive. */ + readonly end: number; +} + +/** + * Resolves the character range of a resource's value block inside a synthesized + * CloudFormation template. The range covers the value `{ ... }` under + * `Resources/`, so `JSON.parse(text.slice(start, end))` returns the + * resource object. + * + * Returns `undefined` when the text is not valid JSON, or when there is no such + * resource. + */ +export function resolveResourceRange(templateText: string, logicalId: string): OffsetRange | undefined { + const pointers = parsePointers(templateText); + const mapping = pointers?.[`/Resources/${escapePointerSegment(logicalId)}`]; + if (mapping === undefined) { + return undefined; + } + return { start: mapping.value.pos, end: mapping.valueEnd.pos }; +} + +/** + * The inverse of `resolveResourceRange`: given a character offset into a + * template's text, returns the logical id of the resource whose block contains + * that offset, for linking a position in the template back to its construct. + * + * Returns `undefined` when the text is not valid JSON, or when the offset is not + * inside any `Resources/` block (for example whitespace, the + * `Resources` key, or another top-level section). + */ +export function resolveLogicalIdAtOffset(templateText: string, offset: number): string | undefined { + const pointers = parsePointers(templateText); + if (pointers === undefined) { + return undefined; + } + for (const [pointer, mapping] of Object.entries(pointers)) { + // Match only top-level resources (`/Resources/`), not nested property + // pointers like `/Resources//Properties/...`. + const match = /^\/Resources\/([^/]+)$/.exec(pointer); + if (match && offset >= mapping.value.pos && offset < mapping.valueEnd.pos) { + return unescapePointerSegment(match[1]); + } + } + return undefined; +} + +/** Parse the template into its JSON-pointer map, or `undefined` if it is not valid JSON. */ +function parsePointers(templateText: string): Pointers | undefined { + try { + return parse(templateText).pointers; + } catch { + return undefined; + } +} + +/** Escape a single path segment for use in a JSON pointer. */ +function escapePointerSegment(segment: string): string { + return segment.replace(/~/g, '~0').replace(/\//g, '~1'); +} + +/** Reverse of `escapePointerSegment`. */ +function unescapePointerSegment(segment: string): string { + return segment.replace(/~1/g, '/').replace(/~0/g, '~'); +} diff --git a/packages/@aws-cdk/cloud-assembly-api/package.json b/packages/@aws-cdk/cloud-assembly-api/package.json index d139634c0..de7edd47c 100644 --- a/packages/@aws-cdk/cloud-assembly-api/package.json +++ b/packages/@aws-cdk/cloud-assembly-api/package.json @@ -35,6 +35,7 @@ "@cdklabs/eslint-plugin": "^2.0.8", "@stylistic/eslint-plugin": "^3", "@types/jest": "^29.5.14", + "@types/json-source-map": "^0.6.0", "@types/node": "^20", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", @@ -60,6 +61,7 @@ "@aws-cdk/cloud-assembly-schema": "^0.0.0" }, "dependencies": { + "json-source-map": "^0.6.1", "jsonschema": "^1.5.0", "semver": "^7.8.1" }, diff --git a/packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts b/packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts new file mode 100644 index 000000000..c3ee2b95e --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts @@ -0,0 +1,113 @@ +import { resolveLogicalIdAtOffset, resolveResourceRange } from '../lib'; + +// A template object is the single source of truth: the text under test is its +// 1-space serialization (exactly how synth writes `*.template.json`), and the +// expected values are read straight off the object, so input and expectations +// can never drift. The shapes pack the cases that defeat naive approaches: +// +// - Policy carries an `Fn::Sub` (`${...}` braces), an `InlineJson` value with +// escaped quotes and a `}`, and a `Description` with a lone unmatched `{` — +// all of which break brace-counting but not a real parse. +// - MyBucket / MyBucketF68F3FF0 are a prefix-collision pair. +// - MyBucket also appears as a value (Fn::Sub, DependsOn), never as another key. +const TEMPLATE = { + Resources: { + MyBucket: { + Type: 'AWS::S3::Bucket', + Properties: { BucketName: 'plain-bucket' }, + }, + MyBucketF68F3FF0: { + Type: 'AWS::S3::Bucket', + Properties: { VersioningConfiguration: { Status: 'Enabled' } }, + }, + Policy: { + Type: 'AWS::IAM::Policy', + Properties: { + PolicyDocument: { + Statement: [{ Action: 's3:*', Resource: { 'Fn::Sub': 'arn:${AWS::Partition}:s3:::${MyBucket}/*' } }], + }, + InlineJson: '{"key":"va}lue"}', + Description: 'trailing brace } then a lone open { inside a string', + }, + DependsOn: ['MyBucket'], + }, + Topic: { Type: 'AWS::SNS::Topic' }, + }, +} as const; + +// 1-space indent mirrors `JSON.stringify(template, undefined, 1)` in synth. +const TEMPLATE_TEXT = JSON.stringify(TEMPLATE, undefined, 1); +const RESOURCES: Record = TEMPLATE.Resources; +const LOGICAL_IDS = Object.keys(RESOURCES); + +/** Slice the serialized text by a computed range and re-parse it. */ +const sliceParse = (range: { start: number; end: number }) => + JSON.parse(TEMPLATE_TEXT.slice(range.start, range.end)); + +describe('resolveResourceRange', () => { + // The authoritative check: a range is correct iff the substring it selects + // re-parses to the same value the template's own JSON.parse produced. No + // hand-computed offsets, and it runs over every resource. + test.each(LOGICAL_IDS)('the resolved range for %s re-parses to that exact resource', (logicalId) => { + const block = resolveResourceRange(TEMPLATE_TEXT, logicalId)!; + expect(sliceParse(block)).toEqual(RESOURCES[logicalId]); + }); + + test('resolves a block whose property values contain brace/quote hazards', () => { + // Policy holds an Fn::Sub (${...}), an InlineJson value with escaped quotes + // and a }, and a Description with a lone {, all of which defeat brace counting. + const block = resolveResourceRange(TEMPLATE_TEXT, 'Policy')!; + expect(sliceParse(block)).toEqual(RESOURCES.Policy); + }); + + test('a logical id resolves to its own block despite a prefix-named sibling', () => { + const short = resolveResourceRange(TEMPLATE_TEXT, 'MyBucket')!; + const long = resolveResourceRange(TEMPLATE_TEXT, 'MyBucketF68F3FF0')!; + expect(sliceParse(short)).toEqual(RESOURCES.MyBucket); + expect(sliceParse(long)).toEqual(RESOURCES.MyBucketF68F3FF0); + expect(short).not.toEqual(long); + }); + + test('resolves the definition block even when the id also appears as a value', () => { + // MyBucket appears under Policy's Fn::Sub and DependsOn, never as another key. + const block = resolveResourceRange(TEMPLATE_TEXT, 'MyBucket')!; + expect((sliceParse(block) as { Type: string }).Type).toBe('AWS::S3::Bucket'); + }); + + test('returns undefined for invalid JSON (for example a trailing comma)', () => { + // json-source-map is a strict parser, so a malformed template yields no + // range rather than a wrong one. Synthesized templates are always strict JSON. + const invalid = '{\n "Resources": {\n "B": {\n "Type": "AWS::S3::Bucket",\n }\n }\n}'; + expect(resolveResourceRange(invalid, 'B')).toBeUndefined(); + }); + + test('returns undefined for an unknown logical id', () => { + expect(resolveResourceRange(TEMPLATE_TEXT, 'DoesNotExist')).toBeUndefined(); + }); + + test('returns undefined when the text cannot be parsed into a tree', () => { + expect(resolveResourceRange('not json at all', 'MyBucket')).toBeUndefined(); + }); +}); + +describe('resolveLogicalIdAtOffset', () => { + // Inverse round-trip: an offset inside each resource's block resolves back to it. + test.each(LOGICAL_IDS)('an offset inside %s resolves to that logical id', (logicalId) => { + const block = resolveResourceRange(TEMPLATE_TEXT, logicalId)!; + const mid = Math.floor((block.start + block.end) / 2); + expect(resolveLogicalIdAtOffset(TEMPLATE_TEXT, mid)).toBe(logicalId); + }); + + test('an offset in a nested property value resolves to its owning resource', () => { + expect(resolveLogicalIdAtOffset(TEMPLATE_TEXT, TEMPLATE_TEXT.indexOf('lone open'))).toBe('Policy'); + }); + + test('returns undefined for an offset outside any resource block', () => { + // Offset 0 is the opening brace of the whole document, before Resources. + expect(resolveLogicalIdAtOffset(TEMPLATE_TEXT, 0)).toBeUndefined(); + }); + + test('returns undefined when the text cannot be parsed into a tree', () => { + expect(resolveLogicalIdAtOffset('not json at all', 3)).toBeUndefined(); + }); +}); diff --git a/packages/@aws-cdk/cloud-assembly-api/tsconfig.dev.json b/packages/@aws-cdk/cloud-assembly-api/tsconfig.dev.json index 3b7a2facb..77acfb5f3 100644 --- a/packages/@aws-cdk/cloud-assembly-api/tsconfig.dev.json +++ b/packages/@aws-cdk/cloud-assembly-api/tsconfig.dev.json @@ -26,6 +26,7 @@ "target": "ES2020", "types": [ "jest", + "json-source-map", "node" ], "incremental": true, diff --git a/packages/@aws-cdk/cloud-assembly-api/tsconfig.json b/packages/@aws-cdk/cloud-assembly-api/tsconfig.json index 4bb6c381c..6c246cfb6 100644 --- a/packages/@aws-cdk/cloud-assembly-api/tsconfig.json +++ b/packages/@aws-cdk/cloud-assembly-api/tsconfig.json @@ -28,6 +28,7 @@ "target": "ES2020", "types": [ "jest", + "json-source-map", "node" ], "incremental": true, diff --git a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES index f6831ff4e..dce99b9e7 100644 --- a/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/integ-runner/THIRD_PARTY_LICENSES @@ -29923,6 +29923,32 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ** isarray@1.0.0 - https://www.npmjs.com/package/isarray/v/1.0.0 | MIT +---------------- + +** json-source-map@0.6.1 - https://www.npmjs.com/package/json-source-map/v/0.6.1 | MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** jsonfile@6.2.0 - https://www.npmjs.com/package/jsonfile/v/6.2.0 | MIT diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index e3d58cd49..01d790734 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -30073,6 +30073,32 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ** isarray@1.0.0 - https://www.npmjs.com/package/isarray/v/1.0.0 | MIT +---------------- + +** json-source-map@0.6.1 - https://www.npmjs.com/package/json-source-map/v/0.6.1 | MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** jsonfile@6.2.0 - https://www.npmjs.com/package/jsonfile/v/6.2.0 | MIT diff --git a/packages/cdk-assets/THIRD_PARTY_LICENSES b/packages/cdk-assets/THIRD_PARTY_LICENSES index 149623f1e..5438cc4cd 100644 --- a/packages/cdk-assets/THIRD_PARTY_LICENSES +++ b/packages/cdk-assets/THIRD_PARTY_LICENSES @@ -22174,6 +22174,32 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ** isarray@1.0.0 - https://www.npmjs.com/package/isarray/v/1.0.0 | MIT +---------------- + +** json-source-map@0.6.1 - https://www.npmjs.com/package/json-source-map/v/0.6.1 | MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** jsonschema@1.5.0 - https://www.npmjs.com/package/jsonschema/v/1.5.0 | MIT diff --git a/yarn.lock b/yarn.lock index 4a352da58..aa8f8c6c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -249,6 +249,7 @@ __metadata: "@cdklabs/eslint-plugin": "npm:^2.0.8" "@stylistic/eslint-plugin": "npm:^3" "@types/jest": "npm:^29.5.14" + "@types/json-source-map": "npm:^0.6.0" "@types/node": "npm:^20" "@typescript-eslint/eslint-plugin": "npm:^8" "@typescript-eslint/parser": "npm:^8" @@ -263,6 +264,7 @@ __metadata: eslint-plugin-prettier: "npm:^4.2.5" jest: "npm:^29.7.0" jest-junit: "npm:^16" + json-source-map: "npm:^0.6.1" jsonschema: "npm:^1.5.0" license-checker: "npm:^25.0.1" nx: "npm:^22.7.5" @@ -6819,6 +6821,13 @@ __metadata: languageName: node linkType: hard +"@types/json-source-map@npm:^0.6.0": + version: 0.6.0 + resolution: "@types/json-source-map@npm:0.6.0" + checksum: 10c0/b7db2f52735acc70b70bf49b1e945f4749ac5c9c8429c44ad23c751193f89d030400ed966c416540647745414cd450f35795395bf0be9eae94816bbe03ad0a2f + languageName: node + linkType: hard + "@types/json5@npm:^0.0.29": version: 0.0.29 resolution: "@types/json5@npm:0.0.29" @@ -13657,6 +13666,13 @@ __metadata: languageName: node linkType: hard +"json-source-map@npm:^0.6.1": + version: 0.6.1 + resolution: "json-source-map@npm:0.6.1" + checksum: 10c0/9fe819f7dfc1407caf8e36246f18376eb511b969954d457b251dfb506df74544cefc0c368f64474b512956eeb37807e03045332a9712ddd14b836d54debe03c3 + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" From 5afd77019b70c0c26c12501f25c8d7782491a65d Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:30:54 -0400 Subject: [PATCH 08/28] feat: refresh diagnostics and CodeLens when cdk.out changes (#1630) The LSP currently reads `cdk.out` once at startup and never refreshes. After this PR, any rewrite of `cdk.out` refreshes the editor's diagnostics and CodeLenses automatically. * New `lib/core/assembly-watcher.ts`: a chokidar-backed watcher with a debounced 200ms `onChange`, filtered to `manifest.json`, `tree.json`, `validation-report.json`. RWLock marker files (`synth.lock`, `read...lock`) are excluded. Throws from `onChange` route through `onError` rather than leaking from the timer. Lives in `lib/core/` so the web explorer can reuse it. * `refreshFromAssembly` is now the single fan-out for new assembly data: rebuild `cachedIndex`, publish empty diagnostics for URIs that no longer have violations (clearing resolved squiggles), republish current diagnostics, and send `workspace/codeLens/refresh` (gated on `workspace.codeLens.refreshSupport`). * When the validation report fails to load, last-good diagnostics are preserved, matching the existing contract for `'error'` and `'not-found'` reads. * Watcher started in `onInitialized` after the initial refresh, closed in `onShutdown`. Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 1 + .../@aws-cdk/cdk-explorer/.projen/deps.json | 5 + .../cdk-explorer/lib/core/assembly-reader.ts | 15 +- .../cdk-explorer/lib/core/assembly-watcher.ts | 119 ++++++++++++ .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 67 ++++++- packages/@aws-cdk/cdk-explorer/package.json | 1 + .../test/_fixtures/builders.test.ts | 3 +- .../cdk-explorer/test/_fixtures/builders.ts | 28 +-- .../test/core/assembly-reader.test.ts | 8 +- .../test/core/assembly-watcher.test.ts | 137 ++++++++++++++ .../cdk-explorer/test/lsp/server.test.ts | 179 +++++++++++++++--- .../cloud-assembly-api/lib/cloud-assembly.ts | 2 +- yarn.lock | 1 + 13 files changed, 502 insertions(+), 64 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index 61cdd08bf..2680b537e 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1688,6 +1688,7 @@ const cdkExplorer = configureProject( 'vscode-languageserver-textdocument@^1', 'vscode-jsonrpc@^8', 'express@^4', + 'chokidar@^4', '@jridgewell/trace-mapping@^0.3', 'convert-source-map@^2', ], diff --git a/packages/@aws-cdk/cdk-explorer/.projen/deps.json b/packages/@aws-cdk/cdk-explorer/.projen/deps.json index 2c7be48f1..0bed08be3 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/deps.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/deps.json @@ -125,6 +125,11 @@ "version": "^0.3", "type": "runtime" }, + { + "name": "chokidar", + "version": "^4", + "type": "runtime" + }, { "name": "convert-source-map", "version": "^2", diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts index 452267218..385b8f786 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { buildConstructTree, CloudAssembly, type ConstructTreeNode } from '@aws-cdk/cloud-assembly-api'; +import { buildConstructTree, CloudAssembly, MANIFEST_FILE, type ConstructTreeNode } from '@aws-cdk/cloud-assembly-api'; import { VALIDATION_REPORT_FILE, type PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; import { findCreationStackTrace } from '@aws-cdk/toolkit-lib'; import { SourceMapResolver, type SourceLocation } from './source-resolver'; @@ -19,8 +19,6 @@ export interface ConstructNode extends ConstructTreeNode { export interface AssemblyData { readonly tree: readonly ConstructNode[]; readonly violations?: PolicyValidationReportJson; - /** Set when validation-report.json fails to load. The tree still loads. */ - readonly violationsError?: string; /** Non-fatal warnings collected while reading the assembly (e.g. unparseable source maps). */ readonly warnings: readonly string[]; } @@ -36,7 +34,7 @@ export type AssemblyReadResult = * (tree.json + stack-metadata join) is delegated to buildConstructTree. */ export function readAssembly(assemblyDir: string): AssemblyReadResult { - const manifestPath = path.join(assemblyDir, 'manifest.json'); + const manifestPath = path.join(assemblyDir, MANIFEST_FILE); if (!fs.existsSync(manifestPath)) { return { status: 'not-found' }; } @@ -54,16 +52,19 @@ export function readAssembly(assemblyDir: string): AssemblyReadResult { })); let violations: PolicyValidationReportJson | undefined; - let violationsError: string | undefined; + const warnings = [...sourceResolver.warnings]; try { violations = loadViolations(assemblyDir); } catch (err) { - violationsError = (err as Error).message; + // The producer writes the validation report synchronously, so a corrupt + // file is not reachable through normal synth flow. We treat it the same + // as "no report present" and surface a warning for the rare case. + warnings.push(`Failed to load ${VALIDATION_REPORT_FILE}: ${(err as Error).message}`); } return { status: 'success', - data: { tree, violations, violationsError, warnings: sourceResolver.warnings }, + data: { tree, violations, warnings }, }; } catch (err) { return { status: 'error', message: (err as Error).message }; diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts new file mode 100644 index 000000000..d7a1d5543 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts @@ -0,0 +1,119 @@ +import * as path from 'path'; +import { MANIFEST_FILE } from '@aws-cdk/cloud-assembly-api'; +import { VALIDATION_REPORT_FILE } from '@aws-cdk/cloud-assembly-schema'; +import * as chokidar from 'chokidar'; + +/** + * The name of the construct tree metadata file emitted alongside the manifest. + * Producer (`aws-cdk-lib`) hard-codes this filename in its `TreeMetadata` + * synthesizer rather than importing a shared constant, so this is a + * consumer-side label only. + */ +export const TREE_FILE = 'tree.json'; + +/** + * Basenames whose change signals that the cloud assembly was (re)written. + * The construct tree, manifest, and policy validation report cover everything + * the explorer reads; other files in cdk.out (templates, asset files, RWLock + * markers) are intentionally ignored to avoid spurious refreshes. + */ +const ASSEMBLY_SIGNAL_FILES = new Set([ + MANIFEST_FILE, + TREE_FILE, + VALIDATION_REPORT_FILE, +]); + +const DEBOUNCE_MS = 200; + +/** A running assembly watcher. */ +export interface AssemblyWatcher { + /** Stop watching and release the underlying file handles. */ + close(): Promise; +} + +/** + * Minimal file-watcher surface this module depends on, satisfied by chokidar's + * `FSWatcher`. Declared explicitly so tests can inject a fake emitter and verify + * debouncing/filtering without real filesystem events. + */ +export interface FileWatcher { + on(event: 'all', listener: (eventName: string, filePath: string) => void): FileWatcher; + on(event: 'error', listener: (error: unknown) => void): FileWatcher; + close(): Promise; +} + +export interface AssemblyWatcherOptions { + /** The cloud assembly directory to watch (e.g. `/cdk.out`). */ + readonly assemblyDir: string; + /** Invoked (debounced) when the assembly's signal files change. */ + readonly onChange: () => void; + /** Receives non-fatal watcher errors. */ + readonly onError?: (error: unknown) => void; + /** + * Factory for the underlying file watcher. Defaults to chokidar; overridden + * in tests with a fake so behavior is verified without real file IO. + */ + readonly createWatcher?: (assemblyDir: string) => FileWatcher; +} + +// Thin wrapper over real chokidar; exercised via integration, not unit tests. +/* c8 ignore start */ +function defaultCreateWatcher(assemblyDir: string): FileWatcher { + // Watch the assembly directory itself. chokidar tolerates the directory not + // existing yet and emits events once synth creates it. `ignoreInitial` skips + // the synthetic 'add' events for already-present files, because the caller + // performs its own initial read separately. + return chokidar.watch(assemblyDir, { + ignoreInitial: true, + }) as unknown as FileWatcher; +} +/* c8 ignore stop */ + +/** + * Watch a cloud assembly directory and fire `onChange` (debounced) whenever the + * assembly is rewritten, regardless of which process produced it (an external + * `cdk synth`/`cdk watch`, or a future in-process synth). + * + * Only the assembly signal files trigger a refresh. Template files and, crucially, + * the RWLock marker files (`synth.lock` / `read..lock`) are ignored: their + * rapid create/delete during a synth would otherwise cause spurious refreshes. + */ +export function startAssemblyWatcher(options: AssemblyWatcherOptions): AssemblyWatcher { + const createWatcher = options.createWatcher ?? defaultCreateWatcher; + + let timer: NodeJS.Timeout | undefined; + let closed = false; + + const watcher = createWatcher(options.assemblyDir); + + watcher.on('all', (_eventName, filePath) => { + if (closed) return; + if (!ASSEMBLY_SIGNAL_FILES.has(path.basename(filePath))) return; + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + timer = undefined; + try { + options.onChange(); + } catch (error) { + options.onError?.(error); + } + }, DEBOUNCE_MS); + }); + + watcher.on('error', (error) => { + options.onError?.(error); + }); + + return { + async close() { + closed = true; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + await watcher.close(); + }, + }; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 8d081b6f5..04b24259f 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -8,6 +8,7 @@ import { } from 'vscode-jsonrpc/node'; import { createConnection, + CodeLensRefreshRequest, ProposedFeatures, TextDocumentSyncKind, type CodeLens, @@ -31,6 +32,11 @@ import { type AssemblyReadResult, type ConstructNode, } from '../core/assembly-reader'; +import { + startAssemblyWatcher as defaultStartAssemblyWatcher, + type AssemblyWatcher, + type AssemblyWatcherOptions, +} from '../core/assembly-watcher'; export interface LspHandlerOptions { /** Callback invoked on `didSave` for tracked source files. */ @@ -44,6 +50,17 @@ export interface LspHandlerOptions { readonly logger?: LogSink; /** Receives diagnostics ready to be published to the editor. */ readonly onPublishDiagnostics?: (uri: string, diagnostics: Diagnostic[]) => void; + /** + * Invoked after a refresh when new assembly data may have changed CodeLenses, + * to ask the editor to re-query them. Only called when the client advertised + * `workspace.codeLens.refreshSupport`. + */ + readonly onRefreshCodeLenses?: () => void; + /** + * Starts the cdk.out watcher. Defaults to the real chokidar-backed watcher; + * overridden in tests to drive refreshes deterministically. + */ + readonly startAssemblyWatcher?: (options: AssemblyWatcherOptions) => AssemblyWatcher; } export interface LspServerOptions extends LspHandlerOptions { @@ -84,13 +101,25 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers const log = options.logger ?? NOOP_LOGGER; const onPublishDiagnostics = options.onPublishDiagnostics ?? (() => { }); + const onRefreshCodeLenses = options.onRefreshCodeLenses ?? (() => { + }); + const startWatcher = options.startAssemblyWatcher ?? defaultStartAssemblyWatcher; let applicationDir: string | undefined; let shutdownRequested = false; let shouldIgnore: (filePath: string) => boolean = () => false; - // Latest index from readAssembly, served to CodeLens without re-reading - // cdk.out. Refreshed on every onInitialized; cdk.out watcher is a future feature. + let assemblyWatcher: AssemblyWatcher | undefined; + // Latest index from readAssembly, served to CodeLens. Refreshed at startup + // and whenever the cdk.out watcher detects a re-synth. let cachedIndex: ConstructIndex = ConstructIndex.fromTree([]); + // URIs that currently have published diagnostics. On each refresh we publish + // an empty array for any URI that no longer has diagnostics, otherwise a + // resolved violation would leave a stale squiggle behind. + let publishedUris = new Set(); + // Whether the client supports a server-initiated CodeLens refresh. Captured + // at initialize; if false we skip the refresh request and lenses update on + // the editor's next natural re-query. + let codeLensRefreshSupported = false; function refreshFromAssembly(projectDir: string): void { const assemblyDir = path.join(projectDir, 'cdk.out'); @@ -102,13 +131,10 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers } if (result.status === 'not-found') return; - const { tree, violations, violationsError, warnings } = result.data; + const { tree, violations, warnings } = result.data; for (const warning of warnings) { log.warn(warning); } - if (violationsError) { - log.warn(`validation-report.json failed to load: ${violationsError}`); - } cachedIndex = ConstructIndex.fromTree(tree); @@ -117,14 +143,30 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers for (const drop of dropped) { log.warn(`Dropped diagnostic for '${drop.ruleName}' at '${drop.constructPath}': ${drop.reason}`); } + const nextUris = new Set(byUri.keys()); + // Clear diagnostics for files that had violations on the previous refresh + // but no longer do, so resolved violations disappear from the editor. + for (const uri of publishedUris) { + if (!nextUris.has(uri)) { + onPublishDiagnostics(uri, []); + } + } for (const [uri, diagnostics] of byUri) { onPublishDiagnostics(uri, diagnostics); } + publishedUris = nextUris; + + // New assembly data may change lens titles or positions; ask the editor to + // re-query CodeLenses (it serves them from the now-updated cachedIndex). + if (codeLensRefreshSupported) { + onRefreshCodeLenses(); + } } return { onInitialize(params) { applicationDir = params.initializationOptions?.applicationDir; + codeLensRefreshSupported = params.capabilities.workspace?.codeLens?.refreshSupport ?? false; return { capabilities: { textDocumentSync: { @@ -158,6 +200,14 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers rootDir: projectDir, }); refreshFromAssembly(projectDir); + + // Watch cdk.out so any synth (an external `cdk synth`/`cdk watch`, or a + // future in-process synth) refreshes the editor's diagnostics and lenses. + assemblyWatcher = startWatcher({ + assemblyDir: path.join(projectDir, 'cdk.out'), + onChange: () => refreshFromAssembly(projectDir), + onError: (err) => log.error(`Assembly watcher error: ${(err as Error).message}`), + }); }, onDidSaveTextDocument(params) { if (shutdownRequested) return; @@ -196,6 +246,8 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers }, onShutdown() { shutdownRequested = true; + void assemblyWatcher?.close(); + assemblyWatcher = undefined; }, }; } @@ -214,6 +266,9 @@ export function startServer(options: LspServerOptions): void { onPublishDiagnostics: (uri, diagnostics) => { void connection.sendDiagnostics({ uri, diagnostics }); }, + onRefreshCodeLenses: () => { + void connection.sendRequest(CodeLensRefreshRequest.type); + }, }); connection.onInitialize((params) => handlers.onInitialize(params)); diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index 40ac05126..c1569f19b 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -60,6 +60,7 @@ "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/toolkit-lib": "^0.0.0", "@jridgewell/trace-mapping": "^0.3", + "chokidar": "^4", "convert-source-map": "^2", "express": "^4", "vscode-jsonrpc": "^8", diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts index 68f4e65c0..ef2ca50c5 100644 --- a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts @@ -198,7 +198,7 @@ describe('fixture builders', () => { expect(result.data.tree).toHaveLength(1); expect(result.data.violations).toBeUndefined(); - expect(result.data.violationsError).toBeTruthy(); + expect(result.data.warnings.some((w) => w.includes('validation-report.json'))).toBe(true); }); test('withValidationReport produces a parseable report', () => { @@ -221,7 +221,6 @@ describe('fixture builders', () => { const result = readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); - expect(result.data.violationsError).toBeUndefined(); expect(result.data.violations?.pluginReports[0].pluginName).toBe('test'); }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts index 931bf9637..d87ee21ab 100644 --- a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts @@ -1,7 +1,9 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { MANIFEST_FILE } from '@aws-cdk/cloud-assembly-api'; import { ArtifactMetadataEntryType, VALIDATION_REPORT_FILE } from '@aws-cdk/cloud-assembly-schema'; +import { TREE_FILE } from '../../lib/core/assembly-watcher'; /** * Programmatic fixture builders. Each builder writes a minimal cdk.out/ to a @@ -87,7 +89,7 @@ export interface NestedStackParentSpec { export function buildFlatAssembly(spec: FlatAssemblySpec): string { const dir = mkAssemblyDir('flat'); const artifacts: Record = { - Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + Tree: { type: 'cdk:tree', properties: { file: TREE_FILE } }, }; for (const stack of spec.stacks) { @@ -95,11 +97,11 @@ export function buildFlatAssembly(spec: FlatAssemblySpec): string { writeTemplate(dir, stack.id, stack.resources, stack.id, spec.pathMetadata ?? true); } - writeJson(path.join(dir, 'manifest.json'), { + writeJson(path.join(dir, MANIFEST_FILE), { version: ASSEMBLY_SCHEMA_VERSION, artifacts, }); - writeJson(path.join(dir, 'tree.json'), { + writeJson(path.join(dir, TREE_FILE), { version: TREE_SCHEMA_VERSION, tree: appNode(spec.stacks.map(stackTreeNode)), }); @@ -111,7 +113,7 @@ export function buildFlatAssembly(spec: FlatAssemblySpec): string { export function buildNestedAssembly(spec: NestedAssemblySpec): string { const rootDir = mkAssemblyDir('nested'); const rootArtifacts: Record = { - Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + Tree: { type: 'cdk:tree', properties: { file: TREE_FILE } }, }; const treeChildren: Record = {}; @@ -142,7 +144,7 @@ export function buildNestedAssembly(spec: NestedAssemblySpec): string { writeTemplate(stageDir, artifactId, stack.resources, constructPath); } - writeJson(path.join(stageDir, 'manifest.json'), { + writeJson(path.join(stageDir, MANIFEST_FILE), { version: ASSEMBLY_SCHEMA_VERSION, artifacts: stageArtifacts, }); @@ -161,11 +163,11 @@ export function buildNestedAssembly(spec: NestedAssemblySpec): string { }; } - writeJson(path.join(rootDir, 'manifest.json'), { + writeJson(path.join(rootDir, MANIFEST_FILE), { version: ASSEMBLY_SCHEMA_VERSION, artifacts: rootArtifacts, }); - writeJson(path.join(rootDir, 'tree.json'), { + writeJson(path.join(rootDir, TREE_FILE), { version: TREE_SCHEMA_VERSION, tree: appNode(Object.values(treeChildren)), }); @@ -202,10 +204,10 @@ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec } emitNestedStack(dir, metadata, parentChildren, parentResources, parent.id, ns); } - writeJson(path.join(dir, 'manifest.json'), { + writeJson(path.join(dir, MANIFEST_FILE), { version: ASSEMBLY_SCHEMA_VERSION, artifacts: { - Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + Tree: { type: 'cdk:tree', properties: { file: TREE_FILE } }, [parent.id]: { type: 'aws:cloudformation:stack', environment: 'aws://unknown-account/unknown-region', @@ -215,7 +217,7 @@ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec } }, }, }); - writeJson(path.join(dir, 'tree.json'), { + writeJson(path.join(dir, TREE_FILE), { version: TREE_SCHEMA_VERSION, tree: appNode([{ id: parent.id, @@ -301,7 +303,7 @@ function emitNestedStack( /** Manifest + tree with no metadata, no traces — for non-TS app graceful-degradation tests. */ export function buildNonTypeScriptAssembly(): string { const dir = mkAssemblyDir('nonts'); - writeJson(path.join(dir, 'manifest.json'), { + writeJson(path.join(dir, MANIFEST_FILE), { version: ASSEMBLY_SCHEMA_VERSION, artifacts: { Stack1: { @@ -311,10 +313,10 @@ export function buildNonTypeScriptAssembly(): string { displayName: 'Stack1', // No metadata: non-TS apps emit no aws:cdk:logicalId entries. }, - Tree: { type: 'cdk:tree', properties: { file: 'tree.json' } }, + Tree: { type: 'cdk:tree', properties: { file: TREE_FILE } }, }, }); - writeJson(path.join(dir, 'tree.json'), { + writeJson(path.join(dir, TREE_FILE), { version: TREE_SCHEMA_VERSION, tree: { id: 'App', diff --git a/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts index e9c0eff91..19af13d53 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { MANIFEST_FILE } from '@aws-cdk/cloud-assembly-api'; import { readAssembly, type AssemblyData, type AssemblyReadResult, type ConstructNode } from '../../lib'; import { buildFlatAssembly, @@ -101,7 +102,7 @@ describe('readAssembly', () => { test('returns error for malformed manifest', () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-malformed-')); - fs.writeFileSync(path.join(tmpDir, 'manifest.json'), 'not json{{{'); + fs.writeFileSync(path.join(tmpDir, MANIFEST_FILE), 'not json{{{'); try { const result = readAssembly(tmpDir); expect(result.status).toBe('error'); @@ -143,7 +144,6 @@ describe('readAssembly with violations', () => { expect(data.violations).toBeDefined(); expect(data.violations!.pluginReports[0].conclusion).toBe('failure'); expect(data.violations!.pluginReports[0].violations[0].ruleName).toBe('no-public-buckets'); - expect(data.violationsError).toBeUndefined(); }); test('returns undefined violations when report file is absent', () => { @@ -151,7 +151,6 @@ describe('readAssembly with violations', () => { const data = expectSuccess(readAssembly(dir)); expect(data.violations).toBeUndefined(); - expect(data.violationsError).toBeUndefined(); }); test('malformed validation report does not crash the tree read', () => { @@ -162,7 +161,7 @@ describe('readAssembly with violations', () => { expect(data.tree).toHaveLength(1); expect(data.violations).toBeUndefined(); - expect(data.violationsError).toBeTruthy(); + expect(data.warnings.some((w) => w.includes('validation-report.json'))).toBe(true); }); test('loads a version-less validation report (legacy aws-cdk-lib shape)', () => { @@ -173,7 +172,6 @@ describe('readAssembly with violations', () => { expect(data.tree).toHaveLength(1); expect(data.violations).toBeDefined(); - expect(data.violationsError).toBeUndefined(); }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts new file mode 100644 index 000000000..89b414f54 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts @@ -0,0 +1,137 @@ +import { MANIFEST_FILE } from '@aws-cdk/cloud-assembly-api'; +import { VALIDATION_REPORT_FILE } from '@aws-cdk/cloud-assembly-schema'; +import { startAssemblyWatcher, TREE_FILE, type FileWatcher } from '../../lib/core/assembly-watcher'; + +type AnyListener = (...args: unknown[]) => void; + +/** A controllable in-memory stand-in for chokidar's FSWatcher. */ +class FakeWatcher implements FileWatcher { + public closed = false; + private readonly listeners: Record = {}; + + public on(event: string, listener: AnyListener): FileWatcher { + (this.listeners[event] ??= []).push(listener); + return this; + } + + /** Simulate a chokidar 'all' event for the given file. */ + public emitFile(eventName: string, filePath: string): void { + for (const listener of this.listeners.all ?? []) { + listener(eventName, filePath); + } + } + + /** Simulate any emitted event (e.g. 'error'). */ + public emit(event: string, ...args: unknown[]): void { + for (const listener of this.listeners[event] ?? []) { + listener(...args); + } + } + + public async close(): Promise { + this.closed = true; + } +} + +function setup() { + const fake = new FakeWatcher(); + const onChange = jest.fn(); + const watcher = startAssemblyWatcher({ + assemblyDir: '/p/cdk.out', + onChange, + createWatcher: () => fake, + }); + return { fake, onChange, watcher }; +} + +describe('Assembly Watcher', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + test('coalesces a burst of assembly file changes into a single onChange', () => { + const { fake, onChange } = setup(); + + fake.emitFile('change', `/p/cdk.out/${MANIFEST_FILE}`); + fake.emitFile('change', `/p/cdk.out/${TREE_FILE}`); + fake.emitFile('change', `/p/cdk.out/${VALIDATION_REPORT_FILE}`); + expect(onChange).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(200); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + test('ignores RWLock marker files', () => { + const { fake, onChange } = setup(); + + fake.emitFile('add', '/p/cdk.out/synth.lock'); + fake.emitFile('add', '/p/cdk.out/read.12345.1.lock'); + fake.emitFile('unlink', '/p/cdk.out/synth.lock'); + + jest.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('ignores non-signal files such as templates', () => { + const { fake, onChange } = setup(); + + fake.emitFile('change', '/p/cdk.out/MyStack.template.json'); + + jest.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('reacts to nested stage assembly manifests', () => { + const { fake, onChange } = setup(); + + fake.emitFile('add', `/p/cdk.out/assembly-Prod/${MANIFEST_FILE}`); + + jest.advanceTimersByTime(200); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + test('close stops a pending onChange and closes the underlying watcher', async () => { + const { fake, onChange, watcher } = setup(); + + fake.emitFile('change', `/p/cdk.out/${MANIFEST_FILE}`); + await watcher.close(); + + jest.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + expect(fake.closed).toBe(true); + }); + + test('forwards watcher errors to onError', () => { + const fake = new FakeWatcher(); + const onError = jest.fn(); + startAssemblyWatcher({ + assemblyDir: '/p/cdk.out', + onChange: jest.fn(), + createWatcher: () => fake, + onError, + }); + + fake.emit('error', new Error('boom')); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + }); + + test('forwards an onChange throw to onError instead of leaking from the timer', () => { + const fake = new FakeWatcher(); + const onError = jest.fn(); + const failure = new Error('refresh blew up'); + startAssemblyWatcher({ + assemblyDir: '/p/cdk.out', + onChange: () => { + throw failure; + }, + createWatcher: () => fake, + onError, + }); + + fake.emitFile('change', `/p/cdk.out/${MANIFEST_FILE}`); + expect(() => jest.advanceTimersByTime(200)).not.toThrow(); + + expect(onError).toHaveBeenCalledWith(failure); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index 51a4e31ea..86da656ff 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { pathToFileURL } from 'url'; -import type { Diagnostic } from 'vscode-languageserver/node'; +import type { Diagnostic, InitializeParams } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; import type { AssemblyReadResult } from '../../lib'; import { createLspHandlers, type LspHandlerOptions, type LspHandlers } from '../../lib/lsp/server'; @@ -11,11 +11,18 @@ interface CapturedClient { handlers: LspHandlers; published: Array<{ uri: string; diagnostics: Diagnostic[] }>; log: { warn: jest.Mock; error: jest.Mock }; + refreshCodeLens: jest.Mock; + watcherClosed: jest.Mock; + /** Fire the cdk.out watcher's onChange, as a real re-synth would. */ + triggerWatcher: () => void; } function createTestClient(opts?: Partial>): CapturedClient { const published: Array<{ uri: string; diagnostics: Diagnostic[] }> = []; const log = { warn: jest.fn(), error: jest.fn() }; + const refreshCodeLens = jest.fn(); + const watcherClosed = jest.fn(); + let watcherOnChange: (() => void) | undefined; const handlers = createLspHandlers({ onSynthRequest: opts?.onSynthRequest, // Default to "no assembly" so tests that don't care about diagnostics @@ -23,20 +30,86 @@ function createTestClient(opts?: Partial ({ status: 'not-found' })), logger: log, onPublishDiagnostics: (uri, diagnostics) => published.push({ uri, diagnostics }), + onRefreshCodeLenses: refreshCodeLens, + // Inject a fake watcher so unit tests never start a real chokidar instance; + // capture its onChange so tests can simulate a re-synth deterministically. + startAssemblyWatcher: (watchOpts) => { + watcherOnChange = watchOpts.onChange; + return { + close: async () => { + watcherClosed(); + }, + }; + }, }); - return { handlers, published, log }; + return { + handlers, + published, + log, + refreshCodeLens, + watcherClosed, + triggerWatcher: () => watcherOnChange?.(), + }; } -function initializeClient(client: CapturedClient, options?: Record): void { +function initializeClient( + client: CapturedClient, + options?: Record, + capabilities?: InitializeParams['capabilities'], +): void { client.handlers.onInitialize({ processId: null, - capabilities: {}, + capabilities: capabilities ?? {}, rootUri: null, initializationOptions: options ?? {}, }); client.handlers.onInitialized(); } +function bucketViolationFixtures() { + const tree = [{ + path: 'Stack1', + id: 'Stack1', + children: [{ + path: 'Stack1/MyBucket', + id: 'MyBucket', + children: [], + sourceLocation: { file: '/p/lib/stack.ts', line: 12, column: 5 }, + }], + }]; + const violations = { + version: '1.0.0', + pluginReports: [{ + pluginName: 'test-plugin', + conclusion: 'failure', + violations: [{ + ruleName: 'no-public-buckets', + description: 'no public buckets', + severity: 'error', + violatingConstructs: [{ constructPath: 'Stack1/MyBucket' }], + }], + }], + }; + return { tree, violations }; +} + +/** + * A readAssembly that reports a violation on the first read and the same tree + * with the violation resolved on every read after, simulating a user fixing it + * and re-synthing. + */ +function readAssemblyResolvingAfterFirst(): () => AssemblyReadResult { + const { tree, violations } = bucketViolationFixtures(); + let call = 0; + return (): AssemblyReadResult => { + call += 1; + return { + status: 'success', + data: call === 1 ? { warnings: [], tree, violations } : { warnings: [], tree }, + }; + }; +} + describe('LSP Server', () => { test('initialize advertises codeLens, definition, and save-sync capabilities', () => { const client = createTestClient(); @@ -144,35 +217,11 @@ describe('LSP Server', () => { }); test('publishes diagnostics on initialized when assembly has violations', () => { + const { tree, violations } = bucketViolationFixtures(); const client = createTestClient({ readAssembly: () => ({ status: 'success', - data: { - warnings: [], - tree: [{ - path: 'Stack1', - id: 'Stack1', - children: [{ - path: 'Stack1/MyBucket', - id: 'MyBucket', - children: [], - sourceLocation: { file: '/p/lib/stack.ts', line: 12, column: 5 }, - }], - }], - violations: { - version: '1.0.0', - pluginReports: [{ - pluginName: 'test-plugin', - conclusion: 'failure', - violations: [{ - ruleName: 'no-public-buckets', - description: 'no public buckets', - severity: 'error', - violatingConstructs: [{ constructPath: 'Stack1/MyBucket' }], - }], - }], - }, - }, + data: { warnings: [], tree, violations }, }), }); @@ -229,6 +278,76 @@ describe('LSP Server', () => { expect(client.published).toHaveLength(0); }); + test('clears diagnostics for a violation resolved on a later refresh', () => { + const client = createTestClient({ readAssembly: readAssemblyResolvingAfterFirst() }); + + initializeClient(client, { applicationDir: '/p' }); + expect(client.published).toHaveLength(1); + const violationUri = client.published[0].uri; + expect(client.published[0].diagnostics).toHaveLength(1); + + // Simulate a re-synth picked up by the cdk.out watcher. + client.triggerWatcher(); + + expect(client.published).toHaveLength(2); + expect(client.published[1]).toEqual({ uri: violationUri, diagnostics: [] }); + }); + + test('requests a CodeLens refresh after a refresh when the client supports it', () => { + const client = createTestClient({ + readAssembly: (): AssemblyReadResult => ({ + status: 'success', + data: { warnings: [], tree: [{ path: 'Stack1', id: 'Stack1', children: [] }] }, + }), + }); + + initializeClient(client, { applicationDir: '/p' }, { + workspace: { codeLens: { refreshSupport: true } }, + }); + + expect(client.refreshCodeLens).toHaveBeenCalledTimes(1); + }); + + test('does not request a CodeLens refresh when the client lacks refreshSupport', () => { + const client = createTestClient({ + readAssembly: (): AssemblyReadResult => ({ + status: 'success', + data: { warnings: [], tree: [{ path: 'Stack1', id: 'Stack1', children: [] }] }, + }), + }); + + initializeClient(client, { applicationDir: '/p' }); + + expect(client.refreshCodeLens).not.toHaveBeenCalled(); + }); + + test('a watcher-detected re-synth refreshes diagnostics and lenses', () => { + const client = createTestClient({ readAssembly: readAssemblyResolvingAfterFirst() }); + + initializeClient(client, { applicationDir: '/p' }, { + workspace: { codeLens: { refreshSupport: true } }, + }); + expect(client.published).toHaveLength(1); + const violationUri = client.published[0].uri; + expect(client.refreshCodeLens).toHaveBeenCalledTimes(1); + + // Simulate a re-synth picked up by the cdk.out watcher. + client.triggerWatcher(); + + expect(client.published).toHaveLength(2); + expect(client.published[1]).toEqual({ uri: violationUri, diagnostics: [] }); + expect(client.refreshCodeLens).toHaveBeenCalledTimes(2); + }); + + test('closes the cdk.out watcher on shutdown', () => { + const client = createTestClient(); + initializeClient(client, { applicationDir: '/p' }); + + client.handlers.onShutdown(); + + expect(client.watcherClosed).toHaveBeenCalledTimes(1); + }); + test('onDefinition resolves a template position back to construct source', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'server-def-')); const templateFile = path.join(dir, 'Stack1.template.json'); diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-assembly.ts b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-assembly.ts index d820d708e..026fff831 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/cloud-assembly.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/cloud-assembly.ts @@ -15,7 +15,7 @@ const CLOUD_ASSEMBLY_SYMBOL = Symbol.for('@aws-cdk/cx-api.CloudAssembly'); /** * The name of the root manifest file of the assembly. */ -const MANIFEST_FILE = 'manifest.json'; +export const MANIFEST_FILE = 'manifest.json'; /** * Represents a deployable cloud application. diff --git a/yarn.lock b/yarn.lock index aa8f8c6c2..95e354d6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -187,6 +187,7 @@ __metadata: "@types/node": "npm:^20" "@typescript-eslint/eslint-plugin": "npm:^8" "@typescript-eslint/parser": "npm:^8" + chokidar: "npm:^4" constructs: "npm:^10.0.0" convert-source-map: "npm:^2" eslint: "npm:^9" From cdcf33caeabe226a7572c9844af7a77a8d6a957e Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:25:59 -0400 Subject: [PATCH 09/28] feat: cloudscape frontend (#1606) Builds the `cdk explore` web explorer UI, with a React + Cloudscape single-page app is served by the existing Express server, alongside file-serving API endpoints. - **API** (`lib/web/`): `GET /api/health`, `GET /api/files?dir=`,`GET /api/file?path=`, - **SPA** (`frontend/`): Cloudscape `ContentLayout` with four regions. The two center/right panes are functional server-backed file viewers (browse + view). Header shows a "last updated" placeholder. - **Build**: `frontend/` is compiled by esbuild (separate `tsconfig.frontend.json`, kept off the Node `tsc` path) in `post-compile`, then embedded into `web-assets.generated.json` and served from memory so it travels in the bundled CLI. ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .projenrc.ts | 15 + packages/@aws-cdk/cdk-explorer/.gitignore | 2 + packages/@aws-cdk/cdk-explorer/.npmignore | 2 + .../@aws-cdk/cdk-explorer/.projen/deps.json | 48 + .../@aws-cdk/cdk-explorer/.projen/tasks.json | 9 +- .../build-tools/bundle-frontend.ts | 59 ++ .../@aws-cdk/cdk-explorer/frontend/App.tsx | 40 + .../@aws-cdk/cdk-explorer/frontend/api.ts | 17 + .../frontend/components/FilePane.tsx | 115 +++ .../@aws-cdk/cdk-explorer/frontend/index.html | 13 + .../@aws-cdk/cdk-explorer/frontend/index.tsx | 17 + .../@aws-cdk/cdk-explorer/lib/web/protocol.ts | 18 + .../@aws-cdk/cdk-explorer/lib/web/routes.ts | 106 +++ .../cdk-explorer/lib/web/safe-path.ts | 40 + .../@aws-cdk/cdk-explorer/lib/web/server.ts | 25 +- .../cdk-explorer/lib/web/web-assets.ts | 34 + packages/@aws-cdk/cdk-explorer/package.json | 10 + .../cdk-explorer/test/web/routes.test.ts | 113 +++ .../cdk-explorer/test/web/safe-path.test.ts | 71 ++ .../cdk-explorer/test/web/server.test.ts | 8 + .../@aws-cdk/cdk-explorer/tsconfig.dev.json | 11 +- .../cdk-explorer/tsconfig.frontend.json | 18 + packages/@aws-cdk/cdk-explorer/tsconfig.json | 9 +- packages/aws-cdk/THIRD_PARTY_LICENSES | 99 ++ yarn.lock | 848 +++++++++++++++++- 25 files changed, 1722 insertions(+), 25 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/build-tools/bundle-frontend.ts create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/App.tsx create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/api.ts create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/index.html create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/index.tsx create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/routes.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/safe-path.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/web-assets.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/safe-path.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/tsconfig.frontend.json diff --git a/.projenrc.ts b/.projenrc.ts index 03815e5c0..dc16bcec3 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1692,12 +1692,23 @@ const cdkExplorer = configureProject( devDeps: [ 'vscode-languageserver-protocol@^3', '@types/express@^4', + 'react@^18', + 'react-dom@^18', + '@types/react@^18', + '@types/react-dom@^18', + '@cloudscape-design/components@^3', + '@cloudscape-design/global-styles@^1', + 'esbuild', + 'tsx', + 'supertest@^6', + '@types/supertest@^6', '@types/convert-source-map@^2', ], tsconfig: { compilerOptions: { ...defaultTsOptions, }, + exclude: ['frontend'], }, jestOptions: jestOptionsForProject({ jestConfig: { @@ -1712,6 +1723,10 @@ const cdkExplorer = configureProject( }), ); fixupTestTask(cdkExplorer); +cdkExplorer.postCompileTask.exec('tsx build-tools/bundle-frontend.ts'); +cdkExplorer.tsconfigDev.addInclude('build-tools/**/*.ts'); +cdkExplorer.gitignore.addPatterns('lib/web/static/', 'lib/web/web-assets.generated.json'); +cdkExplorer.npmignore?.addPatterns('frontend', 'tsconfig.frontend.json'); cli.deps.addDependency('@aws-cdk/cdk-explorer', pj.DependencyType.RUNTIME); // #endregion diff --git a/packages/@aws-cdk/cdk-explorer/.gitignore b/packages/@aws-cdk/cdk-explorer/.gitignore index 7722c6259..5cbc9d581 100644 --- a/packages/@aws-cdk/cdk-explorer/.gitignore +++ b/packages/@aws-cdk/cdk-explorer/.gitignore @@ -51,3 +51,5 @@ jspm_packages/ /dist/ !/.eslintrc.json !/.eslintrc.js +lib/web/static/ +lib/web/web-assets.generated.json diff --git a/packages/@aws-cdk/cdk-explorer/.npmignore b/packages/@aws-cdk/cdk-explorer/.npmignore index 75d96a45f..ed5888bfa 100644 --- a/packages/@aws-cdk/cdk-explorer/.npmignore +++ b/packages/@aws-cdk/cdk-explorer/.npmignore @@ -21,4 +21,6 @@ tsconfig.tsbuildinfo *.ts !*.d.ts build-tools +frontend +tsconfig.frontend.json /.gitattributes diff --git a/packages/@aws-cdk/cdk-explorer/.projen/deps.json b/packages/@aws-cdk/cdk-explorer/.projen/deps.json index 2c7be48f1..0a5c77243 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/deps.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/deps.json @@ -4,6 +4,16 @@ "name": "@cdklabs/eslint-plugin", "type": "build" }, + { + "name": "@cloudscape-design/components", + "version": "^3", + "type": "build" + }, + { + "name": "@cloudscape-design/global-styles", + "version": "^1", + "type": "build" + }, { "name": "@stylistic/eslint-plugin", "version": "^3", @@ -28,6 +38,21 @@ "version": "^20", "type": "build" }, + { + "name": "@types/react-dom", + "version": "^18", + "type": "build" + }, + { + "name": "@types/react", + "version": "^18", + "type": "build" + }, + { + "name": "@types/supertest", + "version": "^6", + "type": "build" + }, { "name": "@typescript-eslint/eslint-plugin", "version": "^8", @@ -43,6 +68,10 @@ "version": "^10.0.0", "type": "build" }, + { + "name": "esbuild", + "type": "build" + }, { "name": "eslint-config-prettier", "type": "build" @@ -94,10 +123,29 @@ "name": "projen", "type": "build" }, + { + "name": "react-dom", + "version": "^18", + "type": "build" + }, + { + "name": "react", + "version": "^18", + "type": "build" + }, + { + "name": "supertest", + "version": "^6", + "type": "build" + }, { "name": "ts-jest", "type": "build" }, + { + "name": "tsx", + "type": "build" + }, { "name": "typescript", "version": "5.9", diff --git a/packages/@aws-cdk/cdk-explorer/.projen/tasks.json b/packages/@aws-cdk/cdk-explorer/.projen/tasks.json index 2116f760c..f17cea020 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/tasks.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/tasks.json @@ -39,7 +39,7 @@ }, "steps": [ { - "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,jest,nx,projen,ts-jest" + "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/jest,esbuild,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,jest,nx,projen,ts-jest,tsx" } ] }, @@ -119,7 +119,12 @@ }, "post-compile": { "name": "post-compile", - "description": "Runs after successful compilation" + "description": "Runs after successful compilation", + "steps": [ + { + "exec": "tsx build-tools/bundle-frontend.ts" + } + ] }, "pre-compile": { "name": "pre-compile", diff --git a/packages/@aws-cdk/cdk-explorer/build-tools/bundle-frontend.ts b/packages/@aws-cdk/cdk-explorer/build-tools/bundle-frontend.ts new file mode 100644 index 000000000..7ff372d4a --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/build-tools/bundle-frontend.ts @@ -0,0 +1,59 @@ +/** + * Builds the web explorer SPA into lib/web/static (typechecked via + * tsconfig.frontend.json, since esbuild only transpiles), then writes the same + * assets to lib/web/web-assets.generated.json so they ride the require() graph + * into the published CLI bundle (express.static paths are not bundled). + */ +import { execFileSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as esbuild from 'esbuild'; + +const packageRoot = path.resolve(__dirname, '..'); +const frontendDir = path.join(packageRoot, 'frontend'); +const outDir = path.join(packageRoot, 'lib', 'web', 'static'); +const embeddedAssetsFile = path.join(packageRoot, 'lib', 'web', 'web-assets.generated.json'); + +async function main(): Promise { + typecheck(); + + fs.mkdirSync(outDir, { recursive: true }); + + await esbuild.build({ + entryPoints: [path.join(frontendDir, 'index.tsx')], + bundle: true, + outfile: path.join(outDir, 'bundle.js'), + format: 'iife', + platform: 'browser', + target: 'es2020', + jsx: 'automatic', + loader: { '.css': 'css', '.svg': 'dataurl', '.png': 'dataurl' }, + sourcemap: true, + logLevel: 'info', + }); + + fs.copyFileSync(path.join(frontendDir, 'index.html'), path.join(outDir, 'index.html')); + writeEmbeddedAssets(); +} + +function writeEmbeddedAssets(): void { + const assets: Record = { + 'index.html': fs.readFileSync(path.join(frontendDir, 'index.html'), 'utf-8'), + 'bundle.js': fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf-8'), + 'bundle.css': fs.readFileSync(path.join(outDir, 'bundle.css'), 'utf-8'), + }; + fs.writeFileSync(embeddedAssetsFile, JSON.stringify(assets)); +} + +function typecheck(): void { + execFileSync('tsc', ['--noEmit', '-p', path.join(packageRoot, 'tsconfig.frontend.json')], { + cwd: packageRoot, + stdio: 'inherit', + }); +} + +main().catch((err) => { + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); +}); diff --git a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx new file mode 100644 index 000000000..ee3809586 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx @@ -0,0 +1,40 @@ +import Box from '@cloudscape-design/components/box'; +import Container from '@cloudscape-design/components/container'; +import ContentLayout from '@cloudscape-design/components/content-layout'; +import Grid from '@cloudscape-design/components/grid'; +import Header from '@cloudscape-design/components/header'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import * as React from 'react'; +import { FilePane } from './components/FilePane'; + +/** Web explorer shell: Resource Tree (left), two file panes, Violations (bottom). Tree/violations are placeholders until the cloud-assembly reader is wired in. */ +export function App(): JSX.Element { + return ( + + CDK Web Explorer + + } + > + + + Resource Tree}> + + Construct tree appears here once the cloud-assembly reader is wired in. + + + + + + + + Violations}> + + Policy-validation violations appear here once the cloud-assembly reader is wired in. + + + + + ); +} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/api.ts b/packages/@aws-cdk/cdk-explorer/frontend/api.ts new file mode 100644 index 000000000..3e1c9ed90 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/api.ts @@ -0,0 +1,17 @@ +import type { DirEntry, FilesResponse, FileResponse } from '../lib/web/protocol'; + +export type { DirEntry, FilesResponse, FileResponse }; + +async function getJson(url: string): Promise { + const res = await fetch(url); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error((body as { error?: string }).error ?? `request failed: ${res.status}`); + } + return res.json() as Promise; +} + +export const api = { + listFiles: (dir = ''): Promise => getJson(`/api/files?dir=${encodeURIComponent(dir)}`), + readFile: (filePath: string): Promise => getJson(`/api/file?path=${encodeURIComponent(filePath)}`), +}; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx new file mode 100644 index 000000000..b0a97ac49 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx @@ -0,0 +1,115 @@ +import Box from '@cloudscape-design/components/box'; +import Button from '@cloudscape-design/components/button'; +import Container from '@cloudscape-design/components/container'; +import Header from '@cloudscape-design/components/header'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import * as React from 'react'; +import { api, type DirEntry } from '../api'; + +interface FilePaneProps { + /** Heading shown in the pane header (e.g. "file 1"). */ + readonly title: string; +} + +/** + * A self-contained code pane with a server-backed file picker. Browses + * directories under the app root via /api/files and shows file contents via + * /api/file. Rendered once per code pane (center and right). + */ +export function FilePane({ title }: FilePaneProps): JSX.Element { + const [picking, setPicking] = React.useState(false); + const [dir, setDir] = React.useState(''); + const [entries, setEntries] = React.useState([]); + const [filePath, setFilePath] = React.useState(); + const [content, setContent] = React.useState(''); + const [error, setError] = React.useState(); + + const browse = React.useCallback(async (nextDir: string) => { + try { + const res = await api.listFiles(nextDir); + setDir(res.dir); + setEntries(res.entries); + setError(undefined); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + }, []); + + const openPicker = React.useCallback(() => { + setPicking(true); + void browse(''); + }, [browse]); + + const choose = React.useCallback(async (entry: DirEntry) => { + if (entry.type === 'dir') { + void browse(entry.path); + return; + } + try { + const res = await api.readFile(entry.path); + setFilePath(res.path); + setContent(res.content); + setPicking(false); + setError(undefined); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + }, [browse]); + + return ( + Open file…}> + {filePath ?? title} + + } + > + + {error && {error}} + {picking ? ( + browse(parentOf(dir))} /> + ) : ( +
{content || 'No file selected.'}
+ )} +
+
+ ); +} + +const CODE_STYLE: React.CSSProperties = { + margin: 0, + maxHeight: '60vh', + overflow: 'auto', + fontFamily: 'Monaco, Menlo, "Courier New", monospace', + fontSize: '12px', + whiteSpace: 'pre', +}; + +function FileBrowser(props: { + readonly dir: string; + readonly entries: readonly DirEntry[]; + readonly onChoose: (entry: DirEntry) => void; + readonly onUp: () => void; +}): JSX.Element { + return ( + + /{props.dir} + {props.dir !== '' && } + {props.entries.map((entry) => ( + + ))} + + ); +} + +function parentOf(dir: string): string { + const idx = dir.lastIndexOf('/'); + return idx === -1 ? '' : dir.slice(0, idx); +} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/index.html b/packages/@aws-cdk/cdk-explorer/frontend/index.html new file mode 100644 index 000000000..8aacc9e5a --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + CDK Web Explorer + + + +
+ + + diff --git a/packages/@aws-cdk/cdk-explorer/frontend/index.tsx b/packages/@aws-cdk/cdk-explorer/frontend/index.tsx new file mode 100644 index 000000000..8f261abc4 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/index.tsx @@ -0,0 +1,17 @@ +import '@cloudscape-design/global-styles/index.css'; +import { applyMode, Mode } from '@cloudscape-design/global-styles'; +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; + +applyMode(Mode.Light); + +const container = document.getElementById('root'); +if (!container) { + throw new Error('CDK Explorer: #root element not found'); +} +createRoot(container).render( + + + , +); diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts new file mode 100644 index 000000000..a4f512935 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts @@ -0,0 +1,18 @@ +/** HTTP contract shared between the web server and the SPA. Types only. */ + +export interface DirEntry { + readonly name: string; + /** Path relative to the app directory, usable as the next `dir`/`path` value. POSIX separators. */ + readonly path: string; + readonly type: 'dir' | 'file'; +} + +export interface FilesResponse { + readonly dir: string; + readonly entries: readonly DirEntry[]; +} + +export interface FileResponse { + readonly path: string; + readonly content: string; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts new file mode 100644 index 000000000..433400733 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts @@ -0,0 +1,106 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { type Router, type Express } from 'express'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import express = require('express'); +import type { DirEntry } from './protocol'; +import { resolveWithinRoot } from './safe-path'; + +/** Largest file the viewer will return inline, to avoid streaming huge artifacts. */ +const MAX_FILE_BYTES = 2 * 1024 * 1024; + +export interface ApiOptions { + /** Root of the CDK app; all file listing/reading is confined to this directory. */ + readonly appDir: string; +} + +export function createApiRouter(options: ApiOptions): Router { + const appDir = canonicalDir(options.appDir); + const router = express.Router(); + + router.get('/health', (_req, res) => { + res.json({ status: 'ok' }); + }); + + router.get('/files', (req, res) => { + const dir = typeof req.query.dir === 'string' ? req.query.dir : ''; + const resolved = resolveWithinRoot(appDir, dir); + if (!resolved) { + return res.status(403).json({ error: 'path escapes application directory' }); + } + let stat: fs.Stats; + try { + stat = fs.statSync(resolved); + } catch { + return res.status(404).json({ error: 'directory not found' }); + } + if (!stat.isDirectory()) { + return res.status(400).json({ error: 'not a directory' }); + } + return res.json({ dir: toPosix(path.relative(appDir, resolved)), entries: listDir(appDir, resolved) }); + }); + + router.get('/file', (req, res) => { + const requested = typeof req.query.path === 'string' ? req.query.path : ''; + if (!requested) { + return res.status(400).json({ error: 'path query parameter is required' }); + } + const resolved = resolveWithinRoot(appDir, requested); + if (!resolved) { + return res.status(403).json({ error: 'path escapes application directory' }); + } + let stat: fs.Stats; + try { + stat = fs.statSync(resolved); + } catch { + return res.status(404).json({ error: 'file not found' }); + } + if (!stat.isFile()) { + return res.status(400).json({ error: 'not a file' }); + } + if (stat.size > MAX_FILE_BYTES) { + return res.status(413).json({ error: `file exceeds ${MAX_FILE_BYTES} byte limit` }); + } + const buffer = fs.readFileSync(resolved); + if (isBinary(buffer)) { + return res.status(415).json({ error: 'binary file cannot be displayed' }); + } + return res.json({ path: toPosix(path.relative(appDir, resolved)), content: buffer.toString('utf-8') }); + }); + + return router; +} + +export function registerApi(app: Express, options: ApiOptions): void { + app.use('/api', createApiRouter(options)); +} + +function listDir(appDir: string, dir: string): DirEntry[] { + return fs.readdirSync(dir, { withFileTypes: true }) + .map((entry): DirEntry => ({ + name: entry.name, + path: toPosix(path.relative(appDir, path.join(dir, entry.name))), + type: entry.isDirectory() ? 'dir' : 'file', + })) + .sort(byTypeThenName); +} + +function byTypeThenName(a: DirEntry, b: DirEntry): number { + if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; + return a.name.localeCompare(b.name); +} + +/** Normalize OS separators to '/' so the API contract is stable across platforms. */ +function toPosix(p: string): string { + return p.split(path.sep).join('/'); +} + +/** Canonical app root: realpath so relative paths match resolveWithinRoot's realpathed output. */ +function canonicalDir(dir: string): string { + return fs.realpathSync(path.resolve(dir)); +} + +/** A NUL byte in the first chunk reliably indicates non-text content. */ +function isBinary(buffer: Buffer): boolean { + return buffer.subarray(0, 8000).includes(0); +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/safe-path.ts b/packages/@aws-cdk/cdk-explorer/lib/web/safe-path.ts new file mode 100644 index 000000000..5842e3a3d --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/safe-path.ts @@ -0,0 +1,40 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Resolve a client-supplied, root-relative path to an absolute path guaranteed + * to stay inside `root`. Returns `undefined` when the request escapes the root + * (via `..` or a symlink pointing outside), which callers must treat as a 403. + * + * `root` is expected to be absolute. A leading `/` on `requested` is stripped so + * absolute-looking inputs cannot jump to the filesystem root. + */ +export function resolveWithinRoot(root: string, requested: string): string | undefined { + const realRoot = realOrSelf(path.resolve(root)); + const relative = requested.replace(/^[/\\]+/, ''); + const resolved = path.resolve(realRoot, relative); + + if (!isInside(realRoot, resolved)) { + return undefined; + } + // Follow symlinks on the target: an existing file reached through a symlinked + // directory must still land inside the root. A non-existent target resolves to + // itself and stays caught by the lexical check above (and the caller 404s it). + if (!isInside(realRoot, realOrSelf(resolved))) { + return undefined; + } + return resolved; +} + +function isInside(root: string, candidate: string): boolean { + return candidate === root || candidate.startsWith(root + path.sep); +} + +/** Real path with symlinks resolved, or the input unchanged if it does not exist. */ +function realOrSelf(p: string): string { + try { + return fs.realpathSync(p); + } catch { + return p; + } +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts index 338a77106..56021d169 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts @@ -1,6 +1,8 @@ import * as http from 'http'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); +import { registerApi } from './routes'; +import { indexHtml, webAsset } from './web-assets'; export const DEFAULT_PORT = 4200; const MAX_PORT_ATTEMPTS = 100; @@ -8,6 +10,11 @@ const MAX_PORT_ATTEMPTS = 100; export interface WebServerOptions { readonly port?: number; readonly host?: string; + /** + * Root of the CDK app. File listing/reading is confined here. Defaults to + * `process.cwd()`. + */ + readonly appDir?: string; } export interface WebServer { @@ -25,11 +32,25 @@ export interface WebServer { */ export async function startWebServer(options: WebServerOptions = {}): Promise { const host = options.host ?? '127.0.0.1'; + const appDir = options.appDir ?? process.cwd(); const app = express(); - app.get('/api/health', (_req, res) => { - res.json({ status: 'ok' }); + registerApi(app, { appDir }); + + // Unknown /api routes must return JSON 404, not fall through to the SPA. + app.use('/api', (_req, res) => res.status(404).json({ error: 'unknown endpoint' })); + + // Serve the SPA from the embedded bundle (survives CLI bundling). Named assets + // by path; any other GET falls back to index.html for client-side routing. + app.get('/:asset', (req, res, next) => { + const asset = webAsset(req.params.asset); + if (!asset) return next(); + return res.type(asset.contentType).send(asset.body); + }); + app.get('*', (_req, res) => { + const index = indexHtml(); + res.type(index.contentType).send(index.body); }); const server = http.createServer(app); diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/web-assets.ts b/packages/@aws-cdk/cdk-explorer/lib/web/web-assets.ts new file mode 100644 index 000000000..b0815e880 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/web-assets.ts @@ -0,0 +1,34 @@ +/** + * Serves the web explorer SPA from memory. The bytes come from + * web-assets.generated.json (written by build-tools/bundle-frontend.ts); the + * static `require` is what pulls them into the published CLI bundle. The build + * always generates this file, so its absence is a build error, not a runtime + * condition we handle (same convention as the CLI's build-info.json). + */ +export interface WebAsset { + readonly contentType: string; + readonly body: string; +} + +const CONTENT_TYPES: Record = { + 'index.html': 'text/html; charset=utf-8', + 'bundle.js': 'text/javascript; charset=utf-8', + 'bundle.css': 'text/css; charset=utf-8', +}; + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const raw = require('./web-assets.generated.json') as Record; + +const WEB_ASSETS: Record = Object.fromEntries( + Object.entries(raw).map(([name, body]) => [name, { contentType: CONTENT_TYPES[name], body }]), +); + +/** The SPA entry document. */ +export function indexHtml(): WebAsset { + return WEB_ASSETS['index.html']; +} + +/** A named SPA asset (e.g. "bundle.js"), or undefined if not part of the build. */ +export function webAsset(name: string): WebAsset | undefined { + return WEB_ASSETS[name]; +} diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index 40ac05126..981fc453b 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -31,14 +31,20 @@ }, "devDependencies": { "@cdklabs/eslint-plugin": "^2.0.8", + "@cloudscape-design/components": "^3", + "@cloudscape-design/global-styles": "^1", "@stylistic/eslint-plugin": "^3", "@types/convert-source-map": "^2", "@types/express": "^4", "@types/jest": "^29.5.14", "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@types/supertest": "^6", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", "constructs": "^10.0.0", + "esbuild": "^0.28.1", "eslint": "^9", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^4.4.5", @@ -51,7 +57,11 @@ "nx": "^22.7.5", "prettier": "^2.8", "projen": "^0.99.70", + "react": "^18", + "react-dom": "^18", + "supertest": "^6", "ts-jest": "^29.4.11", + "tsx": "^4.22.4", "typescript": "5.9", "vscode-languageserver-protocol": "^3" }, diff --git a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts new file mode 100644 index 000000000..9028bfbe0 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts @@ -0,0 +1,113 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import express = require('express'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +import request = require('supertest'); +import { createApiRouter } from '../../lib/web/routes'; + +let appDir: string; +let app: express.Express; + +beforeEach(() => { + appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-routes-')); + fs.writeFileSync(path.join(appDir, 'app.ts'), 'export const x = 1;\n'); + fs.mkdirSync(path.join(appDir, 'lib')); + fs.writeFileSync(path.join(appDir, 'lib', 'stack.ts'), 'class Stack {}\n'); + + app = express(); + app.use('/api', createApiRouter({ appDir })); +}); + +afterEach(() => { + fs.rmSync(appDir, { recursive: true, force: true }); +}); + +describe('GET /api/health', () => { + test('returns ok', async () => { + const res = await request(app).get('/api/health'); + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'ok' }); + }); +}); + +describe('GET /api/files', () => { + test('lists the root directory with directories first', async () => { + const res = await request(app).get('/api/files'); + expect(res.status).toBe(200); + expect(res.body.dir).toBe(''); + expect(res.body.entries).toEqual([ + { name: 'lib', path: 'lib', type: 'dir' }, + { name: 'app.ts', path: 'app.ts', type: 'file' }, + ]); + }); + + test('lists a subdirectory', async () => { + const res = await request(app).get('/api/files').query({ dir: 'lib' }); + expect(res.status).toBe(200); + expect(res.body.entries).toEqual([{ name: 'stack.ts', path: path.join('lib', 'stack.ts'), type: 'file' }]); + }); + + test('rejects traversal outside the app directory with 403', async () => { + const res = await request(app).get('/api/files').query({ dir: '../..' }); + expect(res.status).toBe(403); + }); + + test('returns 404 for a missing directory', async () => { + const res = await request(app).get('/api/files').query({ dir: 'nope' }); + expect(res.status).toBe(404); + }); +}); + +describe('GET /api/file', () => { + test('returns file content', async () => { + const res = await request(app).get('/api/file').query({ path: 'app.ts' }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ path: 'app.ts', content: 'export const x = 1;\n' }); + }); + + test('requires a path parameter', async () => { + const res = await request(app).get('/api/file'); + expect(res.status).toBe(400); + }); + + test('rejects traversal with 403', async () => { + const res = await request(app).get('/api/file').query({ path: '../../../etc/passwd' }); + expect(res.status).toBe(403); + }); + + test('returns 404 for a missing file', async () => { + const res = await request(app).get('/api/file').query({ path: 'missing.ts' }); + expect(res.status).toBe(404); + }); + + test('returns 400 when the path is a directory', async () => { + const res = await request(app).get('/api/file').query({ path: 'lib' }); + expect(res.status).toBe(400); + }); + + test('rejects binary files with 415', async () => { + fs.writeFileSync(path.join(appDir, 'bin.dat'), Buffer.from([0x00, 0x01, 0x02, 0x03])); + const res = await request(app).get('/api/file').query({ path: 'bin.dat' }); + expect(res.status).toBe(415); + }); +}); + +describe('symlink containment', () => { + test('rejects a symlink inside appDir that points outside with 403', async () => { + const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-outside-')); + fs.writeFileSync(path.join(outside, 'secret.txt'), 'top secret'); + try { + try { + fs.symlinkSync(path.join(outside, 'secret.txt'), path.join(appDir, 'link.txt')); + } catch { + return; // symlinks not permitted in this environment; skip + } + const res = await request(app).get('/api/file').query({ path: 'link.txt' }); + expect(res.status).toBe(403); + } finally { + fs.rmSync(outside, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/safe-path.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/safe-path.test.ts new file mode 100644 index 000000000..5c65eb6a7 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/safe-path.test.ts @@ -0,0 +1,71 @@ +import * as path from 'path'; +import { resolveWithinRoot } from '../../lib/web/safe-path'; + +const root = path.resolve('/srv/app'); + +describe('resolveWithinRoot', () => { + test('resolves a simple relative path inside the root', () => { + expect(resolveWithinRoot(root, 'lib/stack.ts')).toBe(path.join(root, 'lib/stack.ts')); + }); + + test('treats an empty path as the root itself', () => { + expect(resolveWithinRoot(root, '')).toBe(root); + }); + + test('strips a leading slash instead of jumping to filesystem root', () => { + expect(resolveWithinRoot(root, '/lib/stack.ts')).toBe(path.join(root, 'lib/stack.ts')); + }); + + test('rejects traversal above the root', () => { + expect(resolveWithinRoot(root, '../secrets')).toBeUndefined(); + expect(resolveWithinRoot(root, '../../etc/passwd')).toBeUndefined(); + expect(resolveWithinRoot(root, 'lib/../../escape')).toBeUndefined(); + }); + + test('allows traversal that stays within the root', () => { + expect(resolveWithinRoot(root, 'lib/../bin/cdk.ts')).toBe(path.join(root, 'bin/cdk.ts')); + }); + + test('does not treat a sibling directory with a shared prefix as inside', () => { + expect(resolveWithinRoot(root, '../app-secrets/file')).toBeUndefined(); + }); +}); + +// Reload safe-path with `path` swapped for its +// win32 flavor to test real Windows behavior on the Linux runner. +describe('resolveWithinRoot (win32 semantics)', () => { + const winRoot = 'C:\\srv\\app'; + let resolveWin: typeof resolveWithinRoot; + + beforeAll(() => { + jest.resetModules(); + jest.doMock('path', () => jest.requireActual('path').win32); + // eslint-disable-next-line @typescript-eslint/no-require-imports + resolveWin = require('../../lib/web/safe-path').resolveWithinRoot; + }); + + afterAll(() => { + jest.dontMock('path'); + jest.resetModules(); + }); + + test('resolves forward-slash client paths under the root', () => { + expect(resolveWin(winRoot, 'lib/stack.ts')).toBe('C:\\srv\\app\\lib\\stack.ts'); + }); + + test('strips a leading backslash instead of jumping to the drive root', () => { + expect(resolveWin(winRoot, '\\lib\\stack.ts')).toBe('C:\\srv\\app\\lib\\stack.ts'); + }); + + test('rejects backslash traversal above the root', () => { + expect(resolveWin(winRoot, '..\\secrets')).toBeUndefined(); + }); + + test('rejects an absolute path on a different drive', () => { + expect(resolveWin(winRoot, 'D:\\evil')).toBeUndefined(); + }); + + test('does not treat a sibling drive-prefixed directory as inside', () => { + expect(resolveWin(winRoot, '..\\app-secrets\\file')).toBeUndefined(); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts index c73549ae2..ce05239ab 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts @@ -56,4 +56,12 @@ describe('Web Server', () => { await server.stop(); await server.stop(); }); + + test('unknown /api route returns a JSON 404 rather than the SPA', async () => { + server = await startWebServer(); + const res = await fetch(`${server.url}/api/does-not-exist`); + expect(res.status).toBe(404); + expect(res.headers.get('content-type')).toMatch(/application\/json/); + expect((await res.json()).error).toBeDefined(); + }); }); diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json index ac3e8501d..ed785310b 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json @@ -28,7 +28,10 @@ "convert-source-map", "express", "jest", - "node" + "node", + "react-dom", + "react", + "supertest" ], "incremental": true, "skipLibCheck": true, @@ -38,10 +41,12 @@ }, "include": [ "lib/**/*.ts", - "test/**/*.ts" + "test/**/*.ts", + "build-tools/**/*.ts" ], "exclude": [ - "node_modules" + "node_modules", + "frontend" ], "references": [ { diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.frontend.json b/packages/@aws-cdk/cdk-explorer/tsconfig.frontend.json new file mode 100644 index 000000000..d17fdb341 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.frontend.json @@ -0,0 +1,18 @@ +{ + "//": "TypeScript config for the web explorer SPA. Bundled by esbuild, not by the package's CommonJS/Node tsc build. Used for editor IntelliSense and the bundler's typecheck pass.", + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "types": ["react", "react-dom"] + }, + "include": ["frontend"] +} diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.json b/packages/@aws-cdk/cdk-explorer/tsconfig.json index 7324429b1..54247ff97 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.json @@ -30,7 +30,10 @@ "convert-source-map", "express", "jest", - "node" + "node", + "react-dom", + "react", + "supertest" ], "incremental": true, "skipLibCheck": true, @@ -40,7 +43,9 @@ "include": [ "lib/**/*.ts" ], - "exclude": [], + "exclude": [ + "frontend" + ], "references": [ { "path": "../cloud-assembly-schema" diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index 0a3a5dc06..3cd33d7f3 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -16039,6 +16039,77 @@ Apache License of your accepting any such warranty or additional liability. +---------------- + +** @jridgewell/resolve-uri@3.1.2 - https://www.npmjs.com/package/@jridgewell/resolve-uri/v/3.1.2 | MIT +Copyright 2019 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------- + +** @jridgewell/sourcemap-codec@1.5.5 - https://www.npmjs.com/package/@jridgewell/sourcemap-codec/v/1.5.5 | MIT +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** @jridgewell/trace-mapping@0.3.31 - https://www.npmjs.com/package/@jridgewell/trace-mapping/v/0.3.31 | MIT +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** @nodelib/fs.scandir@2.1.5 - https://www.npmjs.com/package/@nodelib/fs.scandir/v/2.1.5 | MIT @@ -29002,6 +29073,34 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** convert-source-map@2.0.0 - https://www.npmjs.com/package/convert-source-map/v/2.0.0 | MIT +Copyright 2013 Thorsten Lorenz. +All rights reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** cookie-signature@1.0.7 - https://www.npmjs.com/package/cookie-signature/v/1.0.7 | MIT diff --git a/yarn.lock b/yarn.lock index 787d55ea9..78a67465e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -179,16 +179,22 @@ __metadata: "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/toolkit-lib": "npm:^0.0.0" "@cdklabs/eslint-plugin": "npm:^2.0.8" + "@cloudscape-design/components": "npm:^3" + "@cloudscape-design/global-styles": "npm:^1" "@jridgewell/trace-mapping": "npm:^0.3" "@stylistic/eslint-plugin": "npm:^3" "@types/convert-source-map": "npm:^2" "@types/express": "npm:^4" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^20" + "@types/react": "npm:^18" + "@types/react-dom": "npm:^18" + "@types/supertest": "npm:^6" "@typescript-eslint/eslint-plugin": "npm:^8" "@typescript-eslint/parser": "npm:^8" constructs: "npm:^10.0.0" convert-source-map: "npm:^2" + esbuild: "npm:^0.28.1" eslint: "npm:^9" eslint-config-prettier: "npm:^10.1.8" eslint-import-resolver-typescript: "npm:^4.4.5" @@ -202,7 +208,11 @@ __metadata: nx: "npm:^22.7.5" prettier: "npm:^2.8" projen: "npm:^0.99.70" + react: "npm:^18" + react-dom: "npm:^18" + supertest: "npm:^6" ts-jest: "npm:^29.4.11" + tsx: "npm:^4.22.4" typescript: "npm:5.9" vscode-jsonrpc: "npm:^8" vscode-languageserver: "npm:^9" @@ -3835,6 +3845,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.0, @babel/runtime@npm:^7.8.7": + version: 7.29.7 + resolution: "@babel/runtime@npm:7.29.7" + checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e + languageName: node + linkType: hard + "@babel/template@npm:^7.28.6, @babel/template@npm:^7.3.3": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" @@ -3922,6 +3939,83 @@ __metadata: languageName: node linkType: hard +"@cloudscape-design/collection-hooks@npm:^1.0.0": + version: 1.0.98 + resolution: "@cloudscape-design/collection-hooks@npm:1.0.98" + dependencies: + "@cloudscape-design/component-toolkit": "npm:^1.0.0-beta" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/74bfe864a896f2f870fbeec8be915b6f50a109c6539cb98f18dbb8e29c17163c733a92d6810d053a0c489f7988c3ea568a9a4622d051bf69b315f7cba72a04db + languageName: node + linkType: hard + +"@cloudscape-design/component-toolkit@npm:^1.0.0-beta": + version: 1.0.0-beta.165 + resolution: "@cloudscape-design/component-toolkit@npm:1.0.0-beta.165" + dependencies: + tslib: "npm:^2.3.1" + weekstart: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/e8cc367653f36d48a1dd2c261ec46900df5e12246f615f6ae16824d37913001377004f030d2cc8f14916bb134111693e42169f133615620d430fe3c766f48a75 + languageName: node + linkType: hard + +"@cloudscape-design/components@npm:^3": + version: 3.0.1310 + resolution: "@cloudscape-design/components@npm:3.0.1310" + dependencies: + "@cloudscape-design/collection-hooks": "npm:^1.0.0" + "@cloudscape-design/component-toolkit": "npm:^1.0.0-beta" + "@cloudscape-design/test-utils-core": "npm:^1.0.0" + "@cloudscape-design/theming-runtime": "npm:^1.0.0" + "@dnd-kit/core": "npm:^6.0.8" + "@dnd-kit/sortable": "npm:^7.0.2" + "@dnd-kit/utilities": "npm:^3.2.1" + ace-builds: "npm:^1.34.0" + clsx: "npm:^1.1.0" + d3-shape: "npm:^1.3.7" + date-fns: "npm:^2.25.0" + intl-messageformat: "npm:^10.3.1" + mnth: "npm:^2.0.0" + react-is: "npm:^18.2.0" + react-transition-group: "npm:^4.4.2" + tslib: "npm:^2.4.0" + weekstart: "npm:^1.1.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/f52cec1103c77a9c478fa00de85026cb226840d20831c306d41129a0446566032b79c15f525aa30f0826828107e0410b4f031b5d8347954b1edee60e9f0af62d + languageName: node + linkType: hard + +"@cloudscape-design/global-styles@npm:^1": + version: 1.0.60 + resolution: "@cloudscape-design/global-styles@npm:1.0.60" + checksum: 10c0/51b99ef21c10ca73c91571550f1e886fa73dd6b12339d6f6a2cdd43a359ebabbf048e93a611ca41ac069a528d35a1ef9570278cd732f2f649d574f945d4832e3 + languageName: node + linkType: hard + +"@cloudscape-design/test-utils-core@npm:^1.0.0": + version: 1.0.83 + resolution: "@cloudscape-design/test-utils-core@npm:1.0.83" + dependencies: + css-selector-tokenizer: "npm:^0.8.0" + css.escape: "npm:^1.5.1" + checksum: 10c0/2d99192bc89699187fc1b6c96c7eea305f530cf47082c6d5b37eed1258850a9a4d792845dcd77a2b24362fc823f2b33c7f3da04c218fabf7e2c221e97d0569b1 + languageName: node + linkType: hard + +"@cloudscape-design/theming-runtime@npm:^1.0.0": + version: 1.0.115 + resolution: "@cloudscape-design/theming-runtime@npm:1.0.115" + dependencies: + "@material/material-color-utilities": "npm:^0.3.0" + tslib: "npm:^2.4.0" + checksum: 10c0/509c9369f7eef017047fe08206259cd386720e438d1665293dc755773fb9ab151041480c859e9d23f8b8b745ca27dd02e35efa1ab41d33fe46832c476d1365b4 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -3931,6 +4025,55 @@ __metadata: languageName: node linkType: hard +"@dnd-kit/accessibility@npm:^3.1.1": + version: 3.1.1 + resolution: "@dnd-kit/accessibility@npm:3.1.1" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/be0bf41716dc58f9386bc36906ec1ce72b7b42b6d1d0e631d347afe9bd8714a829bd6f58a346dd089b1519e93918ae2f94497411a61a4f5e4d9247c6cfd1fef8 + languageName: node + linkType: hard + +"@dnd-kit/core@npm:^6.0.8": + version: 6.3.1 + resolution: "@dnd-kit/core@npm:6.3.1" + dependencies: + "@dnd-kit/accessibility": "npm:^3.1.1" + "@dnd-kit/utilities": "npm:^3.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10c0/196db95d81096d9dc248983533eab91ba83591770fa5c894b1ac776f42af0d99522b3fd5bb3923411470e4733fcfa103e6ee17adc17b9b7eb54c7fbec5ff7c52 + languageName: node + linkType: hard + +"@dnd-kit/sortable@npm:^7.0.2": + version: 7.0.2 + resolution: "@dnd-kit/sortable@npm:7.0.2" + dependencies: + "@dnd-kit/utilities": "npm:^3.2.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@dnd-kit/core": ^6.0.7 + react: ">=16.8.0" + checksum: 10c0/06aeb113eeeb470bb2443bf1c48d597157bb3a1caa9740e60c2fa73a3076e753cd083a2d381f0556bd7e9873e851a49ce8ea14796ac02e2d796eabea4e27196d + languageName: node + linkType: hard + +"@dnd-kit/utilities@npm:^3.2.0, @dnd-kit/utilities@npm:^3.2.1, @dnd-kit/utilities@npm:^3.2.2": + version: 3.2.2 + resolution: "@dnd-kit/utilities@npm:3.2.2" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10c0/9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0 + languageName: node + linkType: hard + "@emnapi/core@npm:1.4.5": version: 1.4.5 resolution: "@emnapi/core@npm:1.4.5" @@ -4014,6 +4157,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/aix-ppc64@npm:0.28.1" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/android-arm64@npm:0.28.0" @@ -4021,6 +4171,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm64@npm:0.28.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/android-arm@npm:0.28.0" @@ -4028,6 +4185,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm@npm:0.28.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/android-x64@npm:0.28.0" @@ -4035,6 +4199,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-x64@npm:0.28.1" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/darwin-arm64@npm:0.28.0" @@ -4042,6 +4213,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-arm64@npm:0.28.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/darwin-x64@npm:0.28.0" @@ -4049,6 +4227,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-x64@npm:0.28.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/freebsd-arm64@npm:0.28.0" @@ -4056,6 +4241,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-arm64@npm:0.28.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/freebsd-x64@npm:0.28.0" @@ -4063,6 +4255,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-x64@npm:0.28.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-arm64@npm:0.28.0" @@ -4070,6 +4269,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm64@npm:0.28.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-arm@npm:0.28.0" @@ -4077,6 +4283,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm@npm:0.28.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-ia32@npm:0.28.0" @@ -4084,6 +4297,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ia32@npm:0.28.1" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-loong64@npm:0.28.0" @@ -4091,6 +4311,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-loong64@npm:0.28.1" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-mips64el@npm:0.28.0" @@ -4098,6 +4325,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-mips64el@npm:0.28.1" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-ppc64@npm:0.28.0" @@ -4105,6 +4339,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ppc64@npm:0.28.1" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-riscv64@npm:0.28.0" @@ -4112,6 +4353,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-riscv64@npm:0.28.1" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-s390x@npm:0.28.0" @@ -4119,6 +4367,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-s390x@npm:0.28.1" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/linux-x64@npm:0.28.0" @@ -4126,6 +4381,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-x64@npm:0.28.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/netbsd-arm64@npm:0.28.0" @@ -4133,6 +4395,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-arm64@npm:0.28.1" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/netbsd-x64@npm:0.28.0" @@ -4140,6 +4409,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-x64@npm:0.28.1" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/openbsd-arm64@npm:0.28.0" @@ -4147,6 +4423,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-arm64@npm:0.28.1" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/openbsd-x64@npm:0.28.0" @@ -4154,6 +4437,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-x64@npm:0.28.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openharmony-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/openharmony-arm64@npm:0.28.0" @@ -4161,6 +4451,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openharmony-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openharmony-arm64@npm:0.28.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/sunos-x64@npm:0.28.0" @@ -4168,6 +4465,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/sunos-x64@npm:0.28.1" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/win32-arm64@npm:0.28.0" @@ -4175,6 +4479,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-arm64@npm:0.28.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/win32-ia32@npm:0.28.0" @@ -4182,6 +4493,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-ia32@npm:0.28.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.28.0": version: 0.28.0 resolution: "@esbuild/win32-x64@npm:0.28.0" @@ -4189,6 +4507,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-x64@npm:0.28.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" @@ -4277,6 +4602,57 @@ __metadata: languageName: node linkType: hard +"@formatjs/ecma402-abstract@npm:2.3.6": + version: 2.3.6 + resolution: "@formatjs/ecma402-abstract@npm:2.3.6" + dependencies: + "@formatjs/fast-memoize": "npm:2.2.7" + "@formatjs/intl-localematcher": "npm:0.6.2" + decimal.js: "npm:^10.4.3" + tslib: "npm:^2.8.0" + checksum: 10c0/63be2a73d3168bf45ab5d50db58376e852db5652d89511ae6e44f1fa03ad96ebbfe9b06a1dfaa743db06e40eb7f33bd77530b9388289855cca79a0e3fc29eacf + languageName: node + linkType: hard + +"@formatjs/fast-memoize@npm:2.2.7": + version: 2.2.7 + resolution: "@formatjs/fast-memoize@npm:2.2.7" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10c0/f5eabb0e4ab7162297df8252b4cfde194b23248120d9df267592eae2be2d2f7c4f670b5a70523d91b4ecdc35d40e65823bb8eeba8dd79fbf8601a972bf3b8866 + languageName: node + linkType: hard + +"@formatjs/icu-messageformat-parser@npm:2.11.4": + version: 2.11.4 + resolution: "@formatjs/icu-messageformat-parser@npm:2.11.4" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.3.6" + "@formatjs/icu-skeleton-parser": "npm:1.8.16" + tslib: "npm:^2.8.0" + checksum: 10c0/3ea9e9dae18282881d19a5f88107b6013f514ec8675684ed2c04bee2a174032377858937243e3bd9c9263a470988a3773a53bf8d208a34a78e7843ce66f87f3b + languageName: node + linkType: hard + +"@formatjs/icu-skeleton-parser@npm:1.8.16": + version: 1.8.16 + resolution: "@formatjs/icu-skeleton-parser@npm:1.8.16" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.3.6" + tslib: "npm:^2.8.0" + checksum: 10c0/6fa1586dc11c925cd8d17e927cc635d238c969a6b7e97282a924376f78622fc25336c407589d19796fb6f8124a0e7765f99ecdb1aac014edcfbe852e7c3d87f3 + languageName: node + linkType: hard + +"@formatjs/intl-localematcher@npm:0.6.2": + version: 0.6.2 + resolution: "@formatjs/intl-localematcher@npm:0.6.2" + dependencies: + tslib: "npm:^2.8.0" + checksum: 10c0/22a17a4c67160b6c9f52667914acfb7b79cd6d80630d4ac6d4599ce447cb89d2a64f7d58fa35c3145ddb37fef893f0a45b9a55e663a4eb1f2ae8b10a89fac235 + languageName: node + linkType: hard + "@gar/promise-retry@npm:^1.0.0, @gar/promise-retry@npm:^1.0.2": version: 1.0.3 resolution: "@gar/promise-retry@npm:1.0.3" @@ -4866,6 +5242,13 @@ __metadata: languageName: node linkType: hard +"@material/material-color-utilities@npm:^0.3.0": + version: 0.3.0 + resolution: "@material/material-color-utilities@npm:0.3.0" + checksum: 10c0/3bef025428b893f2acc9e9e2bd186363a60b7c0836fe43c78222e29fe67dc579618e844f0661a20657ed9f7fd8b94fd43a2961892894d0b6a2ba5264ce2673f8 + languageName: node + linkType: hard + "@microsoft/api-extractor-model@npm:7.33.8": version: 7.33.8 resolution: "@microsoft/api-extractor-model@npm:7.33.8" @@ -4941,6 +5324,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^1.1.5": + version: 1.8.0 + resolution: "@noble/hashes@npm:1.8.0" + checksum: 10c0/06a0b52c81a6fa7f04d67762e08b2c476a00285858150caeaaff4037356dd5e119f45b2a530f638b77a5eeca013168ec1b655db41bae3236cb2e9d511484fc77 + languageName: node + linkType: hard + "@nodable/entities@npm:2.1.0, @nodable/entities@npm:^2.1.0": version: 2.1.0 resolution: "@nodable/entities@npm:2.1.0" @@ -5409,6 +5799,15 @@ __metadata: languageName: node linkType: hard +"@paralleldrive/cuid2@npm:^2.2.2": + version: 2.3.1 + resolution: "@paralleldrive/cuid2@npm:2.3.1" + dependencies: + "@noble/hashes": "npm:^1.1.5" + checksum: 10c0/6576b73de49d826b0f33cbab88424dec1f6fa454a9e59a7b621f78c2cfdd2e59d7f48175826d698940a717f45eeb5e87a508583a7316e608f6a05a861a40c129 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -6685,6 +7084,13 @@ __metadata: languageName: node linkType: hard +"@types/cookiejar@npm:^2.1.5": + version: 2.1.5 + resolution: "@types/cookiejar@npm:2.1.5" + checksum: 10c0/af38c3d84aebb3ccc6e46fb6afeeaac80fb26e63a487dd4db5a8b87e6ad3d4b845ba1116b2ae90d6f886290a36200fa433d8b1f6fe19c47da6b81872ce9a2764 + languageName: node + linkType: hard + "@types/cors@npm:^2.8.6": version: 2.8.19 resolution: "@types/cors@npm:2.8.19" @@ -6851,6 +7257,13 @@ __metadata: languageName: node linkType: hard +"@types/methods@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/methods@npm:1.1.4" + checksum: 10c0/a78534d79c300718298bfff92facd07bf38429c66191f640c1db4c9cff1e36f819304298a96f7536b6512bfc398e5c3e6b831405e138cd774b88ad7be78d682a + languageName: node + linkType: hard + "@types/mime@npm:^1": version: 1.3.5 resolution: "@types/mime@npm:1.3.5" @@ -6945,6 +7358,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.15 + resolution: "@types/prop-types@npm:15.7.15" + checksum: 10c0/b59aad1ad19bf1733cf524fd4e618196c6c7690f48ee70a327eb450a42aab8e8a063fbe59ca0a5701aebe2d92d582292c0fb845ea57474f6a15f6994b0e260b2 + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.15.1 resolution: "@types/qs@npm:6.15.1" @@ -6959,6 +7379,25 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^18": + version: 18.3.7 + resolution: "@types/react-dom@npm:18.3.7" + peerDependencies: + "@types/react": ^18.0.0 + checksum: 10c0/8bd309e2c3d1604a28a736a24f96cbadf6c05d5288cfef8883b74f4054c961b6b3a5e997fd5686e492be903c8f3380dba5ec017eff3906b1256529cd2d39603e + languageName: node + linkType: hard + +"@types/react@npm:^18": + version: 18.3.31 + resolution: "@types/react@npm:18.3.31" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.2.2" + checksum: 10c0/44180549dd045f536ececd39e39aacdf828e76adc1c4a90b132f453e23cc370c4648d9102ae401172ebd8fd8b1977a901a39e214e53ec77171b27514b588c179 + languageName: node + linkType: hard + "@types/readdir-glob@npm:*": version: 1.1.5 resolution: "@types/readdir-glob@npm:1.1.5" @@ -7037,6 +7476,28 @@ __metadata: languageName: node linkType: hard +"@types/superagent@npm:^8.1.0": + version: 8.1.10 + resolution: "@types/superagent@npm:8.1.10" + dependencies: + "@types/cookiejar": "npm:^2.1.5" + "@types/methods": "npm:^1.1.4" + "@types/node": "npm:*" + form-data: "npm:^4.0.0" + checksum: 10c0/cd911f7e926aae3f23caeebac055c46c5a63576561548356e0ca809147b372a27359112e02a569d527fd677cd90ee6d018b5cdf509a0a5ac70f74289bd29c9bc + languageName: node + linkType: hard + +"@types/supertest@npm:^6": + version: 6.0.3 + resolution: "@types/supertest@npm:6.0.3" + dependencies: + "@types/methods": "npm:^1.1.4" + "@types/superagent": "npm:^8.1.0" + checksum: 10c0/a2080f870154b09db123864a484fb633bc9e2a0f7294a194388df4c7effe5af9de36d5a5ebf819f72b404fa47b5e813c47d5a3a51354251fd2fa8589bfb64f2c + languageName: node + linkType: hard + "@types/workerpool@npm:^6": version: 6.4.7 resolution: "@types/workerpool@npm:6.4.7" @@ -7614,6 +8075,13 @@ __metadata: languageName: node linkType: hard +"ace-builds@npm:^1.34.0": + version: 1.44.0 + resolution: "ace-builds@npm:1.44.0" + checksum: 10c0/c5a614b082daef1621e5392024daaa0c472275cff2ac79e85422027a9f12442fcee8ad8362eab7223d0c90feeaa84280d83991f75a64372ade4854fb4ea60291 + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -9149,6 +9617,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^1.1.0": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 10c0/34dead8bee24f5e96f6e7937d711978380647e936a22e76380290e35486afd8634966ce300fc4b74a32f3762c7d4c0303f442c3e259f4ce02374eb0c82834f27 + languageName: node + linkType: hard + "cmd-shim@npm:^8.0.0": version: 8.0.0 resolution: "cmd-shim@npm:8.0.0" @@ -9325,6 +9800,13 @@ __metadata: languageName: node linkType: hard +"component-emitter@npm:^1.3.0": + version: 1.3.1 + resolution: "component-emitter@npm:1.3.1" + checksum: 10c0/e4900b1b790b5e76b8d71b328da41482118c0f3523a516a41be598dc2785a07fd721098d9bf6e22d89b19f4fa4e1025160dc00317ea111633a3e4f75c2b86032 + languageName: node + linkType: hard + "compress-commons@npm:^6.0.2": version: 6.0.2 resolution: "compress-commons@npm:6.0.2" @@ -9592,6 +10074,13 @@ __metadata: languageName: node linkType: hard +"cookiejar@npm:^2.1.4": + version: 2.1.4 + resolution: "cookiejar@npm:2.1.4" + checksum: 10c0/2dae55611c6e1678f34d93984cbd4bda58f4fe3e5247cc4993f4a305cd19c913bbaf325086ed952e892108115073a747596453d3dc1c34947f47f731818b8ad1 + languageName: node + linkType: hard + "core-util-is@npm:^1.0.3, core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -9692,6 +10181,23 @@ __metadata: languageName: node linkType: hard +"css-selector-tokenizer@npm:^0.8.0": + version: 0.8.0 + resolution: "css-selector-tokenizer@npm:0.8.0" + dependencies: + cssesc: "npm:^3.0.0" + fastparse: "npm:^1.1.2" + checksum: 10c0/86f68cc666d41f9d153351677694002a9d00e2609e6abc66fcfd7f580be3d6ecc0929e46a06c621ab28da5febbb54567db9709b819414edae4a36d9ff9133e16 + languageName: node + linkType: hard + +"css.escape@npm:^1.5.1": + version: 1.5.1 + resolution: "css.escape@npm:1.5.1" + checksum: 10c0/5e09035e5bf6c2c422b40c6df2eb1529657a17df37fda5d0433d722609527ab98090baf25b13970ca754079a0f3161dd3dfc0e743563ded8cfa0749d861c1525 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -9701,6 +10207,29 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.0.2, csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce + languageName: node + linkType: hard + +"d3-path@npm:1": + version: 1.0.9 + resolution: "d3-path@npm:1.0.9" + checksum: 10c0/e35e84df5abc18091f585725b8235e1fa97efc287571585427d3a3597301e6c506dea56b11dfb3c06ca5858b3eb7f02c1bf4f6a716aa9eade01c41b92d497eb5 + languageName: node + linkType: hard + +"d3-shape@npm:^1.3.7": + version: 1.3.7 + resolution: "d3-shape@npm:1.3.7" + dependencies: + d3-path: "npm:1" + checksum: 10c0/548057ce59959815decb449f15632b08e2a1bdce208f9a37b5f98ec7629dda986c2356bc7582308405ce68aedae7d47b324df41507404df42afaf352907577ae + languageName: node + linkType: hard + "dargs@npm:^7.0.0": version: 7.0.0 resolution: "dargs@npm:7.0.0" @@ -9755,6 +10284,15 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^2.25.0": + version: 2.30.0 + resolution: "date-fns@npm:2.30.0" + dependencies: + "@babel/runtime": "npm:^7.21.0" + checksum: 10c0/e4b521fbf22bc8c3db332bbfb7b094fd3e7627de0259a9d17c7551e2d2702608a7307a449206065916538e384f37b181565447ce2637ae09828427aed9cb5581 + languageName: node + linkType: hard + "date-format@npm:^4.0.14": version: 4.0.14 resolution: "date-format@npm:4.0.14" @@ -9830,6 +10368,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + "dedent@npm:^1.0.0": version: 1.7.2 resolution: "dedent@npm:1.7.2" @@ -10111,7 +10656,7 @@ __metadata: languageName: node linkType: hard -"dezalgo@npm:^1.0.0": +"dezalgo@npm:^1.0.0, dezalgo@npm:^1.0.4": version: 1.0.4 resolution: "dezalgo@npm:1.0.4" dependencies: @@ -10181,6 +10726,16 @@ __metadata: languageName: node linkType: hard +"dom-helpers@npm:^5.0.1": + version: 5.2.1 + resolution: "dom-helpers@npm:5.2.1" + dependencies: + "@babel/runtime": "npm:^7.8.7" + csstype: "npm:^3.0.2" + checksum: 10c0/f735074d66dd759b36b158fa26e9d00c9388ee0e8c9b16af941c38f014a37fc80782de83afefd621681b19ac0501034b4f1c4a3bff5caa1b8667f0212b5e124c + languageName: node + linkType: hard + "dot-prop@npm:^5.1.0": version: 5.3.0 resolution: "dot-prop@npm:5.3.0" @@ -10585,6 +11140,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.28.1": + version: 0.28.1 + resolution: "esbuild@npm:0.28.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.1" + "@esbuild/android-arm": "npm:0.28.1" + "@esbuild/android-arm64": "npm:0.28.1" + "@esbuild/android-x64": "npm:0.28.1" + "@esbuild/darwin-arm64": "npm:0.28.1" + "@esbuild/darwin-x64": "npm:0.28.1" + "@esbuild/freebsd-arm64": "npm:0.28.1" + "@esbuild/freebsd-x64": "npm:0.28.1" + "@esbuild/linux-arm": "npm:0.28.1" + "@esbuild/linux-arm64": "npm:0.28.1" + "@esbuild/linux-ia32": "npm:0.28.1" + "@esbuild/linux-loong64": "npm:0.28.1" + "@esbuild/linux-mips64el": "npm:0.28.1" + "@esbuild/linux-ppc64": "npm:0.28.1" + "@esbuild/linux-riscv64": "npm:0.28.1" + "@esbuild/linux-s390x": "npm:0.28.1" + "@esbuild/linux-x64": "npm:0.28.1" + "@esbuild/netbsd-arm64": "npm:0.28.1" + "@esbuild/netbsd-x64": "npm:0.28.1" + "@esbuild/openbsd-arm64": "npm:0.28.1" + "@esbuild/openbsd-x64": "npm:0.28.1" + "@esbuild/openharmony-arm64": "npm:0.28.1" + "@esbuild/sunos-x64": "npm:0.28.1" + "@esbuild/win32-arm64": "npm:0.28.1" + "@esbuild/win32-ia32": "npm:0.28.1" + "@esbuild/win32-x64": "npm:0.28.1" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/29cd456a79ce35ac2c7e05fe871330416b2c395c045d849653f843e51378d6e0d6e774d6dcd01b35f4e83238a29bf8decd04fcd34b3780c589a250b21e5f92bb + languageName: node + linkType: hard + "escalade@npm:3.2.0, escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -11219,6 +11863,13 @@ __metadata: languageName: node linkType: hard +"fast-safe-stringify@npm:^2.1.1": + version: 2.1.1 + resolution: "fast-safe-stringify@npm:2.1.1" + checksum: 10c0/d90ec1c963394919828872f21edaa3ad6f1dddd288d2bd4e977027afff09f5db40f94e39536d4646f7e01761d704d72d51dce5af1b93717f3489ef808f5f4e4d + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.1.2 resolution: "fast-uri@npm:3.1.2" @@ -11285,6 +11936,13 @@ __metadata: languageName: node linkType: hard +"fastparse@npm:^1.1.2": + version: 1.1.2 + resolution: "fastparse@npm:1.1.2" + checksum: 10c0/c08d6e7ef10c0928426c1963dd4593e2baaf44d223ab1e5ba5d7b30470144b3a4ecb3605958b73754cea3f857ecef00b67c885f07ca2c312b38b67d9d88b84b5 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.20.1 resolution: "fastq@npm:1.20.1" @@ -11495,7 +12153,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:4.0.5, form-data@npm:^4.0.5": +"form-data@npm:4.0.5, form-data@npm:^4.0.0, form-data@npm:^4.0.5": version: 4.0.5 resolution: "form-data@npm:4.0.5" dependencies: @@ -11508,6 +12166,18 @@ __metadata: languageName: node linkType: hard +"formidable@npm:^2.1.2": + version: 2.1.5 + resolution: "formidable@npm:2.1.5" + dependencies: + "@paralleldrive/cuid2": "npm:^2.2.2" + dezalgo: "npm:^1.0.4" + once: "npm:^1.4.0" + qs: "npm:^6.11.0" + checksum: 10c0/2c68ca6cccc1ac3de497c50236631fafea8e1a09396d88b4dd2dc9db6029b5abaeb6747b8b97ebc1143cd40cf62c27ba485b8c6317088c066fc999af3ac621d4 + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -12402,6 +13072,18 @@ __metadata: languageName: node linkType: hard +"intl-messageformat@npm:^10.3.1": + version: 10.7.18 + resolution: "intl-messageformat@npm:10.7.18" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.3.6" + "@formatjs/fast-memoize": "npm:2.2.7" + "@formatjs/icu-messageformat-parser": "npm:2.11.4" + tslib: "npm:^2.8.0" + checksum: 10c0/d54da9987335cb2bca26246304cea2ca6b1cb44ca416d6b28f3aa62b11477c72f7ce0bf3f11f5d236ceb1842bdc3378a926e606496d146fde18783ec92c314e1 + languageName: node + linkType: hard + "ip-address@npm:^10.0.1": version: 10.2.0 resolution: "ip-address@npm:10.2.0" @@ -13461,7 +14143,7 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^4.0.0": +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed @@ -14131,6 +14813,17 @@ __metadata: languageName: node linkType: hard +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: "npm:^3.0.0 || ^4.0.0" + bin: + loose-envify: cli.js + checksum: 10c0/655d110220983c1a4b9c0c679a2e8016d4b67f6e9c7b5435ff5979ecdb20d0813f4dec0a08674fcbdd4846a3f07edbb50a36811fd37930b94aaa0d9daceb017e + languageName: node + linkType: hard + "lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -14332,7 +15025,7 @@ __metadata: languageName: node linkType: hard -"methods@npm:~1.1.2": +"methods@npm:^1.1.2, methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" checksum: 10c0/bdf7cc72ff0a33e3eede03708c08983c4d7a173f91348b4b1e4f47d4cdbf734433ad971e7d1e8c77247d9e5cd8adb81ea4c67b0a2db526b758b2233d7814b8b2 @@ -14374,7 +15067,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:^2": +"mime@npm:2.6.0, mime@npm:^2": version: 2.6.0 resolution: "mime@npm:2.6.0" bin: @@ -14565,6 +15258,15 @@ __metadata: languageName: node linkType: hard +"mnth@npm:^2.0.0": + version: 2.0.0 + resolution: "mnth@npm:2.0.0" + dependencies: + "@babel/runtime": "npm:^7.8.0" + checksum: 10c0/7767b52008ed7aee6c8ad7c7e6f4a4b2b9102a44aa9454bc3e2beec84e3f2e928c8b75b53b125127aa48d32a44e9bac491a29f7980ca8e83dc77fbbbcee181a8 + languageName: node + linkType: hard + "mock-fs@npm:^5, mock-fs@npm:^5.5.0": version: 5.5.0 resolution: "mock-fs@npm:5.5.0" @@ -15314,7 +16016,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4": +"object-assign@npm:^4, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 @@ -16266,6 +16968,17 @@ __metadata: languageName: node linkType: hard +"prop-types@npm:^15.6.2": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: "npm:^1.4.0" + object-assign: "npm:^4.1.1" + react-is: "npm:^16.13.1" + checksum: 10c0/59ece7ca2fb9838031d73a48d4becb9a7cc1ed10e610517c7d8f19a1e02fa47f7c27d557d8a5702bec3cfeccddc853579832b43f449e54635803f277b1c78077 + languageName: node + linkType: hard + "propagate@npm:^2.0.0": version: 2.0.1 resolution: "propagate@npm:2.0.1" @@ -16369,21 +17082,21 @@ __metadata: languageName: node linkType: hard -"qs@npm:~6.14.0": - version: 6.14.2 - resolution: "qs@npm:6.14.2" +"qs@npm:^6.11.0, qs@npm:~6.15.1": + version: 6.15.2 + resolution: "qs@npm:6.15.2" dependencies: side-channel: "npm:^1.1.0" - checksum: 10c0/646110124476fc9acf3c80994c8c3a0600cbad06a4ede1c9e93341006e8426d64e85e048baf8f0c4995f0f1bf0f37d1f3acc5ec1455850b81978792969a60ef6 + checksum: 10c0/e6fd5f6f0aab06d480fe9ab15cebfc4ce4235303e2f91dc69a8f7f4df1e668a61c11d1cfbabacf4295cbbeb7b670ed23db45307480726259761f98e5695e93a7 languageName: node linkType: hard -"qs@npm:~6.15.1": - version: 6.15.2 - resolution: "qs@npm:6.15.2" +"qs@npm:~6.14.0": + version: 6.14.2 + resolution: "qs@npm:6.14.2" dependencies: side-channel: "npm:^1.1.0" - checksum: 10c0/e6fd5f6f0aab06d480fe9ab15cebfc4ce4235303e2f91dc69a8f7f4df1e668a61c11d1cfbabacf4295cbbeb7b670ed23db45307480726259761f98e5695e93a7 + checksum: 10c0/646110124476fc9acf3c80994c8c3a0600cbad06a4ede1c9e93341006e8426d64e85e048baf8f0c4995f0f1bf0f37d1f3acc5ec1455850b81978792969a60ef6 languageName: node linkType: hard @@ -16455,13 +17168,56 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0, react-is@npm:^18.3.1": +"react-dom@npm:^18": + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.2" + peerDependencies: + react: ^18.3.1 + checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85 + languageName: node + linkType: hard + +"react-is@npm:^16.13.1": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 + languageName: node + linkType: hard + +"react-is@npm:^18.0.0, react-is@npm:^18.2.0, react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 languageName: node linkType: hard +"react-transition-group@npm:^4.4.2": + version: 4.4.5 + resolution: "react-transition-group@npm:4.4.5" + dependencies: + "@babel/runtime": "npm:^7.5.5" + dom-helpers: "npm:^5.0.1" + loose-envify: "npm:^1.4.0" + prop-types: "npm:^15.6.2" + peerDependencies: + react: ">=16.6.0" + react-dom: ">=16.6.0" + checksum: 10c0/2ba754ba748faefa15f87c96dfa700d5525054a0141de8c75763aae6734af0740e77e11261a1e8f4ffc08fd9ab78510122e05c21c2d79066c38bb6861a886c82 + languageName: node + linkType: hard + +"react@npm:^18": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3 + languageName: node + linkType: hard + "read-cmd-shim@npm:^6.0.0": version: 6.0.0 resolution: "read-cmd-shim@npm:6.0.0" @@ -16956,6 +17712,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.23.2": + version: 0.23.2 + resolution: "scheduler@npm:0.23.2" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78 + languageName: node + linkType: hard + "semver-intersect@npm:^1.5.0": version: 1.5.0 resolution: "semver-intersect@npm:1.5.0" @@ -16992,6 +17757,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.8": + version: 7.8.4 + resolution: "semver@npm:7.8.4" + bin: + semver: bin/semver.js + checksum: 10c0/81b7c296fd7927b80f67fa516b75fa1017caac8167795320de28e76ccbc6f7f01763c30ecd10d6a0d8fd089708ab0548a5aebb94b0870e99c2a2b4600a46389b + languageName: node + linkType: hard + "semver@npm:^7.8.0": version: 7.8.0 resolution: "semver@npm:7.8.0" @@ -17831,6 +18605,34 @@ __metadata: languageName: node linkType: hard +"superagent@npm:^8.1.2": + version: 8.1.2 + resolution: "superagent@npm:8.1.2" + dependencies: + component-emitter: "npm:^1.3.0" + cookiejar: "npm:^2.1.4" + debug: "npm:^4.3.4" + fast-safe-stringify: "npm:^2.1.1" + form-data: "npm:^4.0.0" + formidable: "npm:^2.1.2" + methods: "npm:^1.1.2" + mime: "npm:2.6.0" + qs: "npm:^6.11.0" + semver: "npm:^7.3.8" + checksum: 10c0/016416fc9c3d3a04fb648bc0efb3d3d5c9d96da00de47e4a625d9976d28c6c37ab0a7f185f2c3ec6d653ee8bb522f70fba0c1072aea7774341a6c0269a9fa77f + languageName: node + linkType: hard + +"supertest@npm:^6": + version: 6.3.4 + resolution: "supertest@npm:6.3.4" + dependencies: + methods: "npm:^1.1.2" + superagent: "npm:^8.1.2" + checksum: 10c0/f8c0b6c73b5e87da31feee6ccb36e7af766a438513cad89d6907f22c97edd83b1e765b4c8de955d5f7af4bca5fd0aaf9149ff48e21567dd290b326a8633af2a7 + languageName: node + linkType: hard + "supports-color@npm:7.2.0, supports-color@npm:^7, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -18225,7 +19027,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -18823,6 +19625,20 @@ __metadata: languageName: node linkType: hard +"weekstart@npm:^1.1.0": + version: 1.1.0 + resolution: "weekstart@npm:1.1.0" + checksum: 10c0/ed35ac719917ee7d1b9261bddb5337c91b894738d619eadf1b35b87f7f21c5edb6f95ff91706de094da79697e7816eebaaf54ccdcbd56b4dbec1dc873bc65de7 + languageName: node + linkType: hard + +"weekstart@npm:^2.0.0": + version: 2.0.0 + resolution: "weekstart@npm:2.0.0" + checksum: 10c0/c955cde61d107c70face430f641543be93caf7c81903626cea4207ca3bed0ae69db39b34b168ff23c7ec65dd656b9b4132142c9c5fb52f287f8bf65d0e65b73a + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" From a4411785d6156bb195f5d1f58bab2727de5b6aed Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:54:54 -0400 Subject: [PATCH 10/28] fix: constrain assembly-derived file reads to the project dir (#1631) The LSP followed source/template paths from the cloud assembly (stack-trace frames, source maps, template paths) with only an extension allow-list. If cdk.out is tampered with, a crafted path like `../../../etc/passwd.js` could be read or surfaced as a nav target. Adds a symlink-aware `isWithinRoot` and gates every read: source paths against the project dir, `templateFile` against the assembly dir. Out-of-root paths are dropped as not-navigable (same as non-TS apps). No change for valid assemblies. Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .../cdk-explorer/lib/core/assembly-reader.ts | 27 ++- .../cdk-explorer/lib/core/source-resolver.ts | 63 +++++- .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 28 ++- .../test/_fixtures/builders.test.ts | 43 +++-- .../cdk-explorer/test/_fixtures/builders.ts | 50 +++-- .../test/core/assembly-reader.test.ts | 88 ++++----- .../test/core/source-resolver.test.ts | 181 +++++++++++++----- .../cdk-explorer/test/lsp/server.test.ts | 107 ++++++----- .../test/lsp/template-locator.test.ts | 4 +- .../cloud-assembly-api/lib/construct-tree.ts | 163 +++++++++++++--- .../test/construct-tree.test.ts | 27 ++- 11 files changed, 559 insertions(+), 222 deletions(-) diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts index 385b8f786..182fcc347 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-reader.ts @@ -1,9 +1,9 @@ import * as fs from 'fs'; import * as path from 'path'; -import { buildConstructTree, CloudAssembly, MANIFEST_FILE, type ConstructTreeNode } from '@aws-cdk/cloud-assembly-api'; +import { buildConstructTreeAsync, CloudAssembly, MANIFEST_FILE, type ConstructTreeNode } from '@aws-cdk/cloud-assembly-api'; import { VALIDATION_REPORT_FILE, type PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; import { findCreationStackTrace } from '@aws-cdk/toolkit-lib'; -import { SourceMapResolver, type SourceLocation } from './source-resolver'; +import { SourceMapResolver, isWithinRoot, type SourceLocation } from './source-resolver'; /** * A construct from the cloud assembly, decorated with the user source location @@ -31,9 +31,9 @@ export type AssemblyReadResult = /** * Decorates the cloud assembly's construct tree with the source location of * each node and attaches any policy-validation violations. Tree construction - * (tree.json + stack-metadata join) is delegated to buildConstructTree. + * (tree.json + stack-metadata join) is delegated to buildConstructTreeAsync. */ -export function readAssembly(assemblyDir: string): AssemblyReadResult { +export async function readAssembly(assemblyDir: string): Promise { const manifestPath = path.join(assemblyDir, MANIFEST_FILE); if (!fs.existsSync(manifestPath)) { return { status: 'not-found' }; @@ -41,13 +41,26 @@ export function readAssembly(assemblyDir: string): AssemblyReadResult { try { const assembly = new CloudAssembly(assemblyDir); + // The project root is the parent of the assembly dir: server.ts always + // builds it as /cdk.out, so dirname() recovers the project dir. + // (If --output support is added to relocate cdk.out, thread the IDE-provided + // application dir here instead.) Used to keep every file the resolver reads + // within the project + const projectRoot = path.dirname(assemblyDir); // One resolver per readAssembly call: caches parsed source maps across // constructs, scoped so a fresh synth observes any moved/edited maps. - const sourceResolver = new SourceMapResolver(); - const tree = buildConstructTree(assembly, (fields, stack, constructPath) => ({ + const sourceResolver = new SourceMapResolver(projectRoot); + const tree = await buildConstructTreeAsync(assembly, async (fields, stack, constructPath) => ({ ...fields, + // templateFile comes from the manifest / a nested stack's aws:asset:path, + // both attacker-influenceable if cdk.out is tampered with. Drop any that + // escape the assembly dir so the template read in resourceTarget stays + // contained + templateFile: fields.templateFile && (await isWithinRoot(assemblyDir, fields.templateFile)) + ? fields.templateFile + : undefined, sourceLocation: stack - ? sourceResolver.resolveFrames(findCreationStackTrace(stack, constructPath)) + ? await sourceResolver.resolveFrames(findCreationStackTrace(stack, constructPath)) : undefined, })); diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts b/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts index 4290f5160..890e19bd9 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts @@ -26,6 +26,16 @@ export class SourceMapResolver { private readonly cache = new Map(); private readonly collectedWarnings: string[] = []; + /** + * @param projectRoot - Absolute path to the project the assembly belongs to. + * Every file this resolver reads or returns must stay within it. The paths + * driving those reads come from the cloud assembly (stack-trace frames and + * the source maps they point at), which are attacker-influenceable if + * cdk.out is tampered with, so anything escaping the root is dropped + */ + constructor(private readonly projectRoot: string) { + } + /** Non-fatal warnings collected during resolution (e.g. an unparseable .js.map). */ public get warnings(): readonly string[] { return this.collectedWarnings; @@ -37,7 +47,7 @@ export class SourceMapResolver { * there's no trace (non-TS apps) or every frame is a skip-placeholder * (framework-only call sites). */ - public resolveFrames(frames: readonly string[] | undefined): SourceLocation | undefined { + public async resolveFrames(frames: readonly string[] | undefined): Promise { if (!frames) return undefined; // aws-cdk-lib's renderCallStackJustMyCode (in node_modules/aws-cdk-lib/core/ @@ -48,6 +58,9 @@ export class SourceMapResolver { const parsed = parseFrame(frame); if (!parsed) continue; if (!isSupportedSourceFile(parsed.file)) return undefined; + // The frame's file path is assembly-derived and attacker-influenceable. + // Never read or surface a location outside the project. + if (!(await isWithinRoot(this.projectRoot, parsed.file))) return undefined; return this.mapJsToOriginalSource(parsed); } return undefined; @@ -57,12 +70,12 @@ export class SourceMapResolver { * Map a .js location to its original .ts via a sibling .js.map. Returns the * input location unchanged when it isn't a .js file or has no usable map. */ - private mapJsToOriginalSource(loc: SourceLocation): SourceLocation { + private async mapJsToOriginalSource(loc: SourceLocation): Promise { // Input is allow-listed to .ts/.tsx/.js by resolveFrames; .ts/.tsx are // already source-space, only .js needs mapping back to its original. if (!loc.file.endsWith('.js')) return loc; - const tracer = this.loadTraceMap(loc.file); + const tracer = await this.loadTraceMap(loc.file); if (!tracer) return loc; // trace-mapping uses 0-based columns, stack frames are 1-based. @@ -73,14 +86,17 @@ export class SourceMapResolver { // map URL when building the TraceMap), with any sourceRoot applied — so it's // a file:// URL for local maps. Convert it back to a path. const file = orig.source.startsWith('file://') ? fileURLToPath(orig.source) : orig.source; + // `sources` comes from the .js.map, which is also assembly-derived. Keep the + // mapped original within the project, else fall back to the (in-root) .js. + if (!(await isWithinRoot(this.projectRoot, file))) return loc; return { file, line: orig.line, column: orig.column + 1 }; } - private loadTraceMap(jsFile: string): TraceMap | null { + private async loadTraceMap(jsFile: string): Promise { const cached = this.cache.get(jsFile); if (cached !== undefined) return cached; - const tracer = this.readSourceMap(jsFile); + const tracer = await this.readSourceMap(jsFile); this.cache.set(jsFile, tracer); return tracer; } @@ -91,10 +107,10 @@ export class SourceMapResolver { * any filename (resolved relative to the .js). Returns null when the file has * no map; warns when a referenced map exists but can't be read or parsed. */ - private readSourceMap(jsFile: string): TraceMap | null { + private async readSourceMap(jsFile: string): Promise { let code: string; try { - code = fs.readFileSync(jsFile, 'utf-8'); + code = await fs.promises.readFile(jsFile, 'utf-8'); } catch { return null; // .js not present/readable -> treat as "no map" } @@ -105,11 +121,16 @@ export class SourceMapResolver { let mapUrl = pathToFileURL(jsFile).href; const converter = convertSourceMap.fromSource(code) - ?? convertSourceMap.fromMapFileSource(code, (mapFile) => { + ?? (await convertSourceMap.fromMapFileSource(code, async (mapFile) => { const mapPath = path.resolve(path.dirname(jsFile), mapFile); + // sourceMappingURL is assembly-derived; don't read a map outside the + // project. The throw is caught below and surfaced as a load warning. + if (!(await isWithinRoot(this.projectRoot, mapPath))) { + throw new Error(`source map path escapes the project root: ${mapPath}`); + } mapUrl = pathToFileURL(mapPath).href; - return fs.readFileSync(mapPath, 'utf-8'); - }); + return fs.promises.readFile(mapPath, 'utf-8'); + })); return converter ? new TraceMap(converter.toObject(), mapUrl) : null; } catch (err) { // A map was referenced but couldn't be read/parsed. Surface it once @@ -137,3 +158,25 @@ function parseFrame(frame: string): SourceLocation | undefined { if (!Number.isFinite(line) || !Number.isFinite(column)) return undefined; return { file: m[1], line, column }; } + +/** + * True when `candidate` resolves to a path inside `root` (or is `root` itself). + * Both are resolved to absolute, symlink-real paths first, so a file reached + * through a symlinked directory pointing outside `root` is rejected too. Used + * to keep file reads driven by (attacker-influenceable) cloud-assembly paths + * within the project before any read happens. + */ +export async function isWithinRoot(root: string, candidate: string): Promise { + const realRoot = await realOrSelf(path.resolve(root)); + const realCandidate = await realOrSelf(path.resolve(candidate)); + return realCandidate === realRoot || realCandidate.startsWith(realRoot + path.sep); +} + +/** Real path with symlinks resolved, or the resolved input if it does not exist. */ +async function realOrSelf(p: string): Promise { + try { + return await fs.promises.realpath(p); + } catch { + return p; + } +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 04b24259f..944900099 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -42,7 +42,7 @@ export interface LspHandlerOptions { /** Callback invoked on `didSave` for tracked source files. */ readonly onSynthRequest?: (projectDir: string) => void; /** Override readAssembly for tests. Defaults to reading /cdk.out. */ - readonly readAssembly?: (assemblyDir: string) => AssemblyReadResult; + readonly readAssembly?: (assemblyDir: string) => Promise; /** * Sink for non-fatal messages. In production, the connection's console writes * to the editor's Output panel; in tests, capture into an array. @@ -71,7 +71,7 @@ export interface LspServerOptions extends LspHandlerOptions { /** Pure handler functions for LSP messages, extracted for direct unit testing. */ export interface LspHandlers { onInitialize(params: InitializeParams): InitializeResult; - onInitialized(): void; + onInitialized(): Promise; onDidSaveTextDocument(params: DidSaveTextDocumentParams): void; onCodeLens(params: CodeLensParams): CodeLens[]; onDefinition(params: DefinitionParams): Location | undefined; @@ -121,9 +121,9 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers // the editor's next natural re-query. let codeLensRefreshSupported = false; - function refreshFromAssembly(projectDir: string): void { + async function refreshFromAssembly(projectDir: string): Promise { const assemblyDir = path.join(projectDir, 'cdk.out'); - const result = readAssembly(assemblyDir); + const result = await readAssembly(assemblyDir); if (result.status === 'error') { log.error(`Failed to read cloud assembly: ${result.message}`); @@ -183,7 +183,7 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers }, }; }, - onInitialized() { + async onInitialized() { const projectDir = applicationDir ?? process.cwd(); // Same exclusion logic as toolkit-lib's watch(): // WATCH_EXCLUDE_DEFAULTS covers common non-source dirs, then we add cdk.out @@ -199,13 +199,18 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers ], rootDir: projectDir, }); - refreshFromAssembly(projectDir); + await refreshFromAssembly(projectDir); // Watch cdk.out so any synth (an external `cdk synth`/`cdk watch`, or a // future in-process synth) refreshes the editor's diagnostics and lenses. assemblyWatcher = startWatcher({ assemblyDir: path.join(projectDir, 'cdk.out'), - onChange: () => refreshFromAssembly(projectDir), + onChange: () => { + // refreshFromAssembly is async; surface a rejection rather than + // floating the promise from this watcher callback. + refreshFromAssembly(projectDir).catch((err) => + log.error(`Assembly refresh failed: ${(err as Error).message}`)); + }, onError: (err) => log.error(`Assembly watcher error: ${(err as Error).message}`), }); }, @@ -272,7 +277,14 @@ export function startServer(options: LspServerOptions): void { }); connection.onInitialize((params) => handlers.onInitialize(params)); - connection.onInitialized(() => handlers.onInitialized()); + connection.onInitialized(() => { + // `initialized` is a notification, so nothing awaits us, but onInitialized's + // async work can still reject. Surface it to the editor Output panel rather + // than leaving an unhandled rejection. + handlers.onInitialized().catch((err) => { + connection.console.error(`onInitialized failed: ${(err as Error).message}`); + }); + }); connection.onDidSaveTextDocument((params) => handlers.onDidSaveTextDocument(params)); connection.onCodeLens((params) => handlers.onCodeLens(params)); connection.onDefinition((params) => handlers.onDefinition(params)); diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts index ef2ca50c5..781102611 100644 --- a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.test.ts @@ -1,5 +1,6 @@ // Sanity tests for the fixture builders. If these pass, every other test // that uses the builders inherits a known-good baseline. +import * as path from 'path'; import { buildFlatAssembly, buildNestedAssembly, @@ -19,7 +20,7 @@ describe('fixture builders', () => { dir = undefined; }); - test('buildFlatAssembly produces a readAssembly-compatible directory', () => { + test('buildFlatAssembly produces a readAssembly-compatible directory', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', @@ -31,14 +32,14 @@ describe('fixture builders', () => { }], }); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error(`expected success, got ${result.status}: ${(result as any).message ?? ''}`); expect(result.data.tree).toHaveLength(1); expect(result.data.tree[0].id).toBe('Stack1'); }); - test('buildFlatAssembly attaches logicalId and cfnType correctly', () => { + test('buildFlatAssembly attaches logicalId and cfnType correctly', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', @@ -50,7 +51,7 @@ describe('fixture builders', () => { }], }); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); const resource = result.data.tree[0].children[0].children[0]; @@ -61,7 +62,7 @@ describe('fixture builders', () => { expect(resource.sourceLocation).toBeUndefined(); }); - test('buildFlatAssembly with creationTrace exposes sourceLocation', () => { + test('buildFlatAssembly with creationTrace exposes sourceLocation', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', @@ -74,24 +75,25 @@ describe('fixture builders', () => { // so the first frame that parses is the user's call site. creationTrace: [ ' ...node_modules-aws-cdk-lib...', - ' at new MyStack (/project/lib/my-stack.ts:12:5)', + ' at new MyStack (/lib/my-stack.ts:12:5)', ], }], }], }); + const projectRoot = path.dirname(dir); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); const resource = result.data.tree[0].children[0].children[0]; expect(resource.sourceLocation).toEqual({ - file: '/project/lib/my-stack.ts', + file: path.join(projectRoot, 'lib/my-stack.ts'), line: 12, column: 5, }); }); - test('buildNestedAssembly produces a Stage-based assembly with correct tree', () => { + test('buildNestedAssembly produces a Stage-based assembly with correct tree', async () => { dir = buildNestedAssembly({ stages: [{ id: 'Prod', @@ -106,7 +108,7 @@ describe('fixture builders', () => { }], }); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); expect(result.data.tree).toHaveLength(1); @@ -123,7 +125,7 @@ describe('fixture builders', () => { expect(resource.type).toBe('AWS::S3::Bucket'); }); - test('buildNestedStackAssembly enriches resources INSIDE a NestedStack via parent metadata', () => { + test('buildNestedStackAssembly enriches resources INSIDE a NestedStack via parent metadata', async () => { // aws-cdk-lib emits nested-stack-internal metadata into the parent's // artifact metadata. Verifies buildNode's metadata inheritance handles // this with no separate walker. @@ -143,14 +145,15 @@ describe('fixture builders', () => { cfnType: 'AWS::S3::Bucket', creationTrace: [ ' ...node_modules-aws-cdk-lib...', - ' at new MyNestedStack (/project/lib/nested.ts:7:5)', + ' at new MyNestedStack (/lib/nested.ts:7:5)', ], }], }], }, }); + const projectRoot = path.dirname(dir); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); const parent = result.data.tree[0]; @@ -168,16 +171,16 @@ describe('fixture builders', () => { expect(nestedBucket.logicalId).toBe('NestedBucketBBB'); expect(nestedBucket.type).toBe('AWS::S3::Bucket'); expect(nestedBucket.sourceLocation).toEqual({ - file: '/project/lib/nested.ts', + file: path.join(projectRoot, 'lib/nested.ts'), line: 7, column: 5, }); }); - test('buildNonTypeScriptAssembly returns success without crashing', () => { + test('buildNonTypeScriptAssembly returns success without crashing', async () => { dir = buildNonTypeScriptAssembly(); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); expect(result.data.tree).toHaveLength(1); @@ -187,13 +190,13 @@ describe('fixture builders', () => { expect(stack.sourceLocation).toBeUndefined(); }); - test('withMalformedValidationReport surfaces the error without breaking the tree', () => { + test('withMalformedValidationReport surfaces the error without breaking the tree', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }], }); withMalformedValidationReport(dir); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); expect(result.data.tree).toHaveLength(1); @@ -201,7 +204,7 @@ describe('fixture builders', () => { expect(result.data.warnings.some((w) => w.includes('validation-report.json'))).toBe(true); }); - test('withValidationReport produces a parseable report', () => { + test('withValidationReport produces a parseable report', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }], }); @@ -218,7 +221,7 @@ describe('fixture builders', () => { }], }); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); expect(result.data.violations?.pluginReports[0].pluginName).toBe('test'); diff --git a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts index d87ee21ab..1c12486ec 100644 --- a/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts +++ b/packages/@aws-cdk/cdk-explorer/test/_fixtures/builders.ts @@ -88,12 +88,13 @@ export interface NestedStackParentSpec { /** Build a flat assembly (stacks directly under App, no Stages). */ export function buildFlatAssembly(spec: FlatAssemblySpec): string { const dir = mkAssemblyDir('flat'); + const projectRoot = path.dirname(dir); const artifacts: Record = { Tree: { type: 'cdk:tree', properties: { file: TREE_FILE } }, }; for (const stack of spec.stacks) { - artifacts[stack.id] = stackArtifact(stack); + artifacts[stack.id] = stackArtifact(stack, projectRoot); writeTemplate(dir, stack.id, stack.resources, stack.id, spec.pathMetadata ?? true); } @@ -112,6 +113,7 @@ export function buildFlatAssembly(spec: FlatAssemblySpec): string { /** Build a Stage-based assembly (each stage produces a nested cloud-assembly). */ export function buildNestedAssembly(spec: NestedAssemblySpec): string { const rootDir = mkAssemblyDir('nested'); + const projectRoot = path.dirname(rootDir); const rootArtifacts: Record = { Tree: { type: 'cdk:tree', properties: { file: TREE_FILE } }, }; @@ -139,7 +141,7 @@ export function buildNestedAssembly(spec: NestedAssemblySpec): string { // displayName is the construct path; the reader keys metadata by // hierarchicalId, which falls back to displayName. displayName: constructPath, - metadata: stackMetadata(stack.resources, `/${constructPath}`), + metadata: stackMetadata(stack.resources, `/${constructPath}`, projectRoot), }; writeTemplate(stageDir, artifactId, stack.resources, constructPath); } @@ -183,6 +185,7 @@ export function buildNestedAssembly(spec: NestedAssemblySpec): string { */ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec }): string { const dir = mkAssemblyDir('nestedstack'); + const projectRoot = path.dirname(dir); const { parent } = spec; // All nested resources' metadata lives under the PARENT artifact, keyed by @@ -192,7 +195,7 @@ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec } const parentResources: Record = {}; for (const r of parent.resources) { - metadata[`/${parent.id}/${r.id}/Resource`] = [logicalIdEntry(r)]; + metadata[`/${parent.id}/${r.id}/Resource`] = [logicalIdEntry(r, projectRoot)]; parentChildren[r.id] = resourceTreeNode(parent.id, r); parentResources[r.logicalId] = { Type: r.cfnType, @@ -201,7 +204,7 @@ export function buildNestedStackAssembly(spec: { parent: NestedStackParentSpec } }; } for (const ns of parent.nestedStacks) { - emitNestedStack(dir, metadata, parentChildren, parentResources, parent.id, ns); + emitNestedStack(dir, metadata, parentChildren, parentResources, parent.id, ns, projectRoot); } writeJson(path.join(dir, MANIFEST_FILE), { @@ -245,6 +248,7 @@ function emitNestedStack( parentResources: Record, parentPath: string, ns: NestedStackSpec, + projectRoot: string, ): void { const nsPath = `${parentPath}/${ns.id}`; const templateFile = `${nsPath.replace(/\//g, '')}.nested.template.json`; @@ -279,7 +283,7 @@ function emitNestedStack( const nsChildren: Record = {}; const nsResources: Record = {}; for (const r of ns.resources) { - metadata[`/${nsPath}/${r.id}/Resource`] = [logicalIdEntry(r)]; + metadata[`/${nsPath}/${r.id}/Resource`] = [logicalIdEntry(r, projectRoot)]; nsChildren[r.id] = resourceTreeNode(nsPath, r); nsResources[r.logicalId] = { Type: r.cfnType, @@ -288,7 +292,7 @@ function emitNestedStack( }; } for (const child of ns.nestedStacks ?? []) { - emitNestedStack(dir, metadata, nsChildren, nsResources, nsPath, child); + emitNestedStack(dir, metadata, nsChildren, nsResources, nsPath, child, projectRoot); } writeJson(path.join(dir, templateFile), { Resources: nsResources }); @@ -384,7 +388,10 @@ export function cleanupFixture(dir: string | undefined): void { // ---------- internals ---------- function mkAssemblyDir(prefix: string): string { - const base = fs.mkdtempSync(path.join(os.tmpdir(), `cdk-explorer-${prefix}-`)); + // realpath the temp dir so the project root (its parent) is canonical and + // matches source paths resolved from traces under the reader's containment + // check (e.g. macOS /var -> /private/var). + const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), `cdk-explorer-${prefix}-`))); return path.join(base, 'cdk.out'); } @@ -437,32 +444,47 @@ function resourceTreeNode(parentPath: string, r: ResourceSpec): unknown { }; } -function stackArtifact(stack: StackSpec): unknown { +function stackArtifact(stack: StackSpec, projectRoot: string): unknown { return { type: 'aws:cloudformation:stack', environment: 'aws://unknown-account/unknown-region', properties: { templateFile: `${stack.id}.template.json` }, displayName: stack.id, - metadata: stackMetadata(stack.resources, `/${stack.id}`), + metadata: stackMetadata(stack.resources, `/${stack.id}`, projectRoot), }; } -function stackMetadata(resources: readonly ResourceSpec[], pathPrefix: string): Record { +function stackMetadata( + resources: readonly ResourceSpec[], + pathPrefix: string, + projectRoot: string, +): Record { const metadata: Record = {}; for (const r of resources) { - metadata[`${pathPrefix}/${r.id}/Resource`] = [logicalIdEntry(r)]; + metadata[`${pathPrefix}/${r.id}/Resource`] = [logicalIdEntry(r, projectRoot)]; } return metadata; } -function logicalIdEntry(r: ResourceSpec): Record { +function logicalIdEntry(r: ResourceSpec, projectRoot: string): Record { const entry: Record = { type: ArtifactMetadataEntryType.LOGICAL_ID, data: r.logicalId }; - if (r.creationTrace && r.creationTrace.length > 0) { - entry.trace = [...r.creationTrace]; + const trace = rebaseTrace(r.creationTrace, projectRoot); + if (trace && trace.length > 0) { + entry.trace = trace; } return entry; } +/** + * Anchor a creation trace's source paths to the fixture's project root (the + * parent of cdk.out), mirroring real apps whose sources live beside cdk.out. + * Tests write `` in a frame; the reader's containment check then + * accepts the resolved path instead of dropping it as out-of-project. + */ +function rebaseTrace(trace: readonly string[] | undefined, projectRoot: string): string[] | undefined { + return trace?.map((frame) => frame.replace(//g, projectRoot)); +} + function writeTemplate( dir: string, fileBaseId: string, diff --git a/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts index 19af13d53..9a4d3c001 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/assembly-reader.test.ts @@ -29,20 +29,20 @@ describe('readAssembly', () => { dir = undefined; }); - test('returns not-found for nonexistent directory', () => { - expect(readAssembly('/nonexistent/path').status).toBe('not-found'); + test('returns not-found for nonexistent directory', async () => { + expect((await readAssembly('/nonexistent/path')).status).toBe('not-found'); }); - test('returns not-found for directory without manifest.json', () => { + test('returns not-found for directory without manifest.json', async () => { const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-empty-')); try { - expect(readAssembly(empty).status).toBe('not-found'); + expect((await readAssembly(empty)).status).toBe('not-found'); } finally { fs.rmSync(empty, { recursive: true, force: true }); } }); - test('parses tree from a flat assembly', () => { + test('parses tree from a flat assembly', async () => { dir = buildFlatAssembly({ stacks: [ { id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }, @@ -50,13 +50,13 @@ describe('readAssembly', () => { ], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(data.tree).toHaveLength(2); expect(data.tree.map((n) => n.id).sort()).toEqual(['Stack1', 'Stack2']); }); - test('enriches resource nodes with logicalId and cfnType', () => { + test('enriches resource nodes with logicalId and cfnType', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', @@ -65,14 +65,14 @@ describe('readAssembly', () => { }], }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); const resource = findNode(data.tree, 'Stack1/MyBucket/Resource')!; expect(resource.logicalId).toBe('MyBucketF68F3FF0'); expect(resource.type).toBe('AWS::S3::Bucket'); }); - test('non-resource constructs (L2 wrappers) have no logicalId or type', () => { + test('non-resource constructs (L2 wrappers) have no logicalId or type', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', @@ -83,28 +83,28 @@ describe('readAssembly', () => { }], }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); const wrapper = findNode(data.tree, 'Stack1/MyBucket')!; expect(wrapper.logicalId).toBeUndefined(); expect(wrapper.type).toBeUndefined(); }); - test('children are arrays', () => { + test('children are arrays', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [{ id: 'X', logicalId: 'X', cfnType: 'AWS::S3::Bucket' }] }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(Array.isArray(data.tree)).toBe(true); expect(Array.isArray(data.tree[0].children)).toBe(true); }); - test('returns error for malformed manifest', () => { + test('returns error for malformed manifest', async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-malformed-')); fs.writeFileSync(path.join(tmpDir, MANIFEST_FILE), 'not json{{{'); try { - const result = readAssembly(tmpDir); + const result = await readAssembly(tmpDir); expect(result.status).toBe('error'); if (result.status === 'error') expect(result.message).toBeTruthy(); } finally { @@ -121,7 +121,7 @@ describe('readAssembly with violations', () => { dir = undefined; }); - test('parses validation report when present', () => { + test('parses validation report when present', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); withValidationReport(dir, { pluginReports: [{ @@ -139,36 +139,36 @@ describe('readAssembly with violations', () => { }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(data.violations).toBeDefined(); expect(data.violations!.pluginReports[0].conclusion).toBe('failure'); expect(data.violations!.pluginReports[0].violations[0].ruleName).toBe('no-public-buckets'); }); - test('returns undefined violations when report file is absent', () => { + test('returns undefined violations when report file is absent', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(data.violations).toBeUndefined(); }); - test('malformed validation report does not crash the tree read', () => { + test('malformed validation report does not crash the tree read', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); withMalformedValidationReport(dir); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(data.tree).toHaveLength(1); expect(data.violations).toBeUndefined(); expect(data.warnings.some((w) => w.includes('validation-report.json'))).toBe(true); }); - test('loads a version-less validation report (legacy aws-cdk-lib shape)', () => { + test('loads a version-less validation report (legacy aws-cdk-lib shape)', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [] }] }); withVersionlessValidationReport(dir); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(data.tree).toHaveLength(1); expect(data.violations).toBeDefined(); @@ -183,17 +183,17 @@ describe('readAssembly with Stage-based (nested-assembly) apps', () => { dir = undefined; }); - test('preserves Stage grouping node in the tree', () => { + test('preserves Stage grouping node in the tree', async () => { dir = buildNestedAssembly({ stages: [{ id: 'Prod', stacks: [{ id: 'Service', resources: [] }] }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); const stage = data.tree.find((n) => n.id === 'Prod')!; expect(stage.path).toBe('Prod'); }); - test('resources inside a Stage stack get logicalId and cfnType', () => { + test('resources inside a Stage stack get logicalId and cfnType', async () => { dir = buildNestedAssembly({ stages: [{ id: 'Prod', @@ -203,14 +203,14 @@ describe('readAssembly with Stage-based (nested-assembly) apps', () => { }], }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); const resource = findNode(data.tree, 'Prod/Service/MyBucket/Resource')!; expect(resource.logicalId).toBe('MyBucketABC'); expect(resource.type).toBe('AWS::S3::Bucket'); }); - test('multi-stage apps route metadata to the correct nested assembly', () => { + test('multi-stage apps route metadata to the correct nested assembly', async () => { // Two stages, same construct ids — proves we don't cross-contaminate // metadata from one stage's nested-assembly manifest to another's. dir = buildNestedAssembly({ @@ -219,7 +219,7 @@ describe('readAssembly with Stage-based (nested-assembly) apps', () => { { id: 'Staging', stacks: [{ id: 'Service', resources: [{ id: 'X', logicalId: 'StagingX', cfnType: 'AWS::S3::Bucket' }] }] }, ], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(findNode(data.tree, 'Prod/Service/X/Resource')!.logicalId).toBe('ProdX'); expect(findNode(data.tree, 'Staging/Service/X/Resource')!.logicalId).toBe('StagingX'); @@ -234,10 +234,10 @@ describe('readAssembly graceful degradation', () => { dir = undefined; }); - test('non-TypeScript app returns success with no source enrichment, no crash', () => { + test('non-TypeScript app returns success with no source enrichment, no crash', async () => { dir = buildNonTypeScriptAssembly(); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(data.tree).toHaveLength(1); const stack = data.tree[0]; @@ -254,11 +254,11 @@ describe('readAssembly resource templateFile', () => { dir = undefined; }); - test('sets templateFile to the resource\'s own stack template, none on wrappers', () => { + test('sets templateFile to the resource\'s own stack template, none on wrappers', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(findNode(data.tree, 'Stack1/MyBucket/Resource')!.templateFile) .toBe(path.join(dir!, 'Stack1.template.json')); @@ -266,7 +266,7 @@ describe('readAssembly resource templateFile', () => { expect(findNode(data.tree, 'Stack1/MyBucket')!.templateFile).toBeUndefined(); }); - test('resolves nested-stack resources to the nested template, not the parent', () => { + test('resolves nested-stack resources to the nested template, not the parent', async () => { dir = buildNestedStackAssembly({ parent: { id: 'Parent', @@ -277,7 +277,7 @@ describe('readAssembly resource templateFile', () => { }], }, }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); // Top-level resource lives in the parent template. expect(findNode(data.tree, 'Parent/TopBucket/Resource')!.templateFile) @@ -287,7 +287,7 @@ describe('readAssembly resource templateFile', () => { .toBe(path.join(dir!, 'ParentMyNested.nested.template.json')); }); - test('keeps templates distinct when two stacks share a logical id (stack-relative ids)', () => { + test('keeps templates distinct when two stacks share a logical id (stack-relative ids)', async () => { // Same-shape stacks produce the SAME stack-relative logicalId in different // templates; each resource must resolve to its OWN stack's template. dir = buildFlatAssembly({ @@ -296,13 +296,13 @@ describe('readAssembly resource templateFile', () => { { id: 'Dev', resources: [{ id: 'Data', logicalId: 'DataX', cfnType: 'AWS::S3::Bucket' }] }, ], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(findNode(data.tree, 'Prod/Data/Resource')!.templateFile).toBe(path.join(dir!, 'Prod.template.json')); expect(findNode(data.tree, 'Dev/Data/Resource')!.templateFile).toBe(path.join(dir!, 'Dev.template.json')); }); - test('resolves a parent resource and its nested-stack twin that share a logical id', () => { + test('resolves a parent resource and its nested-stack twin that share a logical id', async () => { // A NestedStack resets the logical-ID namespace, so a parent resource and a // resource in its own nested stack can share an id. The globally-unique // construct path (aws:cdk:path) disambiguates them. @@ -316,7 +316,7 @@ describe('readAssembly resource templateFile', () => { }], }, }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(findNode(data.tree, 'Parent/Data/Resource')!.templateFile) .toBe(path.join(dir!, 'Parent.template.json')); @@ -324,7 +324,7 @@ describe('readAssembly resource templateFile', () => { .toBe(path.join(dir!, 'ParentNested.nested.template.json')); }); - test('resolves a resource in a doubly-nested stack to the innermost template', () => { + test('resolves a resource in a doubly-nested stack to the innermost template', async () => { dir = buildNestedStackAssembly({ parent: { id: 'Parent', @@ -339,7 +339,7 @@ describe('readAssembly resource templateFile', () => { }], }, }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(findNode(data.tree, 'Parent/Outer/OuterFn/Resource')!.templateFile) .toBe(path.join(dir!, 'ParentOuter.nested.template.json')); @@ -347,7 +347,7 @@ describe('readAssembly resource templateFile', () => { .toBe(path.join(dir!, 'ParentOuterInner.nested.template.json')); }); - test('skips an unreadable nested template instead of failing the whole read', () => { + test('skips an unreadable nested template instead of failing the whole read', async () => { dir = buildNestedStackAssembly({ parent: { id: 'Parent', @@ -360,20 +360,20 @@ describe('readAssembly resource templateFile', () => { }); fs.rmSync(path.join(dir, 'ParentNested.nested.template.json')); // Still a success: the missing nested template degrades only its subtree. - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(findNode(data.tree, 'Parent/TopBucket/Resource')!.templateFile) .toBe(path.join(dir!, 'Parent.template.json')); expect(findNode(data.tree, 'Parent/Nested/NestedQueue/Resource')!.templateFile).toBeUndefined(); }); - test('resolves templateFile without path metadata (positional, not id-based)', () => { + test('resolves templateFile without path metadata (positional, not id-based)', async () => { // Resolution threads the template down the construct tree, so it works // regardless of --no-path-metadata (no reliance on aws:cdk:path). dir = buildFlatAssembly({ pathMetadata: false, stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], }); - const data = expectSuccess(readAssembly(dir)); + const data = expectSuccess(await readAssembly(dir)); expect(findNode(data.tree, 'Stack1/MyBucket/Resource')!.templateFile) .toBe(path.join(dir!, 'Stack1.template.json')); diff --git a/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts index 644e082b9..0709cf281 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts @@ -1,26 +1,47 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { SourceMapResolver } from '../../lib/core/source-resolver'; +import { SourceMapResolver, isWithinRoot } from '../../lib/core/source-resolver'; const SOURCE_MAPS_DIR = path.join(__dirname, '..', '_fixtures', 'source-maps'); +const SAMPLE_JS = path.join(SOURCE_MAPS_DIR, 'sample.js'); +const SAMPLE_MAP = path.join(SOURCE_MAPS_DIR, 'sample.js.map'); + +const tmpDirs: string[] = []; +afterEach(() => { + tmpDirs.forEach((d) => fs.rmSync(d, { recursive: true, force: true })); + tmpDirs.length = 0; + jest.restoreAllMocks(); +}); -let resolver: SourceMapResolver; +const tmpDir = (): string => { + // realpath so the macOS /var -> /private/var symlink doesn't make an existing + // root and a not-yet-created child disagree under isWithinRoot. + const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'srcmap-'))); + tmpDirs.push(dir); + return dir; +}; -beforeEach(() => { - // Fresh resolver (and cache) per test so source-map parses don't bleed between cases. - resolver = new SourceMapResolver(); -}); +// sample.js body without its trailing sourceMappingURL comment, so each test +// can attach its own (inline or external) form. greet() stays on line 5. +const sampleBody = (): string => + fs.readFileSync(SAMPLE_JS, 'utf-8').replace(/\/\/# sourceMappingURL=.*$/m, '').trimEnd(); // NOTE: choosing WHICH metadata entry's frames to use (LOGICAL_ID.trace vs // aws:cdk:creationStack) is toolkit-lib's findCreationStackTrace; this resolver // only turns the chosen frames into a user source location. describe('SourceMapResolver.resolveFrames', () => { - test('returns undefined for no frames', () => { - expect(resolver.resolveFrames(undefined)).toBeUndefined(); + // All frames in this block live under /project; the resolver is rooted there. + let resolver: SourceMapResolver; + beforeEach(() => { + resolver = new SourceMapResolver('/project'); }); - test('returns undefined when all frames are skip-placeholders', () => { + test('returns undefined for no frames', async () => { + expect(await resolver.resolveFrames(undefined)).toBeUndefined(); + }); + + test('returns undefined when all frames are skip-placeholders', async () => { // aws-cdk-lib's renderCallStackJustMyCode emits these for filtered frames // (e.g. node_modules and node: internals). They have no parens and no // :line:col, so they don't match FRAME_RE. @@ -29,87 +50,78 @@ describe('SourceMapResolver.resolveFrames', () => { ' ...node internals...', ' (no user code in 10 frames, use --stack-trace-limit to capture more)', ]; - expect(resolver.resolveFrames(frames)).toBeUndefined(); + expect(await resolver.resolveFrames(frames)).toBeUndefined(); }); - test('skips skip-placeholder frames and picks the first parseable user frame', () => { + test('skips skip-placeholder frames and picks the first parseable user frame', async () => { const frames = [ ' ...node_modules-aws-cdk-lib...', ' at new MyStack (/project/lib/my-stack.ts:42:7)', ' at Object. (/project/bin/app.ts:8:1)', ]; - expect(resolver.resolveFrames(frames)).toEqual({ + expect(await resolver.resolveFrames(frames)).toEqual({ file: '/project/lib/my-stack.ts', line: 42, column: 7, }); }); - test('returns undefined for a non-TypeScript (host-language) frame', () => { - expect(resolver.resolveFrames([' at (/project/app/my_stack.py:42:5)'])).toBeUndefined(); + test('returns undefined for a non-TypeScript (host-language) frame', async () => { + expect(await resolver.resolveFrames([' at (/project/app/my_stack.py:42:5)'])).toBeUndefined(); }); }); describe('source-map resolution', () => { - const SAMPLE_JS = path.join(SOURCE_MAPS_DIR, 'sample.js'); - const SAMPLE_MAP = path.join(SOURCE_MAPS_DIR, 'sample.js.map'); - - const tmpDirs: string[] = []; - afterEach(() => tmpDirs.forEach((d) => fs.rmSync(d, { recursive: true, force: true }))); - - // sample.js body without its trailing sourceMappingURL comment, so each test - // can attach its own (inline or external) form. greet() stays on line 5. - const sampleBody = (): string => - fs.readFileSync(SAMPLE_JS, 'utf-8').replace(/\/\/# sourceMappingURL=.*$/m, '').trimEnd(); - const tmpDir = (): string => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'srcmap-')); - tmpDirs.push(dir); - return dir; - }; - - test('resolves .js to .ts using sibling .js.map', () => { + test('resolves .js to .ts using sibling .js.map', async () => { // sample.js line 5: `function greet(name) {` - const result = resolver.resolveFrames([` at greet (${SAMPLE_JS}:5:10)`]); + const resolver = new SourceMapResolver(SOURCE_MAPS_DIR); + const result = await resolver.resolveFrames([` at greet (${SAMPLE_JS}:5:10)`]); expect(result).toBeDefined(); expect(result!.file).toContain('sample.ts'); expect(result!.line).toBe(2); // ts line 2 = `export function greet` }); - test('falls back to .js location when no .js.map exists', () => { - expect(resolver.resolveFrames([' at someFn (/tmp/no-map-here.js:1:1)'])).toEqual({ - file: '/tmp/no-map-here.js', line: 1, column: 1, + test('falls back to .js location when no .js.map exists', async () => { + const dir = tmpDir(); + const jsPath = path.join(dir, 'no-map-here.js'); + const resolver = new SourceMapResolver(dir); + expect(await resolver.resolveFrames([` at someFn (${jsPath}:1:1)`])).toEqual({ + file: jsPath, line: 1, column: 1, }); }); - test('returns .ts location unchanged (no source-map needed)', () => { - expect(resolver.resolveFrames([' at someFn (/project/lib/foo.ts:3:2)'])).toEqual({ + test('returns .ts location unchanged (no source-map needed)', async () => { + const resolver = new SourceMapResolver('/project'); + expect(await resolver.resolveFrames([' at someFn (/project/lib/foo.ts:3:2)'])).toEqual({ file: '/project/lib/foo.ts', line: 3, column: 2, }); }); - test('resolves an inline (data: URI) source map', () => { + test('resolves an inline (data: URI) source map', async () => { const b64 = Buffer.from(fs.readFileSync(SAMPLE_MAP, 'utf-8'), 'utf-8').toString('base64'); const dir = tmpDir(); const jsPath = path.join(dir, 'sample.js'); fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=data:application/json;base64,${b64}\n`); - const result = resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + const resolver = new SourceMapResolver(dir); + const result = await resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); expect(result!.file).toContain('sample.ts'); expect(result!.line).toBe(2); }); - test('resolves an external map referenced under a non-default filename', () => { + test('resolves an external map referenced under a non-default filename', async () => { const dir = tmpDir(); fs.copyFileSync(SAMPLE_MAP, path.join(dir, 'renamed.map')); const jsPath = path.join(dir, 'sample.js'); fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=renamed.map\n`); - const result = resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + const resolver = new SourceMapResolver(dir); + const result = await resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); expect(result!.file).toContain('sample.ts'); expect(result!.line).toBe(2); }); - test('applies sourceRoot, resolving sources relative to the map', () => { + test('applies sourceRoot, resolving sources relative to the map', async () => { const dir = tmpDir(); const map = JSON.parse(fs.readFileSync(SAMPLE_MAP, 'utf-8')); map.sourceRoot = 'nested/'; @@ -117,8 +129,91 @@ describe('source-map resolution', () => { const jsPath = path.join(dir, 'sample.js'); fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=renamed.map\n`); - const result = resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + const resolver = new SourceMapResolver(dir); + const result = await resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); expect(result!.file).toBe(path.join(dir, 'nested', 'sample.ts')); expect(result!.line).toBe(2); }); }); + +// Paths from the (attacker-influenceable) cloud assembly must never drive a +// read or a navigation target outside the project. +describe('path containment', () => { + test('drops a frame whose file escapes the project root, without reading it', async () => { + const root = tmpDir(); + const readSpy = jest.spyOn(fs, 'readFileSync'); + const resolver = new SourceMapResolver(root); + + expect(await resolver.resolveFrames([' at evil (/etc/passwd.js:1:1)'])).toBeUndefined(); + // The escaping path must be rejected before any read of it. + expect(readSpy.mock.calls.some(([p]) => String(p).includes('/etc/passwd'))).toBe(false); + }); + + test('drops a .ts frame that escapes the project root', async () => { + const root = tmpDir(); + const resolver = new SourceMapResolver(root); + expect(await resolver.resolveFrames([` at f (${path.join(root, '..', 'outside.ts')}:1:1)`])).toBeUndefined(); + }); + + test('ignores an external source map that escapes the root, falling back to the .js', async () => { + const dir = tmpDir(); + const jsPath = path.join(dir, 'sample.js'); + // In-root .js, but its sourceMappingURL points outside the project. + fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=../../../../etc/evil.map\n`); + const readSpy = jest.spyOn(fs, 'readFileSync'); + + const resolver = new SourceMapResolver(dir); + const result = await resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + + // Falls back to the (in-root) .js location; the escaping map is never read. + expect(result).toEqual({ file: jsPath, line: 5, column: 10 }); + expect(readSpy.mock.calls.some(([p]) => String(p).includes('etc/evil.map'))).toBe(false); + expect(resolver.warnings.join('\n')).toMatch(/escapes the project root/); + }); + + test('falls back to the .js when the mapped original resolves outside the root', async () => { + const dir = tmpDir(); + const map = JSON.parse(fs.readFileSync(SAMPLE_MAP, 'utf-8')); + map.sourceRoot = '../../../../../../etc/'; // pushes sample.ts outside the root + fs.writeFileSync(path.join(dir, 'renamed.map'), JSON.stringify(map)); + const jsPath = path.join(dir, 'sample.js'); + fs.writeFileSync(jsPath, `${sampleBody()}\n//# sourceMappingURL=renamed.map\n`); + + const resolver = new SourceMapResolver(dir); + const result = await resolver.resolveFrames([` at greet (${jsPath}:5:10)`]); + expect(result).toEqual({ file: jsPath, line: 5, column: 10 }); + }); +}); + +describe('isWithinRoot', () => { + const root = path.resolve('/srv/app'); + + test('accepts a path inside the root', async () => { + expect(await isWithinRoot(root, path.join(root, 'lib/stack.ts'))).toBe(true); + }); + + test('accepts the root itself', async () => { + expect(await isWithinRoot(root, root)).toBe(true); + }); + + test('rejects traversal above the root', async () => { + expect(await isWithinRoot(root, path.join(root, '../../etc/passwd'))).toBe(false); + }); + + test('accepts traversal that stays within the root', async () => { + expect(await isWithinRoot(root, path.join(root, 'lib/../bin/cdk.ts'))).toBe(true); + }); + + test('does not treat a sibling dir with a shared prefix as inside', async () => { + expect(await isWithinRoot(root, path.resolve('/srv/app-secrets/file'))).toBe(false); + }); + + test('rejects a target reached through a symlink that points outside the root', async () => { + const root2 = tmpDir(); + const outside = tmpDir(); + fs.writeFileSync(path.join(outside, 'secret.ts'), 'secret'); + fs.symlinkSync(outside, path.join(root2, 'link')); + // Lexically inside root2, but realpath lands in `outside`. + expect(await isWithinRoot(root2, path.join(root2, 'link', 'secret.ts'))).toBe(false); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index 86da656ff..2abbd3dc7 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -13,8 +13,8 @@ interface CapturedClient { log: { warn: jest.Mock; error: jest.Mock }; refreshCodeLens: jest.Mock; watcherClosed: jest.Mock; - /** Fire the cdk.out watcher's onChange, as a real re-synth would. */ - triggerWatcher: () => void; + /** Fire the cdk.out watcher's onChange and await the refresh it triggers. */ + triggerWatcher: () => Promise; } function createTestClient(opts?: Partial>): CapturedClient { @@ -27,7 +27,7 @@ function createTestClient(opts?: Partial ({ status: 'not-found' })), + readAssembly: opts?.readAssembly ?? (async () => ({ status: 'not-found' })), logger: log, onPublishDiagnostics: (uri, diagnostics) => published.push({ uri, diagnostics }), onRefreshCodeLenses: refreshCodeLens, @@ -48,22 +48,27 @@ function createTestClient(opts?: Partial watcherOnChange?.(), + triggerWatcher: async () => { + watcherOnChange?.(); + // onChange kicks off refreshFromAssembly fire-and-forget; let it settle + // before the test asserts on the published diagnostics. + await new Promise((resolve) => setImmediate(resolve)); + }, }; } -function initializeClient( +async function initializeClient( client: CapturedClient, options?: Record, capabilities?: InitializeParams['capabilities'], -): void { +): Promise { client.handlers.onInitialize({ processId: null, capabilities: capabilities ?? {}, rootUri: null, initializationOptions: options ?? {}, }); - client.handlers.onInitialized(); + await client.handlers.onInitialized(); } function bucketViolationFixtures() { @@ -98,10 +103,10 @@ function bucketViolationFixtures() { * with the violation resolved on every read after, simulating a user fixing it * and re-synthing. */ -function readAssemblyResolvingAfterFirst(): () => AssemblyReadResult { +function readAssemblyResolvingAfterFirst(): () => Promise { const { tree, violations } = bucketViolationFixtures(); let call = 0; - return (): AssemblyReadResult => { + return async (): Promise => { call += 1; return { status: 'success', @@ -134,12 +139,12 @@ describe('LSP Server', () => { }); }); - test('didSave triggers onSynthRequest for source files', () => { + test('didSave triggers onSynthRequest for source files', async () => { const synthRequests: string[] = []; const client = createTestClient({ onSynthRequest: (dir) => synthRequests.push(dir), }); - initializeClient(client, { applicationDir: '/tmp/test-project' }); + await initializeClient(client, { applicationDir: '/tmp/test-project' }); client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, @@ -148,12 +153,12 @@ describe('LSP Server', () => { expect(synthRequests).toEqual(['/tmp/test-project']); }); - test('didSave does not trigger for ignored files', () => { + test('didSave does not trigger for ignored files', async () => { const synthRequests: string[] = []; const client = createTestClient({ onSynthRequest: (dir) => synthRequests.push(dir), }); - initializeClient(client, { applicationDir: '/tmp/test-project' }); + await initializeClient(client, { applicationDir: '/tmp/test-project' }); client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/node_modules/foo/index.ts' }, @@ -165,9 +170,9 @@ describe('LSP Server', () => { expect(synthRequests).toEqual([]); }); - test('didSave does not throw without onSynthRequest configured', () => { + test('didSave does not throw without onSynthRequest configured', async () => { const client = createTestClient(); - initializeClient(client, { applicationDir: '/tmp/test-project' }); + await initializeClient(client, { applicationDir: '/tmp/test-project' }); expect(() => client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, @@ -177,19 +182,19 @@ describe('LSP Server', () => { expect(() => client.handlers.onShutdown()).not.toThrow(); }); - test('shutdown completes without error', () => { + test('shutdown completes without error', async () => { const client = createTestClient(); - initializeClient(client); + await initializeClient(client); expect(() => client.handlers.onShutdown()).not.toThrow(); }); - test('didSave is ignored after shutdown', () => { + test('didSave is ignored after shutdown', async () => { const synthRequests: string[] = []; const client = createTestClient({ onSynthRequest: (dir) => synthRequests.push(dir), }); - initializeClient(client, { applicationDir: '/tmp/test-project' }); + await initializeClient(client, { applicationDir: '/tmp/test-project' }); client.handlers.onShutdown(); @@ -200,13 +205,13 @@ describe('LSP Server', () => { expect(synthRequests).toEqual([]); }); - test('onSynthRequest errors are caught gracefully', () => { + test('onSynthRequest errors are caught gracefully', async () => { const client = createTestClient({ onSynthRequest: () => { throw new Error('synth failed'); }, }); - initializeClient(client, { applicationDir: '/tmp/test-project' }); + await initializeClient(client, { applicationDir: '/tmp/test-project' }); expect(() => client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, @@ -216,28 +221,28 @@ describe('LSP Server', () => { expect(() => client.handlers.onShutdown()).not.toThrow(); }); - test('publishes diagnostics on initialized when assembly has violations', () => { + test('publishes diagnostics on initialized when assembly has violations', async () => { const { tree, violations } = bucketViolationFixtures(); const client = createTestClient({ - readAssembly: () => ({ + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree, violations }, }), }); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); expect(client.published).toHaveLength(1); expect(client.published[0].uri).toContain('stack.ts'); expect(client.published[0].diagnostics).toHaveLength(1); }); - test('responds to codeLens with resources for the requested file', () => { + test('responds to codeLens with resources for the requested file', async () => { const stackTs = '/p/lib/stack.ts'; const stackUri = pathToFileURL(stackTs).toString(); const client = createTestClient({ - readAssembly: (): AssemblyReadResult => ({ + readAssembly: async (): Promise => ({ status: 'success', data: { warnings: [], @@ -257,7 +262,7 @@ describe('LSP Server', () => { }), }); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); const lenses = client.handlers.onCodeLens({ textDocument: { uri: stackUri }, @@ -268,63 +273,63 @@ describe('LSP Server', () => { expect(lenses[0].command?.title).toBe('Creates AWS::S3::Bucket'); }); - test('publishes nothing when assembly is not-found (pre-synth)', () => { + test('publishes nothing when assembly is not-found (pre-synth)', async () => { const client = createTestClient({ - readAssembly: () => ({ status: 'not-found' }), + readAssembly: async () => ({ status: 'not-found' }), }); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); expect(client.published).toHaveLength(0); }); - test('clears diagnostics for a violation resolved on a later refresh', () => { + test('clears diagnostics for a violation resolved on a later refresh', async () => { const client = createTestClient({ readAssembly: readAssemblyResolvingAfterFirst() }); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); expect(client.published).toHaveLength(1); const violationUri = client.published[0].uri; expect(client.published[0].diagnostics).toHaveLength(1); // Simulate a re-synth picked up by the cdk.out watcher. - client.triggerWatcher(); + await client.triggerWatcher(); expect(client.published).toHaveLength(2); expect(client.published[1]).toEqual({ uri: violationUri, diagnostics: [] }); }); - test('requests a CodeLens refresh after a refresh when the client supports it', () => { + test('requests a CodeLens refresh after a refresh when the client supports it', async () => { const client = createTestClient({ - readAssembly: (): AssemblyReadResult => ({ + readAssembly: async (): Promise => ({ status: 'success', data: { warnings: [], tree: [{ path: 'Stack1', id: 'Stack1', children: [] }] }, }), }); - initializeClient(client, { applicationDir: '/p' }, { + await initializeClient(client, { applicationDir: '/p' }, { workspace: { codeLens: { refreshSupport: true } }, }); expect(client.refreshCodeLens).toHaveBeenCalledTimes(1); }); - test('does not request a CodeLens refresh when the client lacks refreshSupport', () => { + test('does not request a CodeLens refresh when the client lacks refreshSupport', async () => { const client = createTestClient({ - readAssembly: (): AssemblyReadResult => ({ + readAssembly: async (): Promise => ({ status: 'success', data: { warnings: [], tree: [{ path: 'Stack1', id: 'Stack1', children: [] }] }, }), }); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); expect(client.refreshCodeLens).not.toHaveBeenCalled(); }); - test('a watcher-detected re-synth refreshes diagnostics and lenses', () => { + test('a watcher-detected re-synth refreshes diagnostics and lenses', async () => { const client = createTestClient({ readAssembly: readAssemblyResolvingAfterFirst() }); - initializeClient(client, { applicationDir: '/p' }, { + await initializeClient(client, { applicationDir: '/p' }, { workspace: { codeLens: { refreshSupport: true } }, }); expect(client.published).toHaveLength(1); @@ -332,30 +337,30 @@ describe('LSP Server', () => { expect(client.refreshCodeLens).toHaveBeenCalledTimes(1); // Simulate a re-synth picked up by the cdk.out watcher. - client.triggerWatcher(); + await client.triggerWatcher(); expect(client.published).toHaveLength(2); expect(client.published[1]).toEqual({ uri: violationUri, diagnostics: [] }); expect(client.refreshCodeLens).toHaveBeenCalledTimes(2); }); - test('closes the cdk.out watcher on shutdown', () => { + test('closes the cdk.out watcher on shutdown', async () => { const client = createTestClient(); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); client.handlers.onShutdown(); expect(client.watcherClosed).toHaveBeenCalledTimes(1); }); - test('onDefinition resolves a template position back to construct source', () => { + test('onDefinition resolves a template position back to construct source', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'server-def-')); const templateFile = path.join(dir, 'Stack1.template.json'); const text = JSON.stringify({ Resources: { MyBucket: { Type: 'AWS::S3::Bucket' } } }, undefined, 1); fs.writeFileSync(templateFile, text); try { const client = createTestClient({ - readAssembly: () => ({ + readAssembly: async () => ({ status: 'success', data: { tree: [{ @@ -372,7 +377,7 @@ describe('LSP Server', () => { }, }), }); - initializeClient(client, { applicationDir: dir }); + await initializeClient(client, { applicationDir: dir }); const uri = pathToFileURL(templateFile).toString(); const position = TextDocument.create(uri, 'json', 0, text).positionAt(text.indexOf('AWS::S3::Bucket')); @@ -385,9 +390,9 @@ describe('LSP Server', () => { } }); - test('onDefinition returns undefined for a non-template document', () => { + test('onDefinition returns undefined for a non-template document', async () => { const client = createTestClient(); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); const target = client.handlers.onDefinition({ textDocument: { uri: pathToFileURL('/p/lib/stack.ts').toString() }, position: { line: 0, character: 0 }, @@ -395,9 +400,9 @@ describe('LSP Server', () => { expect(target).toBeUndefined(); }); - test('onDefinition returns undefined (does not throw) for a non-file URI', () => { + test('onDefinition returns undefined (does not throw) for a non-file URI', async () => { const client = createTestClient(); - initializeClient(client, { applicationDir: '/p' }); + await initializeClient(client, { applicationDir: '/p' }); expect( client.handlers.onDefinition({ textDocument: { uri: 'untitled:Untitled-1' }, diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts index 80a4cd461..9af0008fb 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts @@ -57,11 +57,11 @@ describe('resourceTarget', () => { return undefined; }; - test('resolves a resource node to its template uri and block range', () => { + test('resolves a resource node to its template uri and block range', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], }); - const result = readAssembly(dir); + const result = await readAssembly(dir); if (result.status !== 'success') throw new Error('expected success'); const node = find(result.data.tree, 'Stack1/MyBucket/Resource')!; const templateFile = path.join(dir!, 'Stack1.template.json'); diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts index 9ad422b95..930edfa0a 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts @@ -54,6 +54,18 @@ export type ConstructNodeDecorator = ( constructPath: string, ) => T; +/** + * Async variant of {@link ConstructNodeDecorator}: the decorator may return a + * `Promise`, so it can do non-blocking I/O (for example reading source maps + * off disk with `fs.promises`) while building each node. Used by + * {@link buildConstructTreeAsync}. + */ +export type AsyncConstructNodeDecorator = ( + fields: ConstructTreeNodeFields, + stack: CloudFormationStackArtifact | undefined, + constructPath: string, +) => T | Promise; + /** * A pre-built index over a construct tree. Walks the tree once and exposes * O(1) path lookup plus pre-order iteration over every node, so callers never @@ -114,28 +126,70 @@ function visit(nodes: readonly T[], fn: (node: T) = * * The `decorate` callback turns each node's generic fields and its metadata * entries into the concrete node type (e.g. attaching a resolved source location). + * + * This is the synchronous variant: `decorate` must return `T`. Callers whose + * decoration needs non-blocking I/O should use {@link buildConstructTreeAsync}. */ export function buildConstructTree( assembly: CloudAssembly, decorate: ConstructNodeDecorator, ): T[] { + const roots = readRoots(assembly); + if (!roots) return []; + const { rawRoots, ctx } = roots; + return rawRoots.map((child) => buildNodeSync(child, ctx, undefined, NO_TEMPLATE, decorate)); +} + +/** + * Async counterpart of {@link buildConstructTree}: the `decorate` callback may + * return a `Promise`, letting it do non-blocking I/O per node (for example + * tracing source locations with `fs.promises`) without blocking the calling + * thread. Behaves identically otherwise; the tree is walked in pre-order and + * each node's children are resolved before the node is decorated. + */ +export async function buildConstructTreeAsync( + assembly: CloudAssembly, + decorate: AsyncConstructNodeDecorator, +): Promise { + const roots = readRoots(assembly); + if (!roots) return []; + const { rawRoots, ctx } = roots; + const out: T[] = []; + for (const child of rawRoots) { + out.push(await buildNodeAsync(child, ctx, undefined, NO_TEMPLATE, decorate)); + } + return out; +} + +/** + * Load tree.json and build the walk context shared by both tree builders. + * Returns the user-facing root raw nodes (cdk-internal ids filtered out) plus + * the context, or undefined when there's no tree to walk. + */ +function readRoots(assembly: CloudAssembly): WalkRoots | undefined { const treeArtifact = assembly.tree(); - if (!treeArtifact) return []; + if (!treeArtifact) return undefined; const rawTree = loadTree(path.join(assembly.directory, treeArtifact.file)); - if (!rawTree) return []; + if (!rawTree) return undefined; - const stackIndex = buildStackIndex(assembly.stacksRecursively); - const ctx: WalkContext = { stackIndex, decorate, templateCache: new Map() }; - return Object.values(rawTree.children ?? {}) - .filter((child) => !isCdkInternal(child.id)) - .map((child) => buildNode(child, ctx, undefined, NO_TEMPLATE)); + const ctx: WalkContext = { + stackIndex: buildStackIndex(assembly.stacksRecursively), + templateCache: new Map(), + }; + const rawRoots = Object.values(rawTree.children ?? {}).filter((child) => !isCdkInternal(child.id)); + return { rawRoots, ctx }; } -/** Shared state for a single {@link buildConstructTree} walk. */ -interface WalkContext { +/** The user-facing root raw nodes plus the shared walk context. */ +interface WalkRoots { + readonly rawRoots: RawTreeNode[]; + readonly ctx: WalkContext; +} + +/** Shared state for a single tree walk (decorator-independent). */ +interface WalkContext { readonly stackIndex: StackMetadataIndex; - readonly decorate: ConstructNodeDecorator; /** Parsed nested templates, cached by absolute path. */ readonly templateCache: Map; } @@ -180,6 +234,29 @@ interface StackMetadata { } type StackMetadataIndex = Map; +/** The generic fields for one node, minus its (decorated) children. */ +type NodeFieldsBase = Omit, 'children'>; + +/** A child to recurse into, paired with the template scope it inherits. */ +interface ChildInput { + readonly raw: RawTreeNode; + readonly scope: TemplateScope; +} + +/** + * Everything {@link buildNodeSync}/{@link buildNodeAsync} need for one node, + * computed once (no I/O beyond the cached template reads), so the two builders + * differ only in how they recurse and invoke `decorate`. + */ +interface PreparedNode { + readonly base: NodeFieldsBase; + /** Owning stack metadata: its `stack` is the decorate arg and the children's inheritedStack. */ + readonly owner: StackMetadata | undefined; + readonly constructPath: string; + readonly childInputs: readonly ChildInput[]; +} + +/** Reads and parses tree.json. Kept synchronous on purpose; see {@link loadTemplate}. */ function loadTree(treePath: string): RawTreeNode | undefined { if (!fs.existsSync(treePath)) return undefined; const content = JSON.parse(fs.readFileSync(treePath, 'utf-8')); @@ -199,12 +276,17 @@ function buildStackIndex(stacks: CloudFormationStackArtifact[]): StackMetadataIn return index; } -function buildNode( +/** + * Compute the generic fields, owning stack, and per-child template scopes for a + * single node. Owns all of the tree/metadata/template join logic; the sync and + * async builders share it so the substantive walk lives in exactly one place. + */ +function prepareNode( raw: RawTreeNode, - ctx: WalkContext, + ctx: WalkContext, inheritedStack: StackMetadata | undefined, inherited: TemplateScope, -): T { +): PreparedNode { const stackHere = ctx.stackIndex.get(raw.path); const owner = stackHere ?? inheritedStack; // A stack node switches to its own template via the artifact's cached getter, @@ -222,17 +304,47 @@ function buildNode( const cfnType = typeof cfnTypeRaw === 'string' ? cfnTypeRaw : undefined; // A NestedStack switches its subtree to the nested scope; other nodes inherit. - const children = Object.values(raw.children ?? {}) + const childInputs: ChildInput[] = Object.values(raw.children ?? {}) .filter((child) => !isCdkInternal(child.id)) - .map((child) => buildNode(child, ctx, owner, nestedBoundary(child, raw, owner, scope.template, ctx) ?? scope)); + .map((child) => ({ raw: child, scope: nestedBoundary(child, raw, owner, scope.template, ctx) ?? scope })); // Only CFN resources (those with a logical ID) carry a templateFile. const nodeTemplateFile = logicalId !== undefined ? scope.file : undefined; - return ctx.decorate( - { path: raw.path, id: raw.id, type: cfnType, logicalId, templateFile: nodeTemplateFile, children }, - owner?.stack, - raw.path, + return { + base: { path: raw.path, id: raw.id, type: cfnType, logicalId, templateFile: nodeTemplateFile }, + owner, + constructPath: raw.path, + childInputs, + }; +} + +function buildNodeSync( + raw: RawTreeNode, + ctx: WalkContext, + inheritedStack: StackMetadata | undefined, + inherited: TemplateScope, + decorate: ConstructNodeDecorator, +): T { + const prepared = prepareNode(raw, ctx, inheritedStack, inherited); + const children = prepared.childInputs.map( + (ci) => buildNodeSync(ci.raw, ctx, prepared.owner, ci.scope, decorate), ); + return decorate({ ...prepared.base, children }, prepared.owner?.stack, prepared.constructPath); +} + +async function buildNodeAsync( + raw: RawTreeNode, + ctx: WalkContext, + inheritedStack: StackMetadata | undefined, + inherited: TemplateScope, + decorate: AsyncConstructNodeDecorator, +): Promise { + const prepared = prepareNode(raw, ctx, inheritedStack, inherited); + const children: T[] = []; + for (const ci of prepared.childInputs) { + children.push(await buildNodeAsync(ci.raw, ctx, prepared.owner, ci.scope, decorate)); + } + return decorate({ ...prepared.base, children }, prepared.owner?.stack, prepared.constructPath); } function logicalIdFromEntries(entries: MetadataEntry[]): string | undefined { @@ -240,7 +352,14 @@ function logicalIdFromEntries(entries: MetadataEntry[]): string | undefined { return typeof entry?.data === 'string' ? entry.data : undefined; } -/** Parses a template file (cached by absolute path); undefined when missing/unparseable. */ +/** + * Parses a template file (cached by absolute path); undefined when missing/unparseable. + * + * Synchronous by design: {@link prepareNode} is shared by the sync `buildConstructTree` + * and the async `buildConstructTreeAsync`, so it cannot await. These reads are O(stacks) + * (one tree.json plus a handful of nested templates), not the per-construct hot path, so + * blocking here is acceptable. Do not switch to `fs.promises` without splitting the shared walk. + */ function loadTemplate(absPath: string, cache: Map): CfnTemplate | undefined { if (cache.has(absPath)) return cache.get(absPath); let template: CfnTemplate | undefined; @@ -265,12 +384,12 @@ function loadTemplate(absPath: string, cache: Map( +function nestedBoundary( child: RawTreeNode, parent: RawTreeNode, owner: StackMetadata | undefined, currentTemplate: CfnTemplate | undefined, - ctx: WalkContext, + ctx: WalkContext, ): TemplateScope | undefined { const resourceNode = parent.children?.[`${child.id}.NestedStack`]?.children?.[`${child.id}.NestedStackResource`]; // No sibling -> not a nested stack. A real nested stack always lives under a diff --git a/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts b/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts index 2643f746d..0bbf2eb60 100644 --- a/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts +++ b/packages/@aws-cdk/cloud-assembly-api/test/construct-tree.test.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { buildConstructTree, CloudAssembly, ConstructIndex, type ConstructTreeNode } from '../lib'; +import { buildConstructTree, buildConstructTreeAsync, CloudAssembly, ConstructIndex, type ConstructTreeNode } from '../lib'; import { rimraf } from './util'; const node = (nodePath: string, children: ConstructTreeNode[] = []): ConstructTreeNode => ({ @@ -151,6 +151,22 @@ describe('buildConstructTree', () => { const assembly = new CloudAssembly(writeAssembly({ withTree: false })); expect(buildConstructTree(assembly, (f) => f)).toEqual([]); }); + + test('buildConstructTreeAsync awaits the decorator and produces the same joined tree', async () => { + const assembly = new CloudAssembly(writeAssembly({ withTree: true })); + const tree = await buildConstructTreeAsync(assembly, async (fields, stack, constructPath) => ({ + ...fields, + stackId: stack?.id, + // Resolve through a real Promise to prove the async decorator is awaited, + // not merely returned synchronously. + decoratedPath: await Promise.resolve(constructPath), + })); + const resource = ConstructIndex.fromTree(tree).byPath('MyStack/Bucket/Resource'); + expect(resource?.type).toBe('AWS::S3::Bucket'); + expect(resource?.logicalId).toBe('BucketABC'); + expect((resource as any)?.decoratedPath).toBe('MyStack/Bucket/Resource'); + expect((resource as any)?.stackId).toBe('MyStack'); + }); }); describe('buildConstructTree -- nested stacks', () => { @@ -288,4 +304,13 @@ describe('buildConstructTree -- nested stacks', () => { const d = writeNestedAssembly({ withAssetMetadata: false }); expect(templateFileOf(d, 'MyStack/Nested/Bucket/Resource')).toBeUndefined(); }); + + test('buildConstructTreeAsync threads nested-stack scope positionally via for...await (twin logical IDs)', async () => { + // Twin logical IDs (NestedStack resets the namespace): the async for...await + // recursion must still route each bucket to its own template, like the sync walk. + const d = writeNestedAssembly({ nestedBucketLogicalId: 'ParentBucket' }); + const index = ConstructIndex.fromTree(await buildConstructTreeAsync(new CloudAssembly(d), async (f) => f)); + expect(index.byPath('MyStack/Bucket/Resource')?.templateFile).toBe(path.join(d, 'template.json')); + expect(index.byPath('MyStack/Nested/Bucket/Resource')?.templateFile).toBe(path.join(d, 'nested.template.json')); + }); }); From 6af94280a16bfef6940902f7f70297eb22e98517 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:12:38 -0400 Subject: [PATCH 11/28] fix: read templates off the LSP event loop (#1662) Convert the two synchronous template reads on the LSP request paths (onDefinition and the CodeLens provider) to fs.promises, continuing the async direction from #1631. resourceTarget, codeLensesForFile, and commandFor are now async and read sequentially, so the number of concurrent reads never grows with app size. The LspHandlers interface widens onCodeLens and onDefinition to return Promises; the connection wiring passes the Promise through unchanged. readAssembly and the source-map reads stay synchronous: they run at startup and on watch events, not per request, and convert-source-map takes a synchronous reader. Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .../@aws-cdk/cdk-explorer/lib/lsp/codelens.ts | 29 +++++++---- .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 8 +-- .../cdk-explorer/lib/lsp/template-locator.ts | 4 +- .../cdk-explorer/test/lsp/codelens.test.ts | 50 +++++++++---------- .../cdk-explorer/test/lsp/server.test.ts | 8 +-- .../test/lsp/template-locator.test.ts | 22 ++++---- 6 files changed, 65 insertions(+), 56 deletions(-) diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts index bd07de5c0..ee484f90e 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts @@ -13,17 +13,22 @@ export const OPEN_RESOURCE_COMMAND = 'cdkExplorer.openResource'; * sourceLocation matches fileUri, group by line and emit one lens per line * summarising the CFN resources produced there. */ -export function codeLensesForFile(index: ConstructIndex, fileUri: string): CodeLens[] { +export async function codeLensesForFile(index: ConstructIndex, fileUri: string): Promise { const matches = [...index] .filter((node) => isResourceOnFile(node, fileUri)) .map((node) => ({ line: node.sourceLocation.line, node })); // Multiple resources can map to one line when an L2 construct fans out - // (e.g. an L2 producing a primary resource + auxiliary resources). - return [...groupBy(matches, (m) => m.line)].map(([line, group]) => ({ - range: lineRange(line), - command: commandFor(group.map((m) => m.node)), - })); + // (e.g. an L2 producing a primary resource + auxiliary resources). Resolve + // sequentially so the number of concurrent reads never grows with app size. + const lenses: CodeLens[] = []; + for (const [line, group] of groupBy(matches, (m) => m.line)) { + lenses.push({ + range: lineRange(line), + command: await commandFor(group.map((m) => m.node)), + }); + } + return lenses; } /** @@ -42,11 +47,15 @@ interface ResourceChoice { * client opens directly (one) or via a picker (several). Unresolvable resources * are dropped; a line where none resolve stays title-only. */ -function commandFor(nodes: readonly ResourceConstruct[]): Command { +async function commandFor(nodes: readonly ResourceConstruct[]): Promise { const title = titleFor(nodes); - const choices = nodes - .map((node) => ({ label: node.type, description: friendlyName(node.path), target: resourceTarget(node) })) - .filter((choice): choice is ResourceChoice => choice.target !== undefined); + const choices: ResourceChoice[] = []; + for (const node of nodes) { + const target = await resourceTarget(node); + if (target !== undefined) { + choices.push({ label: node.type, description: friendlyName(node.path), target }); + } + } if (choices.length === 0) { return { title, command: '' }; } diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 944900099..856f59bbc 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -73,8 +73,8 @@ export interface LspHandlers { onInitialize(params: InitializeParams): InitializeResult; onInitialized(): Promise; onDidSaveTextDocument(params: DidSaveTextDocumentParams): void; - onCodeLens(params: CodeLensParams): CodeLens[]; - onDefinition(params: DefinitionParams): Location | undefined; + onCodeLens(params: CodeLensParams): Promise; + onDefinition(params: DefinitionParams): Promise; onShutdown(): void; } @@ -228,7 +228,7 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers onCodeLens(params) { return codeLensesForFile(cachedIndex, params.textDocument.uri); }, - onDefinition(params) { + async onDefinition(params) { // Only synthesized templates link back to source, and only file: URIs are // readable. Check the scheme before fileURLToPath, which throws on other // schemes (untitled:, git:, diff views). @@ -239,7 +239,7 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers const filePath = fileURLToPath(uri); let templateText: string; try { - templateText = fs.readFileSync(filePath, 'utf-8'); + templateText = await fs.promises.readFile(filePath, 'utf-8'); } catch { return undefined; } diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts index d97518ee8..b2f154253 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts @@ -23,13 +23,13 @@ export interface ResourceTarget { * Returns `undefined` when the node is not navigable: no resolved template, the * template cannot be read, it does not parse, or the logical id is absent. */ -export function resourceTarget(node: { templateFile?: string; logicalId: string }): ResourceTarget | undefined { +export async function resourceTarget(node: { templateFile?: string; logicalId: string }): Promise { if (node.templateFile === undefined) { return undefined; } let templateText: string; try { - templateText = fs.readFileSync(node.templateFile, 'utf-8'); + templateText = await fs.promises.readFile(node.templateFile, 'utf-8'); } catch { return undefined; } diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts index c33e0c597..57dc2cffc 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts @@ -24,11 +24,11 @@ interface CommandChoice { } describe('codeLensesForFile', () => { - test('returns no lenses when tree is empty', () => { - expect(codeLensesForFile(ConstructIndex.fromTree([]), URI)).toEqual([]); + test('returns no lenses when tree is empty', async () => { + expect(await codeLensesForFile(ConstructIndex.fromTree([]), URI)).toEqual([]); }); - test('returns no lenses for non-resource wrapper nodes (no logicalId)', () => { + test('returns no lenses for non-resource wrapper nodes (no logicalId)', async () => { // L2 wrapper: has sourceLocation but no logicalId/cfnType — they live on // its `Resource` child. Wrappers shouldn't get their own lens. const tree = [node({ @@ -36,10 +36,10 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, // logicalId/type intentionally omitted })]; - expect(codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); + expect(await codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); }); - test('emits one lens per resource on its source line', () => { + test('emits one lens per resource on its source line', async () => { const tree = [node({ path: 'Stack1/MyBucket/Resource', logicalId: 'MyBucketF68F3FF0', @@ -47,7 +47,7 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, })]; - const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); expect(lenses).toHaveLength(1); expect(lenses[0].range).toEqual({ start: { line: 11, character: 0 }, @@ -56,7 +56,7 @@ describe('codeLensesForFile', () => { expect(lenses[0].command?.title).toBe('Creates AWS::S3::Bucket'); }); - test('groups multiple resources on the same source line into one lens', () => { + test('groups multiple resources on the same source line into one lens', async () => { // L2 like Bucket can produce Bucket + BucketPolicy + Key, all anchored // to the same `new s3.Bucket(...)` line. One lens, listing all. const tree = [ @@ -80,12 +80,12 @@ describe('codeLensesForFile', () => { }), ]; - const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); expect(lenses).toHaveLength(1); expect(lenses[0].command?.title).toBe('Creates 3 resources: AWS::S3::Bucket, AWS::S3::BucketPolicy, AWS::KMS::Key'); }); - test('emits separate lenses for resources on different lines', () => { + test('emits separate lenses for resources on different lines', async () => { const tree = [ node({ path: 'Stack1/A', @@ -101,12 +101,12 @@ describe('codeLensesForFile', () => { }), ]; - const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); expect(lenses).toHaveLength(2); expect(lenses.map((l) => l.range.start.line).sort((a, b) => a - b)).toEqual([9, 19]); }); - test('filters out resources from other files', () => { + test('filters out resources from other files', async () => { const tree = [ node({ path: 'Stack1/A', @@ -125,15 +125,15 @@ describe('codeLensesForFile', () => { const index = ConstructIndex.fromTree(tree); // Each query returns only the resource defined in that file, which proves the // URI filter selects by file rather than returning everything for any query. - const onThisFile = codeLensesForFile(index, URI); + const onThisFile = await codeLensesForFile(index, URI); expect(onThisFile).toHaveLength(1); expect(onThisFile[0].command?.title).toBe('Creates AWS::S3::Bucket'); - const onOtherFile = codeLensesForFile(index, OTHER_URI); + const onOtherFile = await codeLensesForFile(index, OTHER_URI); expect(onOtherFile).toHaveLength(1); expect(onOtherFile[0].command?.title).toBe('Creates AWS::SQS::Queue'); }); - test('walks descendants — finds resources nested under wrappers', () => { + test('walks descendants — finds resources nested under wrappers', async () => { const tree = [ node({ path: 'Stack1', @@ -155,12 +155,12 @@ describe('codeLensesForFile', () => { }), ]; - const lenses = codeLensesForFile(ConstructIndex.fromTree(tree), URI); + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); expect(lenses).toHaveLength(1); expect(lenses[0].command?.title).toContain('AWS::S3::Bucket'); }); - test('omits resources without sourceLocation (non-TS apps)', () => { + test('omits resources without sourceLocation (non-TS apps)', async () => { const tree = [node({ path: 'Stack1/MyBucket/Resource', logicalId: 'MyBucketF68F3FF0', @@ -168,10 +168,10 @@ describe('codeLensesForFile', () => { // sourceLocation omitted — non-TS app })]; - expect(codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); + expect(await codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); }); - test('a single resource with a resolvable template gets a clickable openResource command', () => { + test('a single resource with a resolvable template gets a clickable openResource command', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codelens-')); const templateFile = path.join(dir, 'Stack1.template.json'); fs.writeFileSync(templateFile, JSON.stringify({ Resources: { MyBucketF68F3FF0: { Type: 'AWS::S3::Bucket' } } }, null, 2)); @@ -184,7 +184,7 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, })]; - const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); const choices = (lens.command!.arguments as CommandChoice[][])[0]; expect(choices).toHaveLength(1); @@ -200,7 +200,7 @@ describe('codeLensesForFile', () => { } }); - test('a multi-resource line carries all resolvable resources as choices', () => { + test('a multi-resource line carries all resolvable resources as choices', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codelens-')); const templateFile = path.join(dir, 'Stack1.template.json'); fs.writeFileSync(templateFile, JSON.stringify({ @@ -212,7 +212,7 @@ describe('codeLensesForFile', () => { node({ path: 'Stack1/B/Policy', logicalId: 'B2', type: 'AWS::S3::BucketPolicy', templateFile, sourceLocation: { file: FILE, line: 12, column: 5 } }), ]; - const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; const uri = pathToFileURL(templateFile).toString(); expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); const choices = (lens.command!.arguments as CommandChoice[][])[0]; @@ -229,18 +229,18 @@ describe('codeLensesForFile', () => { } }); - test('a multi-resource line with no resolvable templates stays title-only', () => { + test('a multi-resource line with no resolvable templates stays title-only', async () => { const tree = [ node({ path: 'Stack1/B/Resource', logicalId: 'B1', type: 'AWS::S3::Bucket', templateFile: '/no/such.json', sourceLocation: { file: FILE, line: 12, column: 5 } }), node({ path: 'Stack1/B/Policy', logicalId: 'B2', type: 'AWS::S3::BucketPolicy', templateFile: '/no/such.json', sourceLocation: { file: FILE, line: 12, column: 5 } }), ]; - const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; expect(lens.command?.command).toBe(''); expect(lens.command?.arguments).toBeUndefined(); }); - test('a single resource whose id is missing from the template degrades to title-only', () => { + test('a single resource whose id is missing from the template degrades to title-only', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codelens-')); const templateFile = path.join(dir, 'Stack1.template.json'); fs.writeFileSync(templateFile, JSON.stringify({ Resources: { SomethingElse: { Type: 'AWS::S3::Bucket' } } }, null, 2)); @@ -253,7 +253,7 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, })]; - const lens = codeLensesForFile(ConstructIndex.fromTree(tree), URI)[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; expect(lens.command?.command).toBe(''); } finally { fs.rmSync(dir, { recursive: true, force: true }); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index 2abbd3dc7..a5c7c568f 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -264,7 +264,7 @@ describe('LSP Server', () => { await initializeClient(client, { applicationDir: '/p' }); - const lenses = client.handlers.onCodeLens({ + const lenses = await client.handlers.onCodeLens({ textDocument: { uri: stackUri }, }); @@ -381,7 +381,7 @@ describe('LSP Server', () => { const uri = pathToFileURL(templateFile).toString(); const position = TextDocument.create(uri, 'json', 0, text).positionAt(text.indexOf('AWS::S3::Bucket')); - const target = client.handlers.onDefinition({ textDocument: { uri }, position }); + const target = await client.handlers.onDefinition({ textDocument: { uri }, position }); expect(target?.uri).toBe(pathToFileURL('/p/lib/stack.ts').toString()); expect(target?.range.start).toEqual({ line: 4, character: 2 }); // 1-based (5,3) -> 0-based @@ -393,7 +393,7 @@ describe('LSP Server', () => { test('onDefinition returns undefined for a non-template document', async () => { const client = createTestClient(); await initializeClient(client, { applicationDir: '/p' }); - const target = client.handlers.onDefinition({ + const target = await client.handlers.onDefinition({ textDocument: { uri: pathToFileURL('/p/lib/stack.ts').toString() }, position: { line: 0, character: 0 }, }); @@ -404,7 +404,7 @@ describe('LSP Server', () => { const client = createTestClient(); await initializeClient(client, { applicationDir: '/p' }); expect( - client.handlers.onDefinition({ + await client.handlers.onDefinition({ textDocument: { uri: 'untitled:Untitled-1' }, position: { line: 0, character: 0 }, }), diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts index 9af0008fb..ef6b6cf4e 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/template-locator.test.ts @@ -66,7 +66,7 @@ describe('resourceTarget', () => { const node = find(result.data.tree, 'Stack1/MyBucket/Resource')!; const templateFile = path.join(dir!, 'Stack1.template.json'); - const target = resourceTarget(node)!; + const target = (await resourceTarget(node))!; expect(target.uri).toBe(pathToFileURL(templateFile).toString()); // The range spans the resource block (not a zero-width cursor) and re-parses to it. const text = fs.readFileSync(templateFile, 'utf-8'); @@ -74,30 +74,30 @@ describe('resourceTarget', () => { expect(sliceParse(text, target.range)).toEqual(JSON.parse(text).Resources.MyBucketF68F3FF0); }); - test('returns undefined for a node without a resolved templateFile', () => { - expect(resourceTarget({ templateFile: undefined, logicalId: 'MyBucketF68F3FF0' })).toBeUndefined(); + test('returns undefined for a node without a resolved templateFile', async () => { + expect(await resourceTarget({ templateFile: undefined, logicalId: 'MyBucketF68F3FF0' })).toBeUndefined(); }); - test('returns undefined when the logical id is not in the resolved template', () => { + test('returns undefined when the logical id is not in the resolved template', async () => { dir = buildFlatAssembly({ stacks: [{ id: 'Stack1', resources: [{ id: 'MyBucket', logicalId: 'MyBucketF68F3FF0', cfnType: 'AWS::S3::Bucket' }] }], }); - expect(resourceTarget({ templateFile: path.join(dir, 'Stack1.template.json'), logicalId: 'GhostId' })).toBeUndefined(); + expect(await resourceTarget({ templateFile: path.join(dir, 'Stack1.template.json'), logicalId: 'GhostId' })).toBeUndefined(); }); - test('returns undefined (does not throw) when the template can no longer be read', () => { - expect(resourceTarget({ templateFile: '/no/such/template.json', logicalId: 'MyBucketF68F3FF0' })).toBeUndefined(); + test('returns undefined (does not throw) when the template can no longer be read', async () => { + expect(await resourceTarget({ templateFile: '/no/such/template.json', logicalId: 'MyBucketF68F3FF0' })).toBeUndefined(); }); - test('resolves the definition block, not Ref/DependsOn occurrences of the same id', () => { + test('resolves the definition block, not Ref/DependsOn occurrences of the same id', async () => { const written = writeTemplate(TEMPLATE); dir = written.dir; // MyBucketF68F3FF0 is defined once and also referenced; the block must be the definition. - const target = resourceTarget({ templateFile: written.file, logicalId: 'MyBucketF68F3FF0' })!; + const target = (await resourceTarget({ templateFile: written.file, logicalId: 'MyBucketF68F3FF0' }))!; expect(sliceParse(TEMPLATE, target.range)).toEqual({ Type: 'AWS::S3::Bucket' }); }); - test('does not match a logical id that is a prefix of a longer key', () => { + test('does not match a logical id that is a prefix of a longer key', async () => { const contents = JSON.stringify( { Resources: { @@ -111,7 +111,7 @@ describe('resourceTarget', () => { const written = writeTemplate(contents); dir = written.dir; // The shorter id must resolve to its own (distinct) block, not the longer-named sibling. - const target = resourceTarget({ templateFile: written.file, logicalId: 'MyBucket' })!; + const target = (await resourceTarget({ templateFile: written.file, logicalId: 'MyBucket' }))!; expect(sliceParse(contents, target.range)).toEqual({ Type: 'AWS::S3::Bucket', Properties: { BucketName: 'short' } }); }); }); From 3afed4c355631a614c5f27db2043c4fb81e09679 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:19:36 -0400 Subject: [PATCH 12/28] feat: support source tracing for python (#1674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add utilities to source resolver to parse jsii host-language creation-stack frames for Python, and drop the TypeScript-only diagnostics gate. Go-to-definition (source → template, and reverse) and policy-violation diagnostics now work for Python apps, not just TypeScript. Requires synthing with `JSII_HOST_STACK_TRACES=1` (opt-in; jsii 1.137+ / aws-cdk-lib 2.260+). Auto-enabling it for LSP-triggered synth is a follow-up. Java support separated into new PR due to FQN path parsing complexities. Example: Screenshot 2026-06-25 at 1 30 10 PM ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Otavio Macedo <288203+otaviomacedo@users.noreply.github.com> --- .../cdk-explorer/lib/core/source-resolver.ts | 42 ++++++++++++------- .../cdk-explorer/lib/lsp/diagnostics.ts | 12 ++---- .../cdk-explorer/lib/lsp/template-locator.ts | 2 +- .../test/core/source-resolver.test.ts | 42 ++++++++++++++++++- .../cdk-explorer/test/lsp/diagnostics.test.ts | 11 +++-- 5 files changed, 77 insertions(+), 32 deletions(-) diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts b/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts index 890e19bd9..12b1f378e 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/source-resolver.ts @@ -44,24 +44,23 @@ export class SourceMapResolver { /** * Resolve the user source location from a creation stack trace (the frames * produced by toolkit-lib's findCreationStackTrace). Returns undefined when - * there's no trace (non-TS apps) or every frame is a skip-placeholder - * (framework-only call sites). + * there's no trace or no frame resolves to an in-root user source file. */ public async resolveFrames(frames: readonly string[] | undefined): Promise { if (!frames) return undefined; - // aws-cdk-lib's renderCallStackJustMyCode (in node_modules/aws-cdk-lib/core/ - // lib/stack-trace.js) pre-filters node_modules/node:internal frames into - // skip-placeholder lines. Those don't match FRAME_RE, so the first frame - // that parses IS the user call site. + // First in-root, supported-source frame wins. Host-language traces carry + // framework .js frames (outside the root) ahead of the user's .py + // frame, so skip past them rather than stop at the first parsed frame. for (const frame of frames) { const parsed = parseFrame(frame); if (!parsed) continue; - if (!isSupportedSourceFile(parsed.file)) return undefined; + const kind = sourceKind(parsed.file); + if (!kind) continue; // The frame's file path is assembly-derived and attacker-influenceable. // Never read or surface a location outside the project. - if (!(await isWithinRoot(this.projectRoot, parsed.file))) return undefined; - return this.mapJsToOriginalSource(parsed); + if (!(await isWithinRoot(this.projectRoot, parsed.file))) continue; + return kind === 'host' ? normalizeHostFrame(parsed) : this.mapJsToOriginalSource(parsed); } return undefined; } @@ -140,21 +139,32 @@ export class SourceMapResolver { } } } -const SUPPORTED_SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js'] as const; +// TypeScript/JavaScript frames go through source-map resolution (.js -> .ts). +const TS_JS_EXTENSIONS = ['.ts', '.tsx', '.js'] as const; +// jsii host-language frames already point at user source (no source map needed). +const HOST_LANGUAGE_EXTENSIONS = ['.py'] as const; + +function sourceKind(file: string): 'tsjs' | 'host' | undefined { + if (TS_JS_EXTENSIONS.some((ext) => file.endsWith(ext))) return 'tsjs'; + if (HOST_LANGUAGE_EXTENSIONS.some((ext) => file.endsWith(ext))) return 'host'; + return undefined; +} -function isSupportedSourceFile(file: string): boolean { - return SUPPORTED_SOURCE_EXTENSIONS.some((ext) => file.endsWith(ext)); +// Treat the host column as 1-based; jsii sends 0 when unavailable, so clamp 0 to line start. +function normalizeHostFrame(loc: SourceLocation): SourceLocation { + return { file: loc.file, line: loc.line, column: Math.max(1, loc.column) }; } -// renderCallStackJustMyCode emits frames as " at (::)". -// Anchoring on "(" avoids capturing the leading "at " into the file group. -const FRAME_RE = /\(([^()\s][^()]*?):(\d+):(\d+)\)\s*$/; +// " (:[:])"; host frames omit the column when +// unavailable, so it's optional. Anchoring on "(" avoids a leading "at ". +const FRAME_RE = /\(([^()\s][^()]*?):(\d+)(?::(\d+))?\)\s*$/; function parseFrame(frame: string): SourceLocation | undefined { const m = FRAME_RE.exec(frame); if (!m) return undefined; const line = Number(m[2]); - const column = Number(m[3]); + // Host frames may omit the column; treat absent as 0 (unavailable). + const column = m[3] !== undefined ? Number(m[3]) : 0; if (!Number.isFinite(line) || !Number.isFinite(column)) return undefined; return { file: m[1], line, column }; } diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts index adfdb2350..98c30e54c 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/diagnostics.ts @@ -27,7 +27,7 @@ export interface MapViolationsResult { /** * Convert a validation report into LSP diagnostics keyed by file URI. - * Violations we can't anchor to a TypeScript source location are dropped + * Violations we can't anchor to a source location are dropped * (with a reason) rather than thrown. */ export function mapViolationsToDiagnostics( @@ -77,7 +77,7 @@ function resolveLocation( if (!constructPath) return { reason: 'violation has no construct path' }; const node = index.byPath(constructPath); if (!node) return { reason: 'not found in the construct tree' }; - if (!node.sourceLocation) return { reason: 'no source location (non-TypeScript app or framework-only trace)' }; + if (!node.sourceLocation) return { reason: 'no source location (framework-only trace or unresolved source)' }; return node.sourceLocation; } @@ -89,16 +89,10 @@ function anchorViolation( const loc = resolveLocation(constructPath, index); if ('reason' in loc) return loc; - // We only surface diagnostics for TypeScript sources for now. - if (!isTypeScript(loc.file)) return { reason: `source file is not TypeScript: ${loc.file}` }; - + // Resolver already vetted the source file; just turn the location into a range. return { uri: pathToFileURL(loc.file).toString(), range: toRange(loc) }; } -function isTypeScript(file: string): boolean { - return file.endsWith('.ts') || file.endsWith('.tsx'); -} - /** * LSP range for a source location. Anchors at the resolved line/column, or at * the top of the file when the file is known but the line/column aren't. LSP diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts index b2f154253..9b9d7863a 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/template-locator.ts @@ -51,7 +51,7 @@ export async function resourceTarget(node: { templateFile?: string; logicalId: s * `index` (matched by both `templateFile` and `logicalId`, since logical ids are * only unique within a template), and returns its source location as a zero-width * range. Undefined when the offset is not inside a resource, no construct owns it, - * or the construct has no source location (for example a non-TypeScript app). + * or the construct has no source location (for example a framework-only trace). */ export function sourceTargetAtTemplateOffset( index: ConstructIndex, diff --git a/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts index 0709cf281..a7659322e 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts @@ -66,8 +66,46 @@ describe('SourceMapResolver.resolveFrames', () => { }); }); - test('returns undefined for a non-TypeScript (host-language) frame', async () => { - expect(await resolver.resolveFrames([' at (/project/app/my_stack.py:42:5)'])).toBeUndefined(); + test('resolves a Python host frame in the real released format (placeholders + no-column .py)', async () => { + // Verbatim shape from aws-cdk-lib 2.260.0 / jsii 1.137.0: kernel frames are + // collapsed to non-parsing placeholders, and host frames carry no column + // (jsii's Python runtime sends column 0, which formatExternalFrame omits). + const frames = [ + '...jsii runtime, node internals...', + '(no user code in 10 frames, use --stack-trace-limit to capture more)', + '__init__ (/project/stacks/network_stack.py:42)', + ' (/project/app.py:10)', + ]; + // No column -> 1-based start of line. + expect(await resolver.resolveFrames(frames)).toEqual({ + file: '/project/stacks/network_stack.py', + line: 42, + column: 1, + }); + }); + + test('skips out-of-root .js frames to reach the first in-root .py frame', async () => { + // Robustness: if raw jsii/aws-cdk-lib .js frames appear (outside the root) + // ahead of the user frame, skip them rather than give up. + const frames = [ + 'Kernel._Kernel_create (/private/var/folders/qw/T/tmpiqvf63c1/lib/program.js:1:61273)', + '__init__ (/project/stacks/network_stack.py:42)', + ]; + expect(await resolver.resolveFrames(frames)).toEqual({ + file: '/project/stacks/network_stack.py', line: 42, column: 1, + }); + }); + + test('preserves an explicit (non-zero) host column', async () => { + // A real host column is already 1-based, so it is kept as-is. Only the 0 + // "unavailable" sentinel is clamped to the start of the line. + expect(await resolver.resolveFrames(['f (/project/stacks/data_stack.py:31:8)'])).toEqual({ + file: '/project/stacks/data_stack.py', line: 31, column: 8, + }); + }); + + test('drops a host frame whose file escapes the project root', async () => { + expect(await resolver.resolveFrames([' (/etc/evil.py:1)'])).toBeUndefined(); }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts index 27ae54670..108c6a8fe 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/diagnostics.test.ts @@ -156,13 +156,16 @@ describe('mapViolationsToDiagnostics', () => { expect(dropped[0].reason).toContain('no source location'); }); - test('drops violations whose source file is not TypeScript', () => { - const tree = indexOf(nodeWithLocation('S/X', '/p/lib/x.py', 1, 1)); + test('anchors a violation to a Python (host-language) source file', () => { + const tree = indexOf(nodeWithLocation('S/X', '/p/stacks/x.py', 31, 1)); const report = reportWith({ ruleName: 'r', severity: 'error', paths: ['S/X'] }); const { byUri, dropped } = mapViolationsToDiagnostics(report, tree); - expect(byUri.size).toBe(0); - expect(dropped[0].reason).toContain('not TypeScript'); + expect(dropped).toHaveLength(0); + expect(byUri.size).toBe(1); + const [uri, diags] = [...byUri.entries()][0]; + expect(uri).toContain('x.py'); + expect(diags[0].range.start).toEqual({ line: 30, character: 0 }); }); test('anchors at the top of the file when line/column are unknown', () => { From 868d0097c0833d18b20ac659341463e9eb41ce07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:52:58 +0000 Subject: [PATCH 13/28] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/dependabot.yml | 1 + packages/@aws-cdk/cdk-explorer/package.json | 4 +- yarn.lock | 465 +++++++++++++++++++- 3 files changed, 465 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d686bd02e..c6369c243 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,6 +13,7 @@ updates: ignore: - dependency-name: "@aws-cdk-testing/cli-integ" - dependency-name: "@aws-cdk/cdk-assets-lib" + - dependency-name: "@aws-cdk/cdk-explorer" - dependency-name: "@aws-cdk/cli-plugin-contract" - dependency-name: "@aws-cdk/cloud-assembly-api" - dependency-name: "@aws-cdk/cloud-assembly-schema" diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index c1569f19b..b206944fc 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -30,7 +30,7 @@ "organization": true }, "devDependencies": { - "@cdklabs/eslint-plugin": "^2.0.8", + "@cdklabs/eslint-plugin": "^2.0.9", "@stylistic/eslint-plugin": "^3", "@types/convert-source-map": "^2", "@types/express": "^4", @@ -50,7 +50,7 @@ "jest-junit": "^16", "nx": "^22.7.5", "prettier": "^2.8", - "projen": "^0.99.70", + "projen": "^0.99.73", "ts-jest": "^29.4.11", "typescript": "5.9", "vscode-languageserver-protocol": "^3" diff --git a/yarn.lock b/yarn.lock index 94f62f6c6..e748bff1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -159,6 +159,47 @@ __metadata: languageName: unknown linkType: soft +"@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer": + version: 0.0.0-use.local + resolution: "@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer" + dependencies: + "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" + "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" + "@aws-cdk/toolkit-lib": "npm:^0.0.0" + "@cdklabs/eslint-plugin": "npm:^2.0.8" + "@jridgewell/trace-mapping": "npm:^0.3" + "@stylistic/eslint-plugin": "npm:^3" + "@types/convert-source-map": "npm:^2" + "@types/express": "npm:^4" + "@types/jest": "npm:^29.5.14" + "@types/node": "npm:^20" + "@typescript-eslint/eslint-plugin": "npm:^8" + "@typescript-eslint/parser": "npm:^8" + chokidar: "npm:^4" + constructs: "npm:^10.0.0" + convert-source-map: "npm:^2" + eslint: "npm:^9" + eslint-config-prettier: "npm:^10.1.8" + eslint-import-resolver-typescript: "npm:^4.4.5" + eslint-plugin-import: "npm:^2.32.0" + eslint-plugin-jest: "npm:^29.15.2" + eslint-plugin-jsdoc: "npm:^62.9.0" + eslint-plugin-prettier: "npm:^4.2.5" + express: "npm:^4" + jest: "npm:^29.7.0" + jest-junit: "npm:^16" + nx: "npm:^22.7.5" + prettier: "npm:^2.8" + projen: "npm:^0.99.70" + ts-jest: "npm:^29.4.11" + typescript: "npm:5.9" + vscode-jsonrpc: "npm:^8" + vscode-languageserver: "npm:^9" + vscode-languageserver-protocol: "npm:^3" + vscode-languageserver-textdocument: "npm:^1" + languageName: unknown + linkType: soft + "@aws-cdk/cli-plugin-contract@npm:^0.0.0, @aws-cdk/cli-plugin-contract@workspace:packages/@aws-cdk/cli-plugin-contract": version: 0.0.0-use.local resolution: "@aws-cdk/cli-plugin-contract@workspace:packages/@aws-cdk/cli-plugin-contract" @@ -197,6 +238,7 @@ __metadata: "@cdklabs/eslint-plugin": "npm:^2.0.9" "@stylistic/eslint-plugin": "npm:^3" "@types/jest": "npm:^29.5.14" + "@types/json-source-map": "npm:^0.6.0" "@types/node": "npm:^20" "@typescript-eslint/eslint-plugin": "npm:^8" "@typescript-eslint/parser": "npm:^8" @@ -211,6 +253,7 @@ __metadata: eslint-plugin-prettier: "npm:^4.2.5" jest: "npm:^29.7.0" jest-junit: "npm:^16" + json-source-map: "npm:^0.6.1" jsonschema: "npm:^1.5.0" license-checker: "npm:^25.0.1" nx: "npm:^22.7.5" @@ -3923,6 +3966,19 @@ __metadata: languageName: node linkType: hard +"@cdklabs/eslint-plugin@npm:^2.0.8": + version: 2.0.11 + resolution: "@cdklabs/eslint-plugin@npm:2.0.11" + dependencies: + "@typescript-eslint/utils": "npm:^8.61.1" + fs-extra: "npm:^11.3.5" + typescript: "npm:^5.9.3" + peerDependencies: + eslint: ">=9 <11" + checksum: 10c0/750759403005775105b75fc09de0812a19b4b620fc19102bd2dc0f082641651fc9dbdf95fc79efa9f28783361092a3014e85289638b6eb658b83ad128df44b7d + languageName: node + linkType: hard + "@cdklabs/eslint-plugin@npm:^2.0.9": version: 2.0.9 resolution: "@cdklabs/eslint-plugin@npm:2.0.9" @@ -4819,7 +4875,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3, @jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -6637,6 +6693,16 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:*": + version: 1.19.6 + resolution: "@types/body-parser@npm:1.19.6" + dependencies: + "@types/connect": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/542da05c924dce58ee23f50a8b981fee36921850c82222e384931fda3e106f750f7880c47be665217d72dbe445129049db6eb1f44e7a06b09d62af8f3cca8ea7 + languageName: node + linkType: hard + "@types/chai@npm:^5.2.2": version: 5.2.3 resolution: "@types/chai@npm:5.2.3" @@ -6647,6 +6713,22 @@ __metadata: languageName: node linkType: hard +"@types/connect@npm:*": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + +"@types/convert-source-map@npm:^2": + version: 2.0.3 + resolution: "@types/convert-source-map@npm:2.0.3" + checksum: 10c0/43dd8ccad61489c245342220db74c1baf3b75586074f99609943fe1bdecf7d5dcff0acd038cb0063dd7533a90cc980101d5899afa70a638883752ad8d66de20b + languageName: node + linkType: hard + "@types/cors@npm:^2.8.6": version: 2.8.19 resolution: "@types/cors@npm:2.8.19" @@ -6670,6 +6752,30 @@ __metadata: languageName: node linkType: hard +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.19.8 + resolution: "@types/express-serve-static-core@npm:4.19.8" + dependencies: + "@types/node": "npm:*" + "@types/qs": "npm:*" + "@types/range-parser": "npm:*" + "@types/send": "npm:*" + checksum: 10c0/6fb58a85b209e0e421b29c52e0a51dbf7c039b711c604cf45d46470937a5c7c16b30aa5ce9bf7da0bd8a2e9361c95b5055599c0500a96bf4414d26c81f02d7fe + languageName: node + linkType: hard + +"@types/express@npm:^4": + version: 4.17.25 + resolution: "@types/express@npm:4.17.25" + dependencies: + "@types/body-parser": "npm:*" + "@types/express-serve-static-core": "npm:^4.17.33" + "@types/qs": "npm:*" + "@types/serve-static": "npm:^1" + checksum: 10c0/f42b616d2c9dbc50352c820db7de182f64ebbfa8dba6fb6c98e5f8f0e2ef3edde0131719d9dc6874803d25ad9ca2d53471d0fec2fbc60a6003a43d015bab72c4 + languageName: node + linkType: hard + "@types/fs-extra@npm:^11": version: 11.0.4 resolution: "@types/fs-extra@npm:11.0.4" @@ -6689,6 +6795,13 @@ __metadata: languageName: node linkType: hard +"@types/http-errors@npm:*": + version: 2.0.5 + resolution: "@types/http-errors@npm:2.0.5" + checksum: 10c0/00f8140fbc504f47356512bd88e1910c2f07e04233d99c88c854b3600ce0523c8cd0ba7d1897667243282eb44c59abb9245959e2428b9de004f93937f52f7c15 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -6750,6 +6863,13 @@ __metadata: languageName: node linkType: hard +"@types/json-source-map@npm:^0.6.0": + version: 0.6.0 + resolution: "@types/json-source-map@npm:0.6.0" + checksum: 10c0/b7db2f52735acc70b70bf49b1e945f4749ac5c9c8429c44ad23c751193f89d030400ed966c416540647745414cd450f35795395bf0be9eae94816bbe03ad0a2f + languageName: node + linkType: hard + "@types/json5@npm:^0.0.29": version: 0.0.29 resolution: "@types/json5@npm:0.0.29" @@ -6782,6 +6902,13 @@ __metadata: languageName: node linkType: hard +"@types/mime@npm:^1": + version: 1.3.5 + resolution: "@types/mime@npm:1.3.5" + checksum: 10c0/c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc + languageName: node + linkType: hard + "@types/mime@npm:^2": version: 2.0.3 resolution: "@types/mime@npm:2.0.3" @@ -6869,6 +6996,20 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:*": + version: 6.15.1 + resolution: "@types/qs@npm:6.15.1" + checksum: 10c0/1dfdbcb4cf2a8f66d57f0b9a9fe6b1c7091cb816687b6698c1351eaf31f62e412cea9b7453a9637b570cd5fad8dced527e5a9e69b4fcc6e318daacd8b749f094 + languageName: node + linkType: hard + +"@types/range-parser@npm:*": + version: 1.2.7 + resolution: "@types/range-parser@npm:1.2.7" + checksum: 10c0/361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c + languageName: node + linkType: hard + "@types/semver@npm:^7, @types/semver@npm:^7.7.1": version: 7.7.1 resolution: "@types/semver@npm:7.7.1" @@ -6876,6 +7017,36 @@ __metadata: languageName: node linkType: hard +"@types/send@npm:*": + version: 1.2.1 + resolution: "@types/send@npm:1.2.1" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/7673747f8c2d8e67f3b1b3b57e9d4d681801a4f7b526ecf09987bb9a84a61cf94aa411c736183884dc762c1c402a61681eb1ef200d8d45d7e5ec0ab67ea5f6c1 + languageName: node + linkType: hard + +"@types/send@npm:<1": + version: 0.17.6 + resolution: "@types/send@npm:0.17.6" + dependencies: + "@types/mime": "npm:^1" + "@types/node": "npm:*" + checksum: 10c0/a9d76797f0637738062f1b974e0fcf3d396a28c5dc18c3f95ecec5dabda82e223afbc2d56a0bca46b6326fd7bb229979916cea40de2270a98128fd94441b87c2 + languageName: node + linkType: hard + +"@types/serve-static@npm:^1": + version: 1.15.10 + resolution: "@types/serve-static@npm:1.15.10" + dependencies: + "@types/http-errors": "npm:*" + "@types/node": "npm:*" + "@types/send": "npm:<1" + checksum: 10c0/842fca14c9e80468f89b6cea361773f2dcd685d4616a9f59013b55e1e83f536e4c93d6d8e3ba5072d40c4e7e64085210edd6646b15d538ded94512940a23021f + languageName: node + linkType: hard + "@types/sinon@npm:^17.0.3, @types/sinon@npm:^17.0.4": version: 17.0.4 resolution: "@types/sinon@npm:17.0.4" @@ -7038,6 +7209,19 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/project-service@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/project-service@npm:8.62.1" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.62.1" + "@typescript-eslint/types": "npm:^8.62.1" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/48af1ede4f491a8d499548ec82287e1fab0ea8ac48952a3be5b14c082a679d5e3b82d11a091ecde7ae1b327cb2d02e93255aa880fd86691392c6738b18baf38f + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.58.1": version: 8.58.1 resolution: "@typescript-eslint/scope-manager@npm:8.58.1" @@ -7058,6 +7242,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/scope-manager@npm:8.62.1" + dependencies: + "@typescript-eslint/types": "npm:8.62.1" + "@typescript-eslint/visitor-keys": "npm:8.62.1" + checksum: 10c0/94cf3724b2fe2f068f357011efd3e42536e5431ee15dcf6dc80f36c842554f2de9a2f185bb3316a38a6e78a07206ffe16b723eb349157f35f820ba1ffe90830b + languageName: node + linkType: hard + "@typescript-eslint/tsconfig-utils@npm:8.58.1, @typescript-eslint/tsconfig-utils@npm:^8.58.1": version: 8.58.1 resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.1" @@ -7076,6 +7270,15 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/tsconfig-utils@npm:8.62.1, @typescript-eslint/tsconfig-utils@npm:^8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.62.1" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/42fb0efb857a6bc6a5b3fc654a2712e4106a75ae7ceb143754fba084f6a4d561adbf666fc334f3aabe6089adcd9a3a896c0009e08f1ef8447bf07900ada3ab66 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.58.1": version: 8.58.1 resolution: "@typescript-eslint/type-utils@npm:8.58.1" @@ -7113,6 +7316,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.62.1, @typescript-eslint/types@npm:^8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/types@npm:8.62.1" + checksum: 10c0/dfa1c9ef016c867a57d3fbef89f7968f3135d8d4621de1a8d2818258f79ed61f26fb6a3381ef27dde0a880817bfd5883f642f03a027dc127d771007022d1d880 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.58.1": version: 8.58.1 resolution: "@typescript-eslint/typescript-estree@npm:8.58.1" @@ -7151,6 +7361,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.62.1" + dependencies: + "@typescript-eslint/project-service": "npm:8.62.1" + "@typescript-eslint/tsconfig-utils": "npm:8.62.1" + "@typescript-eslint/types": "npm:8.62.1" + "@typescript-eslint/visitor-keys": "npm:8.62.1" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/e2f554a5a683ded80e010221de46229134717eb7b5ea41cf704b9b702c6e56a93aabc784cafa93fdbc4293f79c0adc1d15ac2915a489be8dee4e9434ab9975ff + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:^4.33.0": version: 4.33.0 resolution: "@typescript-eslint/typescript-estree@npm:4.33.0" @@ -7199,6 +7428,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^8.61.1": + version: 8.62.1 + resolution: "@typescript-eslint/utils@npm:8.62.1" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.62.1" + "@typescript-eslint/types": "npm:8.62.1" + "@typescript-eslint/typescript-estree": "npm:8.62.1" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/c8970c25049bd4c562c0780f96c1ed50082103ecfc441ccc5be7afd1632883c97e258a6482b6e5998a2a51c3bd4dba6fd066709122108a3b71bc5433cd2fa7c8 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/visitor-keys@npm:4.33.0" @@ -7229,6 +7473,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.62.1": + version: 8.62.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.62.1" + dependencies: + "@typescript-eslint/types": "npm:8.62.1" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/2ab6659c197280b5329be392f4bf3b69ac951e5ab3cc645d05d470381de7f017456938285693b2505f7c02ca56eadc734d55af85c8dee4173b11823c34e7d53c + languageName: node + linkType: hard + "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1": version: 1.11.1 resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1" @@ -8363,6 +8617,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:~1.20.5": + version: 1.20.5 + resolution: "body-parser@npm:1.20.5" + dependencies: + bytes: "npm:~3.1.2" + content-type: "npm:~1.0.5" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:~1.2.0" + http-errors: "npm:~2.0.1" + iconv-lite: "npm:~0.4.24" + on-finished: "npm:~2.4.1" + qs: "npm:~6.15.1" + raw-body: "npm:~2.5.3" + type-is: "npm:~1.6.18" + unpipe: "npm:~1.0.0" + checksum: 10c0/ad777ca5e4711eae253c93f50fdc4608c60b76a9710d79e5e5b84581c76691e6ad21ecc9158986d9ea2b365df73e403ca33c27a8bccc1a7cfc2ccc248548118d + languageName: node + linkType: hard + "bowser@npm:^2.11.0": version: 2.14.1 resolution: "bowser@npm:2.14.1" @@ -9272,7 +9546,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^2.0.0": +"convert-source-map@npm:^2, convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b @@ -9451,6 +9725,15 @@ __metadata: languageName: node linkType: hard +"dax@npm:^0.48.3": + version: 0.48.3 + resolution: "dax@npm:0.48.3" + dependencies: + undici-types: "npm:^5.26" + checksum: 10c0/6db49de2d94c5951050175035141a5a53288d5b2526f4c6a143fa5c981feb0b85194d12d66636b977d8676d8764d2674e4029a454d4cf12f3d2b031332763eae + languageName: node + linkType: hard + "debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -10720,6 +11003,45 @@ __metadata: languageName: node linkType: hard +"express@npm:^4": + version: 4.22.2 + resolution: "express@npm:4.22.2" + dependencies: + accepts: "npm:~1.3.8" + array-flatten: "npm:1.1.1" + body-parser: "npm:~1.20.5" + content-disposition: "npm:~0.5.4" + content-type: "npm:~1.0.4" + cookie: "npm:~0.7.1" + cookie-signature: "npm:~1.0.6" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + encodeurl: "npm:~2.0.0" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + finalhandler: "npm:~1.3.1" + fresh: "npm:~0.5.2" + http-errors: "npm:~2.0.0" + merge-descriptors: "npm:1.0.3" + methods: "npm:~1.1.2" + on-finished: "npm:~2.4.1" + parseurl: "npm:~1.3.3" + path-to-regexp: "npm:~0.1.12" + proxy-addr: "npm:~2.0.7" + qs: "npm:~6.15.1" + range-parser: "npm:~1.2.1" + safe-buffer: "npm:5.2.1" + send: "npm:~0.19.0" + serve-static: "npm:~1.16.2" + setprototypeof: "npm:1.2.0" + statuses: "npm:~2.0.1" + type-is: "npm:~1.6.18" + utils-merge: "npm:1.0.1" + vary: "npm:~1.1.2" + checksum: 10c0/d06dd4379fd217440b30f8abbe45f0e74931114c1395034f03e7d635196ecdab530d4835a1962a6aa34838d61967dc6f1f77846999bba3032373e9e714222c44 + languageName: node + linkType: hard + "express@npm:^4.14.0": version: 4.22.1 resolution: "express@npm:4.22.1" @@ -13242,6 +13564,13 @@ __metadata: languageName: node linkType: hard +"json-source-map@npm:^0.6.1": + version: 0.6.1 + resolution: "json-source-map@npm:0.6.1" + checksum: 10c0/9fe819f7dfc1407caf8e36246f18376eb511b969954d457b251dfb506df74544cefc0c368f64474b512956eeb37807e03045332a9712ddd14b836d54debe03c3 + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -15723,6 +16052,33 @@ __metadata: languageName: node linkType: hard +"projen@npm:^0.99.70": + version: 0.99.80 + resolution: "projen@npm:0.99.80" + dependencies: + "@iarna/toml": "npm:^2.2.5" + case: "npm:^1.6.3" + chalk: "npm:^4.1.2" + comment-json: "npm:4.2.2" + constructs: "npm:^10.5.0" + conventional-changelog-config-spec: "npm:^2.1.0" + dax: "npm:^0.48.3" + fast-glob: "npm:^3.3.3" + fast-json-patch: "npm:^3.1.1" + ini: "npm:^2.0.0" + parse-conflict-json: "npm:^4.0.0" + semver: "npm:^7.8.4" + xmlbuilder2: "npm:^4.0.3" + yaml: "npm:^2.2.2" + yargs: "npm:^17.7.2" + peerDependencies: + constructs: ^10.5.0 + bin: + projen: bin/projen + checksum: 10c0/f7f14980d3cf63bd8de64a07cee680153df48193b2879b78e6ade2531a8c9e8df2d906954aae855ef3595df85241af354cf9de90feacc83d541e96757b974e27 + languageName: node + linkType: hard + "projen@npm:^0.99.73": version: 0.99.73 resolution: "projen@npm:0.99.73" @@ -15904,6 +16260,16 @@ __metadata: languageName: node linkType: hard +"qs@npm:~6.15.1": + version: 6.15.3 + resolution: "qs@npm:6.15.3" + dependencies: + es-define-property: "npm:^1.0.1" + side-channel: "npm:^1.1.1" + checksum: 10c0/8f3f6e45ece255347d57696628401cde29e9ec649fff698b53bd3150dea7cefdf33036e1bc1826b9f110bfa7cb0ec4ab9f5297eca628ce216c55af82c304e08e + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -16679,7 +17045,7 @@ __metadata: languageName: node linkType: hard -"side-channel-list@npm:^1.0.0": +"side-channel-list@npm:^1.0.0, side-channel-list@npm:^1.0.1": version: 1.0.1 resolution: "side-channel-list@npm:1.0.1" dependencies: @@ -16727,6 +17093,19 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.1.1": + version: 1.1.1 + resolution: "side-channel@npm:1.1.1" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.4" + side-channel-list: "npm:^1.0.1" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/dc0ab81d67f61bda9247d053ce93f41c3fd8ad2bdcb9cf9d8d2f8540d488f26d87a5e99ebfc07eea49ec025867b2452b705442d974b1478f0395e69f6bfb3270 + languageName: node + linkType: hard + "signal-exit@npm:3.0.7, signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -17959,6 +18338,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:^5.26": + version: 5.28.4 + resolution: "undici-types@npm:5.28.4" + checksum: 10c0/ce2fb0cf99f3d14d61290375ec3fe784840991cede3db0d63ef1ecbb1976f2267c6cbc8b5ed7e18be0ec090d33899f863a479ae1caf357fc19877e1de40a0c96 + languageName: node + linkType: hard + "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" @@ -18203,6 +18589,79 @@ __metadata: languageName: node linkType: hard +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: 10c0/0789c227057a844f5ead55c84679206227a639b9fb76e881185053abc4e9848aa487245966cc2393fcb342c4541241b015a1a2559fddd20ac1e68945c95344e6 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:9.0.0": + version: 9.0.0 + resolution: "vscode-jsonrpc@npm:9.0.0" + checksum: 10c0/1c53de2a017a43e4df803c2d4368784f181c9a9bdcd198eaecb02859ee47e13877abbbe73aaeec645f53c1dff0bfa37a3797133063cba1857350cd9b87113e47 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:^8": + version: 8.2.1 + resolution: "vscode-jsonrpc@npm:8.2.1" + checksum: 10c0/595e07f779112d979d9cd37a00e4b973c39da09dd7c35b149f9fb7857c87db00d0ece7772c5a6f65cc19e38afc4fc64f8130d2f0e6b5f7fc160a0fc6b94e8c07 + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-protocol@npm:3.17.5" + dependencies: + vscode-jsonrpc: "npm:8.2.0" + vscode-languageserver-types: "npm:3.17.5" + checksum: 10c0/5f38fd80da9868d706eaa4a025f4aff9c3faad34646bcde1426f915cbd8d7e8b6c3755ce3fef6eebd256ba3145426af1085305f8a76e34276d2e95aaf339a90b + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:^3": + version: 3.18.1 + resolution: "vscode-languageserver-protocol@npm:3.18.1" + dependencies: + vscode-jsonrpc: "npm:9.0.0" + vscode-languageserver-types: "npm:3.18.0" + checksum: 10c0/49da7c5dca3676b02fbcd9fc94d0e3b50678c2f64a8af17921cdd0b2d49c091912734fbfa9bbdbc8171538d629c6f131711cd4356aeaffa3767dd0ddb7d02b18 + languageName: node + linkType: hard + +"vscode-languageserver-textdocument@npm:^1": + version: 1.0.13 + resolution: "vscode-languageserver-textdocument@npm:1.0.13" + checksum: 10c0/1de174f1de3bfa9e1660a4b3c1b1c711497196d761e33f4d00785fdbfc556ae5a1f440db04771cbc0f6bd66c2a6f303062bc17d8dc46e76820e39fa2d5626564 + languageName: node + linkType: hard + +"vscode-languageserver-types@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-types@npm:3.17.5" + checksum: 10c0/1e1260de79a2cc8de3e46f2e0182cdc94a7eddab487db5a3bd4ee716f67728e685852707d72c059721ce500447be9a46764a04f0611e94e4321ffa088eef36f8 + languageName: node + linkType: hard + +"vscode-languageserver-types@npm:3.18.0": + version: 3.18.0 + resolution: "vscode-languageserver-types@npm:3.18.0" + checksum: 10c0/d8c51cd286cc55b4c1f333e87fa3cefc66d2aae8c65f6209e7ecf88032d3326308b333d62b05243c194be8a3304760445b87630b298116c6c612607f011b4de0 + languageName: node + linkType: hard + +"vscode-languageserver@npm:^9": + version: 9.0.1 + resolution: "vscode-languageserver@npm:9.0.1" + dependencies: + vscode-languageserver-protocol: "npm:3.17.5" + bin: + installServerIntoExtension: bin/installServerIntoExtension + checksum: 10c0/8a0838d77c98a211c76e54bd3a6249fc877e4e1a73322673fb0e921168d8e91de4f170f1d4ff7e8b6289d0698207afc6aba6662d4c1cd8e4bd7cae96afd6b0c2 + languageName: node + linkType: hard + "walk-up-path@npm:^4.0.0": version: 4.0.0 resolution: "walk-up-path@npm:4.0.0" From edd07955b26b69a058c5d27e5225136a085b0a98 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:59:24 +0000 Subject: [PATCH 14/28] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- yarn.lock | 143 +----------------------------------------------------- 1 file changed, 2 insertions(+), 141 deletions(-) diff --git a/yarn.lock b/yarn.lock index e748bff1f..bfe97139d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -166,7 +166,7 @@ __metadata: "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" "@aws-cdk/toolkit-lib": "npm:^0.0.0" - "@cdklabs/eslint-plugin": "npm:^2.0.8" + "@cdklabs/eslint-plugin": "npm:^2.0.9" "@jridgewell/trace-mapping": "npm:^0.3" "@stylistic/eslint-plugin": "npm:^3" "@types/convert-source-map": "npm:^2" @@ -190,7 +190,7 @@ __metadata: jest-junit: "npm:^16" nx: "npm:^22.7.5" prettier: "npm:^2.8" - projen: "npm:^0.99.70" + projen: "npm:^0.99.73" ts-jest: "npm:^29.4.11" typescript: "npm:5.9" vscode-jsonrpc: "npm:^8" @@ -3966,19 +3966,6 @@ __metadata: languageName: node linkType: hard -"@cdklabs/eslint-plugin@npm:^2.0.8": - version: 2.0.11 - resolution: "@cdklabs/eslint-plugin@npm:2.0.11" - dependencies: - "@typescript-eslint/utils": "npm:^8.61.1" - fs-extra: "npm:^11.3.5" - typescript: "npm:^5.9.3" - peerDependencies: - eslint: ">=9 <11" - checksum: 10c0/750759403005775105b75fc09de0812a19b4b620fc19102bd2dc0f082641651fc9dbdf95fc79efa9f28783361092a3014e85289638b6eb658b83ad128df44b7d - languageName: node - linkType: hard - "@cdklabs/eslint-plugin@npm:^2.0.9": version: 2.0.9 resolution: "@cdklabs/eslint-plugin@npm:2.0.9" @@ -7209,19 +7196,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.62.1": - version: 8.62.1 - resolution: "@typescript-eslint/project-service@npm:8.62.1" - dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.62.1" - "@typescript-eslint/types": "npm:^8.62.1" - debug: "npm:^4.4.3" - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/48af1ede4f491a8d499548ec82287e1fab0ea8ac48952a3be5b14c082a679d5e3b82d11a091ecde7ae1b327cb2d02e93255aa880fd86691392c6738b18baf38f - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:8.58.1": version: 8.58.1 resolution: "@typescript-eslint/scope-manager@npm:8.58.1" @@ -7242,16 +7216,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.62.1": - version: 8.62.1 - resolution: "@typescript-eslint/scope-manager@npm:8.62.1" - dependencies: - "@typescript-eslint/types": "npm:8.62.1" - "@typescript-eslint/visitor-keys": "npm:8.62.1" - checksum: 10c0/94cf3724b2fe2f068f357011efd3e42536e5431ee15dcf6dc80f36c842554f2de9a2f185bb3316a38a6e78a07206ffe16b723eb349157f35f820ba1ffe90830b - languageName: node - linkType: hard - "@typescript-eslint/tsconfig-utils@npm:8.58.1, @typescript-eslint/tsconfig-utils@npm:^8.58.1": version: 8.58.1 resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.1" @@ -7270,15 +7234,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.62.1, @typescript-eslint/tsconfig-utils@npm:^8.62.1": - version: 8.62.1 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.62.1" - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/42fb0efb857a6bc6a5b3fc654a2712e4106a75ae7ceb143754fba084f6a4d561adbf666fc334f3aabe6089adcd9a3a896c0009e08f1ef8447bf07900ada3ab66 - languageName: node - linkType: hard - "@typescript-eslint/type-utils@npm:8.58.1": version: 8.58.1 resolution: "@typescript-eslint/type-utils@npm:8.58.1" @@ -7316,13 +7271,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.62.1, @typescript-eslint/types@npm:^8.62.1": - version: 8.62.1 - resolution: "@typescript-eslint/types@npm:8.62.1" - checksum: 10c0/dfa1c9ef016c867a57d3fbef89f7968f3135d8d4621de1a8d2818258f79ed61f26fb6a3381ef27dde0a880817bfd5883f642f03a027dc127d771007022d1d880 - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:8.58.1": version: 8.58.1 resolution: "@typescript-eslint/typescript-estree@npm:8.58.1" @@ -7361,25 +7309,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.62.1": - version: 8.62.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.62.1" - dependencies: - "@typescript-eslint/project-service": "npm:8.62.1" - "@typescript-eslint/tsconfig-utils": "npm:8.62.1" - "@typescript-eslint/types": "npm:8.62.1" - "@typescript-eslint/visitor-keys": "npm:8.62.1" - debug: "npm:^4.4.3" - minimatch: "npm:^10.2.2" - semver: "npm:^7.7.3" - tinyglobby: "npm:^0.2.15" - ts-api-utils: "npm:^2.5.0" - peerDependencies: - typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/e2f554a5a683ded80e010221de46229134717eb7b5ea41cf704b9b702c6e56a93aabc784cafa93fdbc4293f79c0adc1d15ac2915a489be8dee4e9434ab9975ff - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:^4.33.0": version: 4.33.0 resolution: "@typescript-eslint/typescript-estree@npm:4.33.0" @@ -7428,21 +7357,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^8.61.1": - version: 8.62.1 - resolution: "@typescript-eslint/utils@npm:8.62.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.62.1" - "@typescript-eslint/types": "npm:8.62.1" - "@typescript-eslint/typescript-estree": "npm:8.62.1" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: ">=4.8.4 <6.1.0" - checksum: 10c0/c8970c25049bd4c562c0780f96c1ed50082103ecfc441ccc5be7afd1632883c97e258a6482b6e5998a2a51c3bd4dba6fd066709122108a3b71bc5433cd2fa7c8 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/visitor-keys@npm:4.33.0" @@ -7473,16 +7387,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.62.1": - version: 8.62.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.62.1" - dependencies: - "@typescript-eslint/types": "npm:8.62.1" - eslint-visitor-keys: "npm:^5.0.0" - checksum: 10c0/2ab6659c197280b5329be392f4bf3b69ac951e5ab3cc645d05d470381de7f017456938285693b2505f7c02ca56eadc734d55af85c8dee4173b11823c34e7d53c - languageName: node - linkType: hard - "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1": version: 1.11.1 resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1" @@ -9725,15 +9629,6 @@ __metadata: languageName: node linkType: hard -"dax@npm:^0.48.3": - version: 0.48.3 - resolution: "dax@npm:0.48.3" - dependencies: - undici-types: "npm:^5.26" - checksum: 10c0/6db49de2d94c5951050175035141a5a53288d5b2526f4c6a143fa5c981feb0b85194d12d66636b977d8676d8764d2674e4029a454d4cf12f3d2b031332763eae - languageName: node - linkType: hard - "debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -16052,33 +15947,6 @@ __metadata: languageName: node linkType: hard -"projen@npm:^0.99.70": - version: 0.99.80 - resolution: "projen@npm:0.99.80" - dependencies: - "@iarna/toml": "npm:^2.2.5" - case: "npm:^1.6.3" - chalk: "npm:^4.1.2" - comment-json: "npm:4.2.2" - constructs: "npm:^10.5.0" - conventional-changelog-config-spec: "npm:^2.1.0" - dax: "npm:^0.48.3" - fast-glob: "npm:^3.3.3" - fast-json-patch: "npm:^3.1.1" - ini: "npm:^2.0.0" - parse-conflict-json: "npm:^4.0.0" - semver: "npm:^7.8.4" - xmlbuilder2: "npm:^4.0.3" - yaml: "npm:^2.2.2" - yargs: "npm:^17.7.2" - peerDependencies: - constructs: ^10.5.0 - bin: - projen: bin/projen - checksum: 10c0/f7f14980d3cf63bd8de64a07cee680153df48193b2879b78e6ade2531a8c9e8df2d906954aae855ef3595df85241af354cf9de90feacc83d541e96757b974e27 - languageName: node - linkType: hard - "projen@npm:^0.99.73": version: 0.99.73 resolution: "projen@npm:0.99.73" @@ -18338,13 +18206,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:^5.26": - version: 5.28.4 - resolution: "undici-types@npm:5.28.4" - checksum: 10c0/ce2fb0cf99f3d14d61290375ec3fe784840991cede3db0d63ef1ecbb1976f2267c6cbc8b5ed7e18be0ec090d33899f863a479ae1caf357fc19877e1de40a0c96 - languageName: node - linkType: hard - "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" From 8762f7b92c4f0c0d8717862afabb071ca7a9c133 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:33:10 -0400 Subject: [PATCH 15/28] feat: synth triggering (#1634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds manual synth and auto-synth-on-save to the CDK LSP. The cdk.out watcher (landed in #1630) handles all diagnostics/lens refreshes -- these commands just add new ways to produce a fresh assembly. Features: ↻ Synth now CodeLens at the top of any CDK source file (only when auto-synth is off) ▶ Enable auto-synth / ⏹ Disable auto-synth toggle Auto-synth starts disabled When auto-synth is on, saving any non-ignored file in the project triggers a synth All feedback/output goes to the Output panel, no popups Design decisions (the important ones): Concurrent synths are supressed: if a save fires during a slow synth, that save is skipped. The next save picks it up. cdk.json is read once at startup. Changing it requires an LSP restart. The toggle/synth lenses only appear on files that already have L1 resource lenses. Files with no CDK resources see nothing. Toggle state resets to disabled on LSP restart (not persisted). NOTE: One thing not in this PR (but must land before prod) is a workspace trust gate. We should verify with the user that they trust this workspace before running any synth. I may need to set up some way to preserve this between sessions. Open questions / things I need some feedback on: - Is "auto-synth" the right name? Alternatives: "synth on save", "live synth"? - Where should the toggle live? Line 0 of a CDK file works for LSP-only, but it's only visible when you're in a file with constructs. Status bar item would be better UX but needs a client extension. Is there a better alternative? - Should synth failures (app compile errors) be more visible than the Output panel? A diagnostic on the first line of the failing file, for example? - If the app has context lookups and no cached cdk.context.json, synth fails with an auth/context error. Should the error message detect this and suggest running cdk synth in terminal first? Screenshot 2026-06-16 at 3 41 01 PM Screenshot 2026-06-16 at 3 41 10 PM ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .../cdk-explorer/lib/core/assembly-watcher.ts | 6 +- .../cdk-explorer/lib/core/cdk-config.ts | 35 + .../cdk-explorer/lib/core/synth-runner.ts | 99 +++ .../@aws-cdk/cdk-explorer/lib/lsp/codelens.ts | 39 +- .../@aws-cdk/cdk-explorer/lib/lsp/commands.ts | 94 +++ .../@aws-cdk/cdk-explorer/lib/lsp/io-host.ts | 46 ++ .../@aws-cdk/cdk-explorer/lib/lsp/main.ts | 17 + .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 243 ++++++- .../cdk-explorer/lib/lsp/synth-diagnostics.ts | 87 +++ .../cdk-explorer/test/core/cdk-config.test.ts | 60 ++ .../test/core/synth-runner.test.ts | 162 +++++ .../cdk-explorer/test/lsp/codelens.test.ts | 83 ++- .../cdk-explorer/test/lsp/commands.test.ts | 122 ++++ .../cdk-explorer/test/lsp/io-host.test.ts | 61 ++ .../cdk-explorer/test/lsp/server.test.ts | 629 +++++++++++++++--- .../test/lsp/synth-diagnostics.test.ts | 90 +++ .../private/context-aware-source.ts | 5 +- .../@aws-cdk/toolkit-lib/lib/api/rwlock.ts | 6 +- .../toolkit-lib/lib/toolkit/toolkit-error.ts | 52 ++ .../test/toolkit/toolkit-error.test.ts | 21 +- 20 files changed, 1792 insertions(+), 165 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/core/cdk-config.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/commands.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/io-host.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/synth-diagnostics.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/core/cdk-config.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/core/synth-runner.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/commands.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/io-host.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/synth-diagnostics.test.ts diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts index d7a1d5543..ce065f21e 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts @@ -23,7 +23,11 @@ const ASSEMBLY_SIGNAL_FILES = new Set([ VALIDATION_REPORT_FILE, ]); -const DEBOUNCE_MS = 200; +// Debounce to coalesce a synth's burst of file writes into a single refresh. +// The refresh acquires its own read lock and retries on contention (see +// refreshFromAssembly), so this no longer needs to be tuned against the synth's +// lock-hold timing. +const DEBOUNCE_MS = 50; /** A running assembly watcher. */ export interface AssemblyWatcher { diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/cdk-config.ts b/packages/@aws-cdk/cdk-explorer/lib/core/cdk-config.ts new file mode 100644 index 000000000..edcffd7e6 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/core/cdk-config.ts @@ -0,0 +1,35 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * The subset of `cdk.json` the explorer cares about. + * + * `app` is the command CDK runs to produce a cloud assembly (e.g. + * `npx ts-node bin/app.ts`). We need it to invoke `Toolkit.synth()` + * via `fromCdkApp`. + */ +export interface CdkConfig { + /** The `app` command, or `undefined` if missing/malformed. */ + readonly app: string | undefined; +} + +/** + * Reads `/cdk.json` and returns the parts the explorer uses. + * Never throws. Treats missing files, malformed JSON, or wrong-typed + * fields as "not configured" so callers can fall back gracefully + */ +export function readCdkConfig(projectDir: string): CdkConfig { + const configPath = path.join(projectDir, 'cdk.json'); + if (!fs.existsSync(configPath)) return { app: undefined }; + + let parsed: unknown; + try { + parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } catch { + return { app: undefined }; + } + + if (parsed === null || typeof parsed !== 'object') return { app: undefined }; + const app = (parsed as { app?: unknown }).app; + return { app: typeof app === 'string' ? app : undefined }; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts b/packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts new file mode 100644 index 000000000..f496910d9 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts @@ -0,0 +1,99 @@ +import { ToolkitError, type Toolkit } from '@aws-cdk/toolkit-lib'; +import { readCdkConfig } from './cdk-config'; + +/** + * Hold synth()'s read lock this long before releasing it. While we hold a + * reader, the next synth cannot take the write lock, so a watcher refresh that + * fires in this window shares the read lock rather than being starved under + * continuous synths. The refresh also retries its own acquire on contention + * (see refreshFromAssembly), so this delay is an anti-starvation hint, not a + * correctness requirement. + */ +const READER_HANDOFF_DELAY_MS = 100; + +/** + * The outcome of a single synth attempt. + * + * `success` means the assembly was written to disk (the watcher will see it). + * `app-failure` means the user's CDK app threw, did not compile, or needs uncached context lookups. + * `lock-conflict` means another process holds `/cdk.out` (a `cdk + * synth` running in a terminal, a `cdk watch` loop, or our own previous synth + * not yet released). Callers should not surface this as a hard error. + * `unavailable` means `cdk.json` is missing or has no `app` key, so there is + * nothing to synth. Read fresh on every call, so adding an `app` later is + * picked up without restarting the LSP. + * `error` is reserved for anything we did not classify, including failures + * during dispose. + */ +export type SynthRunResult = + | { status: 'success' } + | { status: 'app-failure'; message: string; details?: string } + | { status: 'lock-conflict' } + | { status: 'unavailable' } + | { status: 'error'; message: string }; + +export interface SynthRunnerOptions { + /** A configured Toolkit instance (its IoHost decides where messages go). */ + readonly toolkit: Toolkit; + /** Directory containing the user's `cdk.json`; also the synth working dir. */ + readonly projectDir: string; +} + +/** + * Run a one-shot synth of the user's CDK app. Writes `/cdk.out` + * via `Toolkit.synth(fromCdkApp(...))`, then immediately disposes the cached + * assembly so the read lock is released before the next call. Holding the + * cached assembly between calls would cause the next acquireWrite to throw + * `ConcurrentReadLock` against ourselves. + * + * The `app` command is read from `cdk.json` on every call, not cached, so an + * edited command or a newly added `app` takes effect on the next synth. + */ +export async function runSynth(options: SynthRunnerOptions): Promise { + const app = readCdkConfig(options.projectDir).app; + if (app === undefined) return { status: 'unavailable' }; + + let cached; + try { + const cx = await options.toolkit.fromCdkApp(app, { + workingDirectory: options.projectDir, + lookups: false, + }); + cached = await options.toolkit.synth(cx); + } catch (err) { + return classify(err); + } + + // Brief hold so the cdk.out watcher can take its own read lock before the + // next writer comes in (see READER_HANDOFF_DELAY_MS), then release. + await new Promise((resolve) => setTimeout(resolve, READER_HANDOFF_DELAY_MS)); + + try { + await cached.dispose(); + } catch (err) { + // Releases the read lock synth() left on the assembly. Failure is rare (an + // fs error deleting the lock file); report it as `error` so the next synth + // does not silently self-conflict on the stale reader. + return { status: 'error', message: (err as Error).message }; + } + + return { status: 'success' }; +} + +function classify(err: unknown): SynthRunResult { + if (ToolkitError.isLockError(err)) { + return { status: 'lock-conflict' }; + } + if (ToolkitError.isContextLookupsDisabledError(err)) { + return { + status: 'app-failure', + message: 'This app needs context lookups that are not in cdk.context.json. ' + + 'Run `cdk synth` in a terminal (with AWS credentials) to populate it, then retry.', + }; + } + if (ToolkitError.isAssemblyError(err)) { + // details = captured subprocess stderr (file:line:col), used for diagnostics. + return { status: 'app-failure', message: err.message, details: (err.cause as Error | undefined)?.message }; + } + return { status: 'error', message: (err as Error).message }; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts index ee484f90e..ef217b1af 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts @@ -1,6 +1,7 @@ import { pathToFileURL } from 'url'; import type { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; import { type CodeLens, type Command, type Range } from 'vscode-languageserver/node'; +import { COMMAND_DISABLE_AUTO_SYNTH, COMMAND_ENABLE_AUTO_SYNTH, COMMAND_SYNTH_NOW } from './commands'; import { resourceTarget, type ResourceTarget } from './template-locator'; import type { ConstructNode } from '../core/assembly-reader'; import type { SourceLocation } from '../core/source-resolver'; @@ -9,11 +10,23 @@ import type { SourceLocation } from '../core/source-resolver'; export const OPEN_RESOURCE_COMMAND = 'cdkExplorer.openResource'; /** - * Build CodeLens entries for a single source file. For every construct whose - * sourceLocation matches fileUri, group by line and emit one lens per line - * summarising the CFN resources produced there. + * Build CodeLens entries for a single source file. Returns an empty array if + * no CDK resources in the index map to `fileUri`. + * + * When resources are found, two header lenses are prepended at line 0: + * - `autoSynthEnabled = false`: "↻ Synth now" + "▶ Enable auto-synth" + * - `autoSynthEnabled = true`: "⏹ Disable auto-synth" (saves trigger synth) + * + * The remaining lenses are one per source line, each summarising the CFN + * resources produced there (multiple L2 fan-out resources are grouped). + * + * @param autoSynthEnabled - current toggle state; controls which header lenses appear */ -export async function codeLensesForFile(index: ConstructIndex, fileUri: string): Promise { +export async function codeLensesForFile( + index: ConstructIndex, + fileUri: string, + autoSynthEnabled: boolean, +): Promise { const matches = [...index] .filter((node) => isResourceOnFile(node, fileUri)) .map((node) => ({ line: node.sourceLocation.line, node })); @@ -21,14 +34,26 @@ export async function codeLensesForFile(index: ConstructIndex, fi // Multiple resources can map to one line when an L2 construct fans out // (e.g. an L2 producing a primary resource + auxiliary resources). Resolve // sequentially so the number of concurrent reads never grows with app size. - const lenses: CodeLens[] = []; + const l1Lenses: CodeLens[] = []; for (const [line, group] of groupBy(matches, (m) => m.line)) { - lenses.push({ + l1Lenses.push({ range: lineRange(line), command: await commandFor(group.map((m) => m.node)), }); } - return lenses; + + if (l1Lenses.length === 0) return []; + + const header0: Range = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }; + // When auto-synth is off, show "Synth now" + "Enable auto-synth". + // When auto-synth is on, show only "Disable auto-synth" (saves handle synth). + const headerLenses: CodeLens[] = autoSynthEnabled + ? [{ range: header0, command: { title: '⏹ Disable auto-synth', command: COMMAND_DISABLE_AUTO_SYNTH } }] + : [ + { range: header0, command: { title: '↻ Synth now', command: COMMAND_SYNTH_NOW } }, + { range: header0, command: { title: '▶ Enable auto-synth', command: COMMAND_ENABLE_AUTO_SYNTH } }, + ]; + return [...headerLenses, ...l1Lenses]; } /** diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/commands.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/commands.ts new file mode 100644 index 000000000..bb89f0a3a --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/commands.ts @@ -0,0 +1,94 @@ +import type { SynthRunResult } from '../core/synth-runner'; + +/** + * Trigger a one-shot synth of the user's CDK app. Only shown when + * auto-synth is disabled (saves handle synth when it is enabled). + */ +export const COMMAND_SYNTH_NOW = 'cdk.explorer.synthNow'; +/** Enable auto-synth-on-save. Replaces "Synth now" in the header lens. */ +export const COMMAND_ENABLE_AUTO_SYNTH = 'cdk.explorer.enableAutoSynth'; +/** Disable auto-synth-on-save. Restores the "Synth now" header lens. */ +export const COMMAND_DISABLE_AUTO_SYNTH = 'cdk.explorer.disableAutoSynth'; + +/** All commands this LSP advertises via `executeCommandProvider`. */ +export const SUPPORTED_COMMANDS = [COMMAND_SYNTH_NOW, COMMAND_ENABLE_AUTO_SYNTH, COMMAND_DISABLE_AUTO_SYNTH] as const; + +/** + * UI sinks the dispatcher uses to communicate with the user. + * Implementations bridge to `connection.console` in the LSP layer. + */ +export interface NotifySink { + /** Write a non-error informational message to the Output panel. */ + info(message: string): void; + /** Write an error message to the Output panel. */ + error(message: string): void; + /** + * Run a long operation with a visible progress indicator. The implementation + * is responsible for ending the indicator regardless of success or failure. + */ + withProgress(message: string, fn: () => Promise): Promise; +} + +export interface CommandHandlerOptions { + /** Invokes a single synth. Resolves with the typed outcome; never rejects. */ + readonly synth: () => Promise; + /** Called with the new desired state when the user toggles auto-synth. */ + readonly toggleAutoSynth: (enabled: boolean) => void; + /** UI sinks for messages and progress. */ + readonly notify: NotifySink; +} + +const SYNTH_UNAVAILABLE_MESSAGE = "CDK synth unavailable: 'cdk.json' missing or has no 'app' key."; +const LOCK_CONFLICT_MESSAGE = 'Another synth is in progress. Results will refresh shortly.'; +const PROGRESS_MESSAGE = 'Synthesizing CDK app...'; + +/** + * Handle a `workspace/executeCommand` request. The synth command runs under a + * progress indicator and reports outcomes through the notify sinks. Unknown + * commands are silently ignored. + */ +export async function executeCommand( + command: string, + _args: unknown[], + options: CommandHandlerOptions, +): Promise { + switch (command) { + case COMMAND_ENABLE_AUTO_SYNTH: + options.toggleAutoSynth(true); + return; + + case COMMAND_DISABLE_AUTO_SYNTH: + options.toggleAutoSynth(false); + return; + + case COMMAND_SYNTH_NOW: + { + const result = await options.notify.withProgress(PROGRESS_MESSAGE, () => options.synth()); + handleSynthResult(result, options.notify); + } + return; + + default: + return; + } +} + +function handleSynthResult(result: SynthRunResult, notify: NotifySink): void { + switch (result.status) { + case 'success': + // Silent. The watcher refreshes the editor when `cdk.out` changes. + return; + case 'app-failure': + notify.error(`CDK synth failed: ${result.message}`); + return; + case 'lock-conflict': + notify.info(LOCK_CONFLICT_MESSAGE); + return; + case 'unavailable': + notify.info(SYNTH_UNAVAILABLE_MESSAGE); + return; + case 'error': + notify.error(`CDK synth failed unexpectedly: ${result.message}`); + return; + } +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/io-host.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/io-host.ts new file mode 100644 index 000000000..2ea1d1991 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/io-host.ts @@ -0,0 +1,46 @@ +import type { IIoHost, IoMessage, IoRequest } from '@aws-cdk/toolkit-lib'; +import type { RemoteConsole } from 'vscode-languageserver/node'; + +/** + * IoHost for the LSP. Routes Toolkit messages into the editor's Output + * channel via the LSP connection's console. + * + * We do NOT reuse `NonInteractiveIoHost` from toolkit-lib even though its + * `requestResponse` is identical: that class writes to `process.stdout` / + * `process.stderr`, which are the JSON-RPC transport for this process. + * Writing Toolkit output there would corrupt the protocol stream. + * + * The LSP cannot prompt the user synchronously through `connection.console`, + * so `requestResponse` returns each message's `defaultResponse`. For synth, + * the only reachable interactive prompt is an MFA token (when the app has + * context lookups, no cached `cdk.context.json`, and an MFA-protected profile). + * Returning the default causes an auth failure, surfaced as `app-failure` with + * a clear message. All other prompts are on deploy/destroy paths we don't call. + */ +export class LspIoHost implements IIoHost { + public constructor(private readonly console: RemoteConsole) { + } + + public async notify(msg: IoMessage): Promise { + switch (msg.level) { + case 'error': + this.console.error(msg.message); + break; + case 'warn': + this.console.warn(msg.message); + break; + case 'debug': + case 'trace': + // Suppress noisy levels; keeps the Output panel readable. + break; + default: + this.console.info(msg.message); + } + } + + public async requestResponse(msg: IoRequest): Promise { + await this.notify(msg); + this.console.info(`Auto-answered with default response: ${JSON.stringify(msg.defaultResponse)}`); + return msg.defaultResponse; + } +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts index f1f09461f..097be72b2 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts @@ -1,9 +1,26 @@ +import { Toolkit } from '@aws-cdk/toolkit-lib'; +import { LspIoHost } from './io-host'; import { startServer } from './server'; +import { runSynth } from '../core/synth-runner'; try { startServer({ readable: process.stdin, writable: process.stdout, + // Build the Toolkit once connection.console exists (so output reaches the + // editor Output panel), then bind the two ops the handlers need. The read + // lock comes from fromAssemblyDirectory().produce(), so we never touch RWLock. + toolkitBindingsFactory: (console) => { + const toolkit = new Toolkit({ ioHost: new LspIoHost(console) }); + return { + synthRunner: (projectDir) => runSynth({ toolkit, projectDir }), + acquireAssemblyLock: async (assemblyDir) => { + const cx = await toolkit.fromAssemblyDirectory(assemblyDir, { failOnMissingContext: false }); + const readable = await cx.produce(); + return { release: () => readable.dispose() }; + }, + }; + }, }); } catch (err) { const e = err as Error; diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 856f59bbc..a333d3930 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import { ToolkitError } from '@aws-cdk/toolkit-lib'; import { StreamMessageReader, StreamMessageWriter, @@ -16,14 +17,18 @@ import { type DefinitionParams, type DidSaveTextDocumentParams, type Diagnostic, + type ExecuteCommandParams, type InitializeParams, type InitializeResult, type Location, + type RemoteConsole, } from 'vscode-languageserver/node'; /* eslint-disable import/no-relative-packages */ import { codeLensesForFile } from './codelens'; +import { executeCommand, SUPPORTED_COMMANDS, type NotifySink } from './commands'; import { mapViolationsToDiagnostics } from './diagnostics'; import { offsetAtPosition } from './positions'; +import { synthFailureDiagnostics } from './synth-diagnostics'; import { sourceTargetAtTemplateOffset } from './template-locator'; import { WATCH_EXCLUDE_DEFAULTS } from '../../../toolkit-lib/lib/actions/watch/private/helpers'; import { createIgnoreMatcher } from '../../../toolkit-lib/lib/util/glob-matcher'; @@ -37,12 +42,31 @@ import { type AssemblyWatcher, type AssemblyWatcherOptions, } from '../core/assembly-watcher'; +import type { SynthRunResult } from '../core/synth-runner'; + +/** + * The cdk.out watcher fires after a synth's file writes but possibly before the + * synth releases its write lock. The toolkit's read lock is fail-fast (it does + * not queue), so refreshFromAssembly polls up to REFRESH_LOCK_RETRIES times, + * REFRESH_LOCK_RETRY_MS apart, for the write lock to clear before giving up on + * a refresh pass. + */ +const REFRESH_LOCK_RETRIES = 10; +const REFRESH_LOCK_RETRY_MS = 50; + +/** + * A held read lock on the cloud assembly directory; `release()` unlocks it. + * In production this wraps the Toolkit's `fromAssemblyDirectory().produce()` readable. + */ +export interface AssemblyLock { + release(): Promise; +} export interface LspHandlerOptions { - /** Callback invoked on `didSave` for tracked source files. */ - readonly onSynthRequest?: (projectDir: string) => void; /** Override readAssembly for tests. Defaults to reading /cdk.out. */ readonly readAssembly?: (assemblyDir: string) => Promise; + /** Acquire a read lock on the assembly dir; throws a `LockError` on writer contention. */ + readonly acquireAssemblyLock: (assemblyDir: string) => Promise; /** * Sink for non-fatal messages. In production, the connection's console writes * to the editor's Output panel; in tests, capture into an array. @@ -61,11 +85,35 @@ export interface LspHandlerOptions { * overridden in tests to drive refreshes deterministically. */ readonly startAssemblyWatcher?: (options: AssemblyWatcherOptions) => AssemblyWatcher; + /** + * Runs a synth of the project at the given root and returns its typed outcome. + * Injected by startServer (built from the toolkit bindings); omitted in tests + * that don't exercise synth. The runner reads `cdk.json` under the passed root + * on each call and returns `unavailable` when there is no `app`, so + * availability is decided per synth, not cached here. + */ + readonly synthRunner?: (projectDir: string) => Promise; + /** User-facing notification sink. Injected by startServer; omitted in tests. */ + readonly notify?: NotifySink; } -export interface LspServerOptions extends LspHandlerOptions { +/** + * The Toolkit-backed operations the handlers need, built from one Toolkit wired + * to the editor Output panel. The read lock comes from the Toolkit's own + * `fromAssemblyDirectory().produce()`, so the LSP never imports `RWLock`. + */ +export interface ToolkitBindings { + readonly synthRunner: (projectDir: string) => Promise; + readonly acquireAssemblyLock: (assemblyDir: string) => Promise; +} + +export type ToolkitBindingsFactory = (console: RemoteConsole) => ToolkitBindings; + +export interface LspServerOptions { readonly readable: NodeJS.ReadableStream; readonly writable: NodeJS.WritableStream; + /** Builds the Toolkit-backed bindings once `connection.console` exists. Required; main.ts always wires it. */ + readonly toolkitBindingsFactory: ToolkitBindingsFactory; } /** Pure handler functions for LSP messages, extracted for direct unit testing. */ @@ -75,6 +123,7 @@ export interface LspHandlers { onDidSaveTextDocument(params: DidSaveTextDocumentParams): void; onCodeLens(params: CodeLensParams): Promise; onDefinition(params: DefinitionParams): Promise; + onExecuteCommand(params: ExecuteCommandParams): Promise; onShutdown(): void; } @@ -90,23 +139,50 @@ const NOOP_LOGGER: LogSink = { }, }; +/** Log auto-synth-on-save outcomes. Errors go to the Output panel; success is silent. */ +function handleSynthOnSave(result: SynthRunResult, log: LogSink): void { + switch (result.status) { + case 'success': + case 'lock-conflict': + case 'unavailable': + return; // silent — watcher handles updates; lock = another synth running; unavailable = no app + case 'app-failure': + log.error(`Auto-synth failed: ${result.message}`); + return; + case 'error': + log.error(`Auto-synth failed unexpectedly: ${result.message}`); + return; + } +} + /** * Build the LSP message handlers as plain functions over closed-over state. * No streams, no JSON-RPC, no framework — testable in isolation. */ -export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers { - const onSynthRequest = options.onSynthRequest ?? (() => { - }); +export function createLspHandlers(options: LspHandlerOptions): LspHandlers { const readAssembly = options.readAssembly ?? defaultReadAssembly; + const acquireAssemblyLock = options.acquireAssemblyLock; const log = options.logger ?? NOOP_LOGGER; const onPublishDiagnostics = options.onPublishDiagnostics ?? (() => { }); const onRefreshCodeLenses = options.onRefreshCodeLenses ?? (() => { }); const startWatcher = options.startAssemblyWatcher ?? defaultStartAssemblyWatcher; + const synthRunner = options.synthRunner; + const notify = options.notify ?? { + info: () => { + }, + error: () => { + }, + withProgress: (_msg: string, fn: () => Promise) => fn(), + }; let applicationDir: string | undefined; let shutdownRequested = false; + let synthInFlight = false; + // URIs (source files, or cdk.json) currently showing synth-failure diagnostics. + let synthFailureUris = new Set(); + let autoSynthEnabled = false; // off by default; user enables via the CodeLens toggle let shouldIgnore: (filePath: string) => boolean = () => false; let assemblyWatcher: AssemblyWatcher | undefined; // Latest index from readAssembly, served to CodeLens. Refreshed at startup @@ -121,9 +197,48 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers // the editor's next natural re-query. let codeLensRefreshSupported = false; + // Single source of truth for the project root: the directory the client opened + // (applicationDir from initialize), falling back to cwd for non-IDE callers. + // Every consumer (assembly read, watcher, synth, diagnostics) reads this so + // they never disagree about which project is being operated on. + function currentProjectDir(): string { + return applicationDir ?? process.cwd(); + } + async function refreshFromAssembly(projectDir: string): Promise { const assemblyDir = path.join(projectDir, 'cdk.out'); - const result = await readAssembly(assemblyDir); + + // Hold a read lock so a concurrent synth can't overwrite cdk.out mid-read. + // The lock is fail-fast, so poll until the writer releases; the file events + // that triggered us are already over. + let lock: AssemblyLock | undefined; + for (let attempt = 0; attempt <= REFRESH_LOCK_RETRIES; attempt++) { + try { + lock = await acquireAssemblyLock(assemblyDir); + break; + } catch (err) { + if (ToolkitError.isLockError(err)) { + if (attempt < REFRESH_LOCK_RETRIES) { + await new Promise((resolve) => setTimeout(resolve, REFRESH_LOCK_RETRY_MS)); + } + continue; + } + // ENOENT (no cdk.out yet) is expected and silent; anything else is real. + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + log.error(`Failed to open cloud assembly for reading: ${(err as Error).message}`); + } + return; + } + } + // Write lock still held after every retry: skip this pass. + if (lock === undefined) return; + + let result: AssemblyReadResult; + try { + result = await readAssembly(assemblyDir); + } finally { + await lock.release(); + } if (result.status === 'error') { log.error(`Failed to read cloud assembly: ${result.message}`); @@ -163,6 +278,52 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers } } + // Shared synth invocation used by both the manual CodeLens command and + // auto-synth-on-save. The in-flight latch suppresses overlapping synths from + // the same LSP instance: a second call while the first is running is dropped + // (not queued) and returns lock-conflict immediately. The Toolkit's RWLock + // would reject it anyway, but this short-circuits before any setup work. + async function guardedSynth(): Promise { + if (synthInFlight) return { status: 'lock-conflict' }; + synthInFlight = true; + try { + const result = await (synthRunner ? synthRunner(currentProjectDir()) : Promise.resolve({ status: 'error', message: 'No synth runner configured' } as const)); + publishSynthDiagnostics(result); + return result; + } finally { + synthInFlight = false; + } + } + + // Publish diagnostics for a failed synth (one per failing source file, or + // cdk.json as a fallback) and clear them once a synth succeeds. The cdk.out + // watcher owns violation diagnostics; this only manages synth-failure ones. + function publishSynthDiagnostics(result: SynthRunResult): void { + // A successful synth resolves all failures; 'unavailable' means there is no + // app to fail, so any prior synth-failure diagnostics no longer apply. + if (result.status === 'success' || result.status === 'unavailable') { + clearSynthFailures(); + return; + } + const failures = synthFailureDiagnostics(result, currentProjectDir()); + if (failures.length === 0) return; // lock-conflict / error: leave any existing diagnostic + const nextUris = new Set(failures.map((f) => f.uri)); + for (const uri of synthFailureUris) { + if (!nextUris.has(uri)) onPublishDiagnostics(uri, []); + } + for (const f of failures) { + onPublishDiagnostics(f.uri, f.diagnostics); + } + synthFailureUris = nextUris; + } + + function clearSynthFailures(): void { + for (const uri of synthFailureUris) { + onPublishDiagnostics(uri, []); + } + synthFailureUris = new Set(); + } + return { onInitialize(params) { applicationDir = params.initializationOptions?.applicationDir; @@ -180,11 +341,12 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers codeLensProvider: { resolveProvider: false }, // Go-to-definition from a synthesized template back to construct source. definitionProvider: true, + executeCommandProvider: { commands: [...SUPPORTED_COMMANDS] }, }, }; }, async onInitialized() { - const projectDir = applicationDir ?? process.cwd(); + const projectDir = currentProjectDir(); // Same exclusion logic as toolkit-lib's watch(): // WATCH_EXCLUDE_DEFAULTS covers common non-source dirs, then we add cdk.out // (our own output) and dotfiles (editor configs, .git, etc.) @@ -218,15 +380,13 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers if (shutdownRequested) return; const filePath = fileURLToPath(params.textDocument.uri); if (shouldIgnore(filePath)) return; - const projectDir = applicationDir ?? process.cwd(); - try { - onSynthRequest(projectDir); - } catch (err) { - log.error(`Synth request failed: ${(err as Error).message}`); - } + if (!autoSynthEnabled) return; + void guardedSynth() + .then((result) => handleSynthOnSave(result, log)) + .catch((err: unknown) => log.error(`Auto-synth threw unexpectedly: ${(err as Error).message}`)); }, onCodeLens(params) { - return codeLensesForFile(cachedIndex, params.textDocument.uri); + return codeLensesForFile(cachedIndex, params.textDocument.uri, autoSynthEnabled); }, async onDefinition(params) { // Only synthesized templates link back to source, and only file: URIs are @@ -249,6 +409,16 @@ export function createLspHandlers(options: LspHandlerOptions = {}): LspHandlers const offset = offsetAtPosition(templateText, params.position); return sourceTargetAtTemplateOffset(cachedIndex, filePath, templateText, offset); }, + async onExecuteCommand(params) { + await executeCommand(params.command, params.arguments ?? [], { + synth: guardedSynth, + toggleAutoSynth: (enabled) => { + autoSynthEnabled = enabled; + onRefreshCodeLenses(); + }, + notify, + }); + }, onShutdown() { shutdownRequested = true; void assemblyWatcher?.close(); @@ -264,19 +434,53 @@ export function startServer(options: LspServerOptions): void { new StreamMessageWriter(options.writable), ); + // Captured from onInitialize; used to gate CodeLens refresh requests. + let codeLensRefreshSupported = false; + + // Build the toolkit bindings once connection.console exists; main.ts owns how + // they are constructed (synth runner + assembly read lock). + const { synthRunner, acquireAssemblyLock } = options.toolkitBindingsFactory(connection.console); + const handlers = createLspHandlers({ - onSynthRequest: options.onSynthRequest, - readAssembly: options.readAssembly, logger: connection.console, onPublishDiagnostics: (uri, diagnostics) => { void connection.sendDiagnostics({ uri, diagnostics }); }, onRefreshCodeLenses: () => { - void connection.sendRequest(CodeLensRefreshRequest.type); + // codeLensRefreshSupported is captured from onInitialize; gate here so + // the toggle and the watcher both respect the client's capability. + if (codeLensRefreshSupported) { + void connection.sendRequest(CodeLensRefreshRequest.type); + } + }, + synthRunner, + acquireAssemblyLock, + notify: { + // Route to the Output panel (connection.console) rather than popups. + // showMessage creates a dismissable toast that interrupts the user's + // workflow; console writes are visible on demand in the Output panel. + info: (msg) => { + connection.console.info(msg); + }, + error: (msg) => { + connection.console.error(msg); + }, + withProgress: async (title, fn) => { + const progress = await connection.window.createWorkDoneProgress(); + progress.begin(title); + try { + return await fn(); + } finally { + progress.done(); + } + }, }, }); - connection.onInitialize((params) => handlers.onInitialize(params)); + connection.onInitialize((params) => { + codeLensRefreshSupported = params.capabilities.workspace?.codeLens?.refreshSupport ?? false; + return handlers.onInitialize(params); + }); connection.onInitialized(() => { // `initialized` is a notification, so nothing awaits us, but onInitialized's // async work can still reject. Surface it to the editor Output panel rather @@ -288,6 +492,7 @@ export function startServer(options: LspServerOptions): void { connection.onDidSaveTextDocument((params) => handlers.onDidSaveTextDocument(params)); connection.onCodeLens((params) => handlers.onCodeLens(params)); connection.onDefinition((params) => handlers.onDefinition(params)); + connection.onExecuteCommand((params) => handlers.onExecuteCommand(params)); connection.onShutdown(() => handlers.onShutdown()); connection.onExit(() => process.exit(0)); diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/synth-diagnostics.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/synth-diagnostics.ts new file mode 100644 index 000000000..23c59dc6c --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/synth-diagnostics.ts @@ -0,0 +1,87 @@ +import * as path from 'path'; +import { pathToFileURL } from 'url'; +import { type Diagnostic, DiagnosticSeverity, type Range } from 'vscode-languageserver/node'; +import type { SynthRunResult } from '../core/synth-runner'; + +/** A compile error parsed from synth stderr. Line/column are 1-based. */ +export interface SynthErrorLocation { + readonly file: string; + readonly line: number; + readonly column: number; + readonly message: string; +} + +// ts-node "pretty": "bin/app.ts:12:5 - error TS2322: ..." +const TS_NODE_RE = /^(.+?):(\d+):(\d+) - (error TS\d+:.+)$/gm; +// tsc / ts-node default: "bin/app.ts(12,5): error TS2322: ..." +const TSC_RE = /^(.+?)\((\d+),(\d+)\): (error TS\d+:.+)$/gm; + +/** + * Find every TypeScript compile error (file:line:col) in synth stderr. + * Returns an empty array when nothing matches (e.g. a non-TypeScript app or a + * runtime failure). + */ +export function parseSynthErrors(stderr: string | undefined): SynthErrorLocation[] { + if (!stderr) return []; + const out: SynthErrorLocation[] = []; + for (const re of [TS_NODE_RE, TSC_RE]) { + for (const m of stderr.matchAll(re)) { + out.push({ file: m[1], line: Number(m[2]), column: Number(m[3]), message: m[4] }); + } + } + return out; +} + +/** A diagnostic set ready to publish, with the URI it belongs to. */ +export interface SynthFailureDiagnostic { + readonly uri: string; + readonly diagnostics: Diagnostic[]; +} + +/** + * Build diagnostics for an app-failure synth outcome: one entry per failing + * source file, considering only files inside `projectDir`. When nothing anchors + * there, falls back to a single diagnostic on `cdk.json` carrying the summary + * message. Returns an empty array for any non-app-failure outcome. + */ +export function synthFailureDiagnostics( + result: SynthRunResult, + projectDir: string, +): SynthFailureDiagnostic[] { + if (result.status !== 'app-failure') return []; + + const byUri = new Map(); + for (const loc of parseSynthErrors(result.details)) { + const abs = path.isAbsolute(loc.file) ? loc.file : path.resolve(projectDir, loc.file); + if (!isWithin(projectDir, abs)) continue; // never point diagnostics outside the project + const uri = pathToFileURL(abs).toString(); + const list = byUri.get(uri) ?? []; + list.push(diagnostic(rangeAt(loc.line, loc.column), loc.message)); + byUri.set(uri, list); + } + + if (byUri.size > 0) { + return [...byUri].map(([uri, diagnostics]) => ({ uri, diagnostics })); + } + + return [{ + uri: pathToFileURL(path.join(projectDir, 'cdk.json')).toString(), + diagnostics: [diagnostic(rangeAt(1, 1), result.message)], + }]; +} + +function diagnostic(range: Range, message: string): Diagnostic { + return { range, severity: DiagnosticSeverity.Error, source: 'cdk synth', message }; +} + +/** LSP positions are 0-based; the parsed line/column are 1-based. */ +function rangeAt(line: number, column: number): Range { + const l = Math.max(0, line - 1); + const c = Math.max(0, column - 1); + return { start: { line: l, character: c }, end: { line: l, character: Number.MAX_VALUE } }; +} + +function isWithin(root: string, candidate: string): boolean { + const rel = path.relative(root, candidate); + return rel.length > 0 && !rel.startsWith('..') && !path.isAbsolute(rel); +} diff --git a/packages/@aws-cdk/cdk-explorer/test/core/cdk-config.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/cdk-config.test.ts new file mode 100644 index 000000000..55b009c62 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/core/cdk-config.test.ts @@ -0,0 +1,60 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { readCdkConfig } from '../../lib/core/cdk-config'; + +function withTempDir(fn: (dir: string) => T): T { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-cdkconfig-')); + try { + return fn(dir); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +function writeCdkJson(dir: string, contents: string): void { + fs.writeFileSync(path.join(dir, 'cdk.json'), contents); +} + +describe('readCdkConfig', () => { + test('returns the app command when present', () => { + withTempDir((dir) => { + writeCdkJson(dir, JSON.stringify({ app: 'npx ts-node bin/app.ts' })); + expect(readCdkConfig(dir)).toEqual({ app: 'npx ts-node bin/app.ts' }); + }); + }); + + test('returns undefined when cdk.json is absent', () => { + withTempDir((dir) => { + expect(readCdkConfig(dir)).toEqual({ app: undefined }); + }); + }); + + test('returns undefined when cdk.json is malformed', () => { + withTempDir((dir) => { + writeCdkJson(dir, '{not valid json'); + expect(readCdkConfig(dir)).toEqual({ app: undefined }); + }); + }); + + test('returns undefined when the app key is missing', () => { + withTempDir((dir) => { + writeCdkJson(dir, JSON.stringify({ context: {} })); + expect(readCdkConfig(dir)).toEqual({ app: undefined }); + }); + }); + + test('returns undefined when the app value is not a string', () => { + withTempDir((dir) => { + writeCdkJson(dir, JSON.stringify({ app: 42 })); + expect(readCdkConfig(dir)).toEqual({ app: undefined }); + }); + }); + + test('returns undefined when cdk.json contains a JSON null', () => { + withTempDir((dir) => { + writeCdkJson(dir, 'null'); + expect(readCdkConfig(dir)).toEqual({ app: undefined }); + }); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/core/synth-runner.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/synth-runner.test.ts new file mode 100644 index 000000000..8981348da --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/core/synth-runner.test.ts @@ -0,0 +1,162 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { AssemblyError, ContextLookupsDisabledError, LockError, ToolkitError, type Toolkit } from '@aws-cdk/toolkit-lib'; +import { runSynth } from '../../lib/core/synth-runner'; + +const APP = 'npx ts-node bin/app.ts'; + +interface FakeCachedAssembly { + dispose: jest.Mock; +} + +interface FakeToolkit { + fromCdkApp: jest.Mock; + synth: jest.Mock; +} + +function makeToolkit(opts: { + synthThrow?: unknown; + disposeThrow?: unknown; +}): { toolkit: FakeToolkit; cached: FakeCachedAssembly } { + const cached: FakeCachedAssembly = { + dispose: jest.fn().mockImplementation(() => + opts.disposeThrow ? Promise.reject(opts.disposeThrow) : Promise.resolve(), + ), + }; + const toolkit: FakeToolkit = { + fromCdkApp: jest.fn().mockResolvedValue({}), + synth: jest.fn().mockImplementation(() => + opts.synthThrow ? Promise.reject(opts.synthThrow) : Promise.resolve(cached), + ), + }; + return { toolkit, cached }; +} + +const tempDirs: string[] = []; + +// Create a throwaway project dir. Pass an object to write its cdk.json +// (`{ app }` for a configured app, `{}` for a cdk.json with no app), or +// `undefined` for no cdk.json at all. runSynth reads the app from this on disk. +function makeProjectDir(cdkJson?: Record): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-synth-')); + tempDirs.push(dir); + if (cdkJson !== undefined) { + fs.writeFileSync(path.join(dir, 'cdk.json'), JSON.stringify(cdkJson)); + } + return dir; +} + +afterAll(() => { + for (const dir of tempDirs) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +function run(toolkit: FakeToolkit, projectDir: string) { + return runSynth({ toolkit: toolkit as unknown as Toolkit, projectDir }); +} + +describe('runSynth', () => { + test('reads the app from cdk.json, returns success, and disposes the cached assembly', async () => { + const { toolkit, cached } = makeToolkit({}); + const projectDir = makeProjectDir({ app: APP }); + + const result = await run(toolkit, projectDir); + + expect(result).toEqual({ status: 'success' }); + expect(toolkit.fromCdkApp).toHaveBeenCalledWith(APP, { workingDirectory: projectDir, lookups: false }); + expect(cached.dispose).toHaveBeenCalledTimes(1); + }); + + test('returns unavailable (without invoking the toolkit) when cdk.json has no app', async () => { + const { toolkit } = makeToolkit({}); + const projectDir = makeProjectDir({}); + + const result = await run(toolkit, projectDir); + + expect(result).toEqual({ status: 'unavailable' }); + expect(toolkit.fromCdkApp).not.toHaveBeenCalled(); + }); + + test('returns unavailable when cdk.json is missing entirely', async () => { + const { toolkit } = makeToolkit({}); + const projectDir = makeProjectDir(undefined); + + const result = await run(toolkit, projectDir); + + expect(result).toEqual({ status: 'unavailable' }); + expect(toolkit.fromCdkApp).not.toHaveBeenCalled(); + }); + + test('classifies AssemblyError as app-failure with the error message', async () => { + const { toolkit } = makeToolkit({ + synthThrow: AssemblyError.withCause('Assembly builder failed', new Error('TypeError: foo')), + }); + + const result = await run(toolkit, makeProjectDir({ app: APP })); + + expect(result).toEqual({ status: 'app-failure', message: expect.stringContaining('Assembly builder failed'), details: 'TypeError: foo' }); + }); + + test('classifies ConcurrentWriteLock as lock-conflict', async () => { + const { toolkit } = makeToolkit({ + synthThrow: new LockError('ConcurrentWriteLock', 'another CLI synthing'), + }); + + const result = await run(toolkit, makeProjectDir({ app: APP })); + + expect(result).toEqual({ status: 'lock-conflict' }); + }); + + test('classifies ConcurrentReadLock as lock-conflict', async () => { + const { toolkit } = makeToolkit({ + synthThrow: new LockError('ConcurrentReadLock', 'another CLI reading'), + }); + + const result = await run(toolkit, makeProjectDir({ app: APP })); + + expect(result).toEqual({ status: 'lock-conflict' }); + }); + + test('classifies ContextLookupsDisabledError as app-failure with the error message', async () => { + const { toolkit } = makeToolkit({ + synthThrow: new ContextLookupsDisabledError('Context lookups have been disabled. Run cdk synth in a terminal.'), + }); + + const result = await run(toolkit, makeProjectDir({ app: APP })); + + expect(result).toEqual({ status: 'app-failure', message: expect.stringContaining('cdk.context.json') }); + }); + + test('classifies an unknown ToolkitError as error', async () => { + const { toolkit } = makeToolkit({ + synthThrow: new ToolkitError('SomeUnexpected', 'unexpected'), + }); + + const result = await run(toolkit, makeProjectDir({ app: APP })); + + expect(result).toEqual({ status: 'error', message: 'unexpected' }); + }); + + test('classifies a plain Error as error', async () => { + const { toolkit } = makeToolkit({ + synthThrow: new Error('disk full'), + }); + + const result = await run(toolkit, makeProjectDir({ app: APP })); + + expect(result).toEqual({ status: 'error', message: 'disk full' }); + }); + + test('returns error when dispose fails after a successful synth', async () => { + const { toolkit, cached } = makeToolkit({ + disposeThrow: new Error('lock release failed'), + }); + + const result = await run(toolkit, makeProjectDir({ app: APP })); + + expect(result).toEqual({ status: 'error', message: 'lock release failed' }); + expect(cached.dispose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts index 57dc2cffc..81d46cbb1 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/codelens.test.ts @@ -5,6 +5,7 @@ import { pathToFileURL } from 'url'; import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; import type { ConstructNode } from '../../lib'; import { codeLensesForFile, OPEN_RESOURCE_COMMAND } from '../../lib/lsp/codelens'; +import { COMMAND_SYNTH_NOW, COMMAND_ENABLE_AUTO_SYNTH, COMMAND_DISABLE_AUTO_SYNTH } from '../../lib/lsp/commands'; const FILE = '/p/lib/stack.ts'; const URI = pathToFileURL(FILE).toString(); @@ -25,7 +26,7 @@ interface CommandChoice { describe('codeLensesForFile', () => { test('returns no lenses when tree is empty', async () => { - expect(await codeLensesForFile(ConstructIndex.fromTree([]), URI)).toEqual([]); + expect(await codeLensesForFile(ConstructIndex.fromTree([]), URI, false)).toEqual([]); }); test('returns no lenses for non-resource wrapper nodes (no logicalId)', async () => { @@ -36,7 +37,7 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, // logicalId/type intentionally omitted })]; - expect(await codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); + expect(await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false)).toEqual([]); }); test('emits one lens per resource on its source line', async () => { @@ -47,13 +48,13 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, })]; - const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); - expect(lenses).toHaveLength(1); - expect(lenses[0].range).toEqual({ + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false); + expect(lenses).toHaveLength(3); // 2 header + 1 L1 + expect(lenses[2].range).toEqual({ start: { line: 11, character: 0 }, end: { line: 11, character: 0 }, }); - expect(lenses[0].command?.title).toBe('Creates AWS::S3::Bucket'); + expect(lenses[2].command?.title).toBe('Creates AWS::S3::Bucket'); }); test('groups multiple resources on the same source line into one lens', async () => { @@ -80,9 +81,9 @@ describe('codeLensesForFile', () => { }), ]; - const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); - expect(lenses).toHaveLength(1); - expect(lenses[0].command?.title).toBe('Creates 3 resources: AWS::S3::Bucket, AWS::S3::BucketPolicy, AWS::KMS::Key'); + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false); + expect(lenses).toHaveLength(3); // 2 header + 1 grouped L1 + expect(lenses[2].command?.title).toBe('Creates 3 resources: AWS::S3::Bucket, AWS::S3::BucketPolicy, AWS::KMS::Key'); }); test('emits separate lenses for resources on different lines', async () => { @@ -101,9 +102,9 @@ describe('codeLensesForFile', () => { }), ]; - const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); - expect(lenses).toHaveLength(2); - expect(lenses.map((l) => l.range.start.line).sort((a, b) => a - b)).toEqual([9, 19]); + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false); + expect(lenses).toHaveLength(4); // 2 header + 2 L1 + expect(lenses.slice(2).map((l) => l.range.start.line).sort((a, b) => a - b)).toEqual([9, 19]); }); test('filters out resources from other files', async () => { @@ -123,14 +124,15 @@ describe('codeLensesForFile', () => { ]; const index = ConstructIndex.fromTree(tree); - // Each query returns only the resource defined in that file, which proves the - // URI filter selects by file rather than returning everything for any query. - const onThisFile = await codeLensesForFile(index, URI); - expect(onThisFile).toHaveLength(1); - expect(onThisFile[0].command?.title).toBe('Creates AWS::S3::Bucket'); - const onOtherFile = await codeLensesForFile(index, OTHER_URI); - expect(onOtherFile).toHaveLength(1); - expect(onOtherFile[0].command?.title).toBe('Creates AWS::SQS::Queue'); + // Each query returns 2 header lenses plus only the resource defined in + // that file, which proves the URI filter selects by file rather than + // returning everything for any query. + const onThisFile = await codeLensesForFile(index, URI, false); + expect(onThisFile).toHaveLength(3); // 2 header + 1 L1 + expect(onThisFile[2].command?.title).toBe('Creates AWS::S3::Bucket'); + const onOtherFile = await codeLensesForFile(index, OTHER_URI, false); + expect(onOtherFile).toHaveLength(3); // 2 header + 1 L1 + expect(onOtherFile[2].command?.title).toBe('Creates AWS::SQS::Queue'); }); test('walks descendants — finds resources nested under wrappers', async () => { @@ -155,9 +157,9 @@ describe('codeLensesForFile', () => { }), ]; - const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI); - expect(lenses).toHaveLength(1); - expect(lenses[0].command?.title).toContain('AWS::S3::Bucket'); + const lenses = await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false); + expect(lenses).toHaveLength(3); // 2 header + 1 + expect(lenses[2].command?.title).toContain('AWS::S3::Bucket'); }); test('omits resources without sourceLocation (non-TS apps)', async () => { @@ -168,7 +170,7 @@ describe('codeLensesForFile', () => { // sourceLocation omitted — non-TS app })]; - expect(await codeLensesForFile(ConstructIndex.fromTree(tree), URI)).toEqual([]); + expect(await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false)).toEqual([]); }); test('a single resource with a resolvable template gets a clickable openResource command', async () => { @@ -184,7 +186,7 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, })]; - const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false))[2]; expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); const choices = (lens.command!.arguments as CommandChoice[][])[0]; expect(choices).toHaveLength(1); @@ -212,7 +214,7 @@ describe('codeLensesForFile', () => { node({ path: 'Stack1/B/Policy', logicalId: 'B2', type: 'AWS::S3::BucketPolicy', templateFile, sourceLocation: { file: FILE, line: 12, column: 5 } }), ]; - const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false))[2]; const uri = pathToFileURL(templateFile).toString(); expect(lens.command?.command).toBe(OPEN_RESOURCE_COMMAND); const choices = (lens.command!.arguments as CommandChoice[][])[0]; @@ -235,7 +237,7 @@ describe('codeLensesForFile', () => { node({ path: 'Stack1/B/Policy', logicalId: 'B2', type: 'AWS::S3::BucketPolicy', templateFile: '/no/such.json', sourceLocation: { file: FILE, line: 12, column: 5 } }), ]; - const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false))[2]; expect(lens.command?.command).toBe(''); expect(lens.command?.arguments).toBeUndefined(); }); @@ -253,10 +255,35 @@ describe('codeLensesForFile', () => { sourceLocation: { file: FILE, line: 12, column: 5 }, })]; - const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI))[0]; + const lens = (await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false))[2]; expect(lens.command?.command).toBe(''); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); + + test('header lens appears at line 0 with synthNow command when L1 lenses are present', async () => { + const tree = [node({ + path: 'Stack1/MyBucket/Resource', + logicalId: 'MyBucketF68F3FF0', + type: 'AWS::S3::Bucket', + sourceLocation: { file: FILE, line: 12, column: 5 }, + })]; + + // auto-synth off: Synth now + Enable auto-synth + L1 + const lensesOff = await codeLensesForFile(ConstructIndex.fromTree(tree), URI, false); + expect(lensesOff[0].range.start.line).toBe(0); + expect(lensesOff[0].command?.command).toBe(COMMAND_SYNTH_NOW); + expect(lensesOff[1].command?.command).toBe(COMMAND_ENABLE_AUTO_SYNTH); + + // auto-synth on: Disable auto-synth + L1 (no Synth now) + const lensesOn = await codeLensesForFile(ConstructIndex.fromTree(tree), URI, true); + expect(lensesOn).toHaveLength(2); // 1 header + 1 L1 + expect(lensesOn[0].command?.command).toBe(COMMAND_DISABLE_AUTO_SYNTH); + }); + + test('no header lenses on files with no L1 lenses', async () => { + // File has no CDK resources → no lenses at all, including no header lenses + expect(await codeLensesForFile(ConstructIndex.fromTree([]), URI, false)).toEqual([]); + }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/commands.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/commands.test.ts new file mode 100644 index 000000000..787af03f7 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/commands.test.ts @@ -0,0 +1,122 @@ +import type { SynthRunResult } from '../../lib/core/synth-runner'; +import { + COMMAND_SYNTH_NOW, + COMMAND_ENABLE_AUTO_SYNTH, + COMMAND_DISABLE_AUTO_SYNTH, + executeCommand, + type CommandHandlerOptions, + type NotifySink, +} from '../../lib/lsp/commands'; + +interface CapturedNotify extends NotifySink { + info: jest.Mock; + error: jest.Mock; + withProgress: jest.Mock; + progressMessages: string[]; +} + +function createNotify(): CapturedNotify { + const progressMessages: string[] = []; + const info = jest.fn(); + const error = jest.fn(); + const withProgress = jest.fn(async (message: string, fn: () => Promise) => { + progressMessages.push(message); + return fn(); + }); + return { info, error, withProgress, progressMessages }; +} + +function makeOptions(overrides: Partial = {}): { + options: CommandHandlerOptions; + notify: CapturedNotify; + synth: jest.Mock; + toggleAutoSynth: jest.Mock; +} { + const notify = createNotify(); + const synth = jest.fn(async () => ({ status: 'success' } as SynthRunResult)); + const toggleAutoSynth = jest.fn(); + return { + notify, + synth, + toggleAutoSynth, + options: { synth, toggleAutoSynth, notify, ...overrides }, + }; +} + +describe('executeCommand', () => { + test('enableAutoSynth calls toggleAutoSynth(true)', async () => { + const { options, toggleAutoSynth } = makeOptions(); + await executeCommand(COMMAND_ENABLE_AUTO_SYNTH, [], options); + expect(toggleAutoSynth).toHaveBeenCalledWith(true); + }); + + test('disableAutoSynth calls toggleAutoSynth(false)', async () => { + const { options, toggleAutoSynth } = makeOptions(); + await executeCommand(COMMAND_DISABLE_AUTO_SYNTH, [], options); + expect(toggleAutoSynth).toHaveBeenCalledWith(false); + }); + test('synthNow surfaces an unavailable result as an info notification', async () => { + const synth = jest.fn(async () => ({ status: 'unavailable' } as SynthRunResult)); + const { options, notify } = makeOptions({ synth }); + + await executeCommand(COMMAND_SYNTH_NOW, [], options); + + expect(synth).toHaveBeenCalledTimes(1); + expect(notify.info).toHaveBeenCalledWith(expect.stringContaining('cdk.json')); + expect(notify.error).not.toHaveBeenCalled(); + }); + + test('synthNow runs synth under withProgress on success and notifies nothing', async () => { + const { options, notify, synth } = makeOptions(); + + await executeCommand(COMMAND_SYNTH_NOW, [], options); + + expect(synth).toHaveBeenCalledTimes(1); + expect(notify.progressMessages).toEqual([expect.stringContaining('Synthesizing')]); + expect(notify.info).not.toHaveBeenCalled(); + expect(notify.error).not.toHaveBeenCalled(); + }); + + test('synthNow surfaces app-failure as an error notification', async () => { + const { options, notify } = makeOptions({ + synth: jest.fn(async () => ({ status: 'app-failure', message: 'TypeError: x' })), + }); + + await executeCommand(COMMAND_SYNTH_NOW, [], options); + + expect(notify.error).toHaveBeenCalledWith(expect.stringContaining('TypeError: x')); + expect(notify.info).not.toHaveBeenCalled(); + }); + + test('synthNow surfaces lock-conflict as an info notification', async () => { + const { options, notify } = makeOptions({ + synth: jest.fn(async () => ({ status: 'lock-conflict' })), + }); + + await executeCommand(COMMAND_SYNTH_NOW, [], options); + + expect(notify.info).toHaveBeenCalledWith(expect.stringContaining('in progress')); + expect(notify.error).not.toHaveBeenCalled(); + }); + + test('synthNow surfaces a generic error as an error notification', async () => { + const { options, notify } = makeOptions({ + synth: jest.fn(async () => ({ status: 'error', message: 'disk full' })), + }); + + await executeCommand(COMMAND_SYNTH_NOW, [], options); + + expect(notify.error).toHaveBeenCalledWith(expect.stringContaining('disk full')); + expect(notify.info).not.toHaveBeenCalled(); + }); + + test('unknown commands are silently ignored', async () => { + const { options, notify, synth } = makeOptions(); + + await executeCommand('cdk.explorer.bogus', [], options); + + expect(synth).not.toHaveBeenCalled(); + expect(notify.info).not.toHaveBeenCalled(); + expect(notify.error).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/io-host.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/io-host.test.ts new file mode 100644 index 000000000..eb34d6f23 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/io-host.test.ts @@ -0,0 +1,61 @@ +import type { IoMessage, IoRequest } from '@aws-cdk/toolkit-lib'; +import type { RemoteConsole } from 'vscode-languageserver/node'; +import { LspIoHost } from '../../lib/lsp/io-host'; + +function makeConsole(): jest.Mocked> { + return { error: jest.fn(), warn: jest.fn(), info: jest.fn() }; +} + +function msg(level: string, message: string): IoMessage { + return { level, message, action: 'test', code: 'TEST_001', data: undefined, time: new Date() } as unknown as IoMessage; +} + +describe('LspIoHost', () => { + test('routes error level to console.error', async () => { + const console = makeConsole(); + const host = new LspIoHost(console as unknown as RemoteConsole); + await host.notify(msg('error', 'something broke')); + expect(console.error).toHaveBeenCalledWith('something broke'); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + }); + + test('routes warn level to console.warn', async () => { + const console = makeConsole(); + const host = new LspIoHost(console as unknown as RemoteConsole); + await host.notify(msg('warn', 'a warning')); + expect(console.warn).toHaveBeenCalledWith('a warning'); + }); + + test('routes info and default levels to console.info', async () => { + const console = makeConsole(); + const host = new LspIoHost(console as unknown as RemoteConsole); + await host.notify(msg('info', 'an info')); + await host.notify(msg('result', 'a result')); + expect(console.info).toHaveBeenCalledTimes(2); + }); + + test('suppresses debug and trace levels', async () => { + const console = makeConsole(); + const host = new LspIoHost(console as unknown as RemoteConsole); + await host.notify(msg('debug', 'verbose')); + await host.notify(msg('trace', 'very verbose')); + expect(console.error).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + }); + + test('requestResponse returns defaultResponse without blocking', async () => { + const console = makeConsole(); + const host = new LspIoHost(console as unknown as RemoteConsole); + const request = { + ...msg('info', 'MFA token?'), + defaultResponse: 'default-token', + } as unknown as IoRequest; + + const result = await host.requestResponse(request); + + expect(result).toBe('default-token'); + expect(console.info).toHaveBeenCalledWith(expect.stringContaining('default-token')); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index a5c7c568f..1ab055343 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -2,10 +2,31 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { pathToFileURL } from 'url'; +import { LockError } from '@aws-cdk/toolkit-lib'; import type { Diagnostic, InitializeParams } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; import type { AssemblyReadResult } from '../../lib'; -import { createLspHandlers, type LspHandlerOptions, type LspHandlers } from '../../lib/lsp/server'; +import type { SynthRunResult } from '../../lib/core/synth-runner'; +import { COMMAND_SYNTH_NOW, type NotifySink } from '../../lib/lsp/commands'; +import { createLspHandlers, type AssemblyLock, type LspHandlerOptions, type LspHandlers } from '../../lib/lsp/server'; + +function makeNotifySink(): NotifySink & { infoMessages: string[]; errorMessages: string[] } { + const infoMessages: string[] = []; + const errorMessages: string[] = []; + return { + infoMessages, + errorMessages, + info: (msg) => infoMessages.push(msg), + error: (msg) => errorMessages.push(msg), + withProgress: async (_msg, fn) => fn(), + }; +} + +/** Simple no-op assembly lock for tests that do not exercise lock contention. */ +const mockAssemblyLock = async (): Promise => ({ + release: async () => { + }, +}); interface CapturedClient { handlers: LspHandlers; @@ -17,22 +38,23 @@ interface CapturedClient { triggerWatcher: () => Promise; } -function createTestClient(opts?: Partial>): CapturedClient { +function createTestClient(opts?: Partial): CapturedClient { const published: Array<{ uri: string; diagnostics: Diagnostic[] }> = []; const log = { warn: jest.fn(), error: jest.fn() }; const refreshCodeLens = jest.fn(); const watcherClosed = jest.fn(); let watcherOnChange: (() => void) | undefined; const handlers = createLspHandlers({ - onSynthRequest: opts?.onSynthRequest, // Default to "no assembly" so tests that don't care about diagnostics // don't need a fake fixture. Tests that do care override this. readAssembly: opts?.readAssembly ?? (async () => ({ status: 'not-found' })), + // Fake assembly lock so refreshFromAssembly does not touch the real filesystem. + acquireAssemblyLock: opts?.acquireAssemblyLock ?? mockAssemblyLock, + synthRunner: opts?.synthRunner, + notify: opts?.notify, logger: log, onPublishDiagnostics: (uri, diagnostics) => published.push({ uri, diagnostics }), onRefreshCodeLenses: refreshCodeLens, - // Inject a fake watcher so unit tests never start a real chokidar instance; - // capture its onChange so tests can simulate a re-synth deterministically. startAssemblyWatcher: (watchOpts) => { watcherOnChange = watchOpts.onChange; return { @@ -108,57 +130,63 @@ function readAssemblyResolvingAfterFirst(): () => Promise { let call = 0; return async (): Promise => { call += 1; - return { - status: 'success', - data: call === 1 ? { warnings: [], tree, violations } : { warnings: [], tree }, - }; + return { status: 'success', data: call === 1 ? { warnings: [], tree, violations } : { warnings: [], tree } }; }; } describe('LSP Server', () => { test('initialize advertises codeLens, definition, and save-sync capabilities', () => { const client = createTestClient(); - const result = client.handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: { applicationDir: '/tmp/test-project' }, }); - expect(result).toMatchObject({ capabilities: { - textDocumentSync: { - openClose: false, - change: 0, - save: { includeText: false }, - }, + textDocumentSync: { openClose: false, change: 0, save: { includeText: false } }, codeLensProvider: { resolveProvider: false }, definitionProvider: true, }, }); }); - test('didSave triggers onSynthRequest for source files', async () => { - const synthRequests: string[] = []; - const client = createTestClient({ - onSynthRequest: (dir) => synthRequests.push(dir), - }); + test('didSave triggers auto-synth for non-ignored source files when auto-synth is enabled', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'success' }); + const client = createTestClient({ synthRunner }); await initializeClient(client, { applicationDir: '/tmp/test-project' }); + // Enable auto-synth via the toggle command first + await client.handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, }); + await new Promise((r) => setTimeout(r, 0)); - expect(synthRequests).toEqual(['/tmp/test-project']); + expect(synthRunner).toHaveBeenCalledTimes(1); }); - test('didSave does not trigger for ignored files', async () => { - const synthRequests: string[] = []; - const client = createTestClient({ - onSynthRequest: (dir) => synthRequests.push(dir), + test('didSave does not trigger synth when auto-synth is disabled (default)', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'success' }); + const client = createTestClient({ synthRunner }); + await initializeClient(client, { applicationDir: '/tmp/test-project' }); + + // auto-synth is off by default + client.handlers.onDidSaveTextDocument({ + textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, }); + await new Promise((r) => setTimeout(r, 0)); + + expect(synthRunner).not.toHaveBeenCalled(); + }); + + test('didSave does not trigger synth for ignored files', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'success' }); + const client = createTestClient({ synthRunner }); await initializeClient(client, { applicationDir: '/tmp/test-project' }); + await client.handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/node_modules/foo/index.ts' }, @@ -166,81 +194,167 @@ describe('LSP Server', () => { client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/cdk.out/tree.json' }, }); + await new Promise((r) => setTimeout(r, 0)); - expect(synthRequests).toEqual([]); + expect(synthRunner).not.toHaveBeenCalled(); }); - test('didSave does not throw without onSynthRequest configured', async () => { - const client = createTestClient(); + test('didSave is ignored after shutdown', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'success' }); + const client = createTestClient({ synthRunner }); await initializeClient(client, { applicationDir: '/tmp/test-project' }); + await client.handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + + client.handlers.onShutdown(); + client.handlers.onDidSaveTextDocument({ + textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, + }); + await new Promise((r) => setTimeout(r, 0)); + + expect(synthRunner).not.toHaveBeenCalled(); + }); + + test('auto-synth app-failure logs to output panel without throwing', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'app-failure', message: 'compile err' }); + const client = createTestClient({ synthRunner }); + await initializeClient(client, { applicationDir: '/tmp/test-project' }); + await client.handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); expect(() => client.handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, })).not.toThrow(); + await new Promise((r) => setTimeout(r, 0)); - // Server should still be responsive after didSave with no callback - expect(() => client.handlers.onShutdown()).not.toThrow(); + expect(client.log.error).toHaveBeenCalledWith(expect.stringContaining('compile err')); }); test('shutdown completes without error', async () => { const client = createTestClient(); await initializeClient(client); - expect(() => client.handlers.onShutdown()).not.toThrow(); }); - test('didSave is ignored after shutdown', async () => { - const synthRequests: string[] = []; + test('publishes diagnostics on initialized when assembly has violations', async () => { + const { tree, violations } = bucketViolationFixtures(); const client = createTestClient({ - onSynthRequest: (dir) => synthRequests.push(dir), + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree, violations } }), }); - await initializeClient(client, { applicationDir: '/tmp/test-project' }); - - client.handlers.onShutdown(); + await initializeClient(client, { applicationDir: '/p' }); + expect(client.published).toHaveLength(1); + expect(client.published[0].uri).toContain('stack.ts'); + expect(client.published[0].diagnostics).toHaveLength(1); + }); - client.handlers.onDidSaveTextDocument({ - textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, + test('acquires and releases the assembly lock around the assembly read', async () => { + const { tree, violations } = bucketViolationFixtures(); + const release = jest.fn(async () => { + }); + const acquireAssemblyLock = jest.fn(async () => ({ release })); + const client = createTestClient({ + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree, violations } }), + acquireAssemblyLock, }); + await initializeClient(client, { applicationDir: '/p' }); + expect(acquireAssemblyLock).toHaveBeenCalledWith(path.join('/p', 'cdk.out')); + expect(release).toHaveBeenCalledTimes(1); + expect(client.published).toHaveLength(1); // the read happened under the lock + }); - expect(synthRequests).toEqual([]); + test('retries while a synth holds the write lock, then skips without error once retries are exhausted', async () => { + jest.useFakeTimers(); + try { + const { tree, violations } = bucketViolationFixtures(); + const acquireAssemblyLock = jest.fn(async () => { + throw new LockError('ConcurrentWriteLock', 'a synth is writing'); + }); + const client = createTestClient({ + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree, violations } }), + acquireAssemblyLock, + }); + // onInitialized awaits refreshFromAssembly, which now polls for the lock. + // Drive the retry delays via fake timers rather than waiting in real time. + client.handlers.onInitialize({ + processId: null, + capabilities: {}, + rootUri: null, + initializationOptions: { applicationDir: '/p' }, + }); + const initialized = client.handlers.onInitialized(); + await jest.runAllTimersAsync(); + await initialized; + + expect(acquireAssemblyLock.mock.calls.length).toBeGreaterThan(1); // it retried + expect(client.published).toHaveLength(0); // never got the lock, so no read + expect(client.log.error).not.toHaveBeenCalled(); // contention is not an error + } finally { + jest.useRealTimers(); + } + }); + + test('retries the assembly lock on write-lock contention and refreshes once it clears', async () => { + jest.useFakeTimers(); + try { + const { tree, violations } = bucketViolationFixtures(); + const release = jest.fn(async () => { + }); + let calls = 0; + const acquireAssemblyLock = jest.fn(async () => { + calls += 1; + if (calls <= 3) throw new LockError('ConcurrentWriteLock', 'a synth is writing'); + return { release }; + }); + const client = createTestClient({ + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree, violations } }), + acquireAssemblyLock, + }); + client.handlers.onInitialize({ + processId: null, + capabilities: {}, + rootUri: null, + initializationOptions: { applicationDir: '/p' }, + }); + const initialized = client.handlers.onInitialized(); + await jest.runAllTimersAsync(); + await initialized; + + expect(acquireAssemblyLock).toHaveBeenCalledTimes(4); // 3 contended + 1 success + expect(release).toHaveBeenCalledTimes(1); // released after the successful read + expect(client.published).toHaveLength(1); // the violation was published + expect(client.published[0].diagnostics).toHaveLength(1); + } finally { + jest.useRealTimers(); + } }); - test('onSynthRequest errors are caught gracefully', async () => { + test('skips silently when there is no assembly yet (ENOENT)', async () => { + const { tree, violations } = bucketViolationFixtures(); const client = createTestClient({ - onSynthRequest: () => { - throw new Error('synth failed'); + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree, violations } }), + acquireAssemblyLock: async () => { + throw Object.assign(new Error('no cdk.out'), { code: 'ENOENT' }); }, }); - await initializeClient(client, { applicationDir: '/tmp/test-project' }); - - expect(() => client.handlers.onDidSaveTextDocument({ - textDocument: { uri: 'file:///tmp/test-project/lib/my-stack.ts' }, - })).not.toThrow(); - - // Server should still be responsive after the error - expect(() => client.handlers.onShutdown()).not.toThrow(); + await initializeClient(client, { applicationDir: '/p' }); + expect(client.published).toHaveLength(0); + expect(client.log.error).not.toHaveBeenCalled(); }); - test('publishes diagnostics on initialized when assembly has violations', async () => { + test('logs and skips on an unexpected lock error', async () => { const { tree, violations } = bucketViolationFixtures(); const client = createTestClient({ - readAssembly: async () => ({ - status: 'success', - data: { warnings: [], tree, violations }, - }), + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree, violations } }), + acquireAssemblyLock: async () => { + throw new Error('disk on fire'); + }, }); - await initializeClient(client, { applicationDir: '/p' }); - - expect(client.published).toHaveLength(1); - expect(client.published[0].uri).toContain('stack.ts'); - expect(client.published[0].diagnostics).toHaveLength(1); + expect(client.published).toHaveLength(0); + expect(client.log.error).toHaveBeenCalled(); }); - test('responds to codeLens with resources for the requested file', async () => { + test('responds to codeLens with header + resource lenses for the requested file', async () => { const stackTs = '/p/lib/stack.ts'; const stackUri = pathToFileURL(stackTs).toString(); - const client = createTestClient({ readAssembly: async (): Promise => ({ status: 'success', @@ -261,31 +375,21 @@ describe('LSP Server', () => { }, }), }); - await initializeClient(client, { applicationDir: '/p' }); - - const lenses = await client.handlers.onCodeLens({ - textDocument: { uri: stackUri }, - }); - - expect(lenses).toHaveLength(1); - expect(lenses[0].range.start.line).toBe(11); // 1-based 12 -> 0-based 11 - expect(lenses[0].command?.title).toBe('Creates AWS::S3::Bucket'); + const lenses = await client.handlers.onCodeLens({ textDocument: { uri: stackUri } }); + expect(lenses).toHaveLength(3); // 2 header + 1 L1 + expect(lenses[2].range.start.line).toBe(11); // 1-based 12 -> 0-based 11 + expect(lenses[2].command?.title).toBe('Creates AWS::S3::Bucket'); }); test('publishes nothing when assembly is not-found (pre-synth)', async () => { - const client = createTestClient({ - readAssembly: async () => ({ status: 'not-found' }), - }); - + const client = createTestClient({ readAssembly: async () => ({ status: 'not-found' }) }); await initializeClient(client, { applicationDir: '/p' }); - expect(client.published).toHaveLength(0); }); test('clears diagnostics for a violation resolved on a later refresh', async () => { const client = createTestClient({ readAssembly: readAssemblyResolvingAfterFirst() }); - await initializeClient(client, { applicationDir: '/p' }); expect(client.published).toHaveLength(1); const violationUri = client.published[0].uri; @@ -305,11 +409,7 @@ describe('LSP Server', () => { data: { warnings: [], tree: [{ path: 'Stack1', id: 'Stack1', children: [] }] }, }), }); - - await initializeClient(client, { applicationDir: '/p' }, { - workspace: { codeLens: { refreshSupport: true } }, - }); - + await initializeClient(client, { applicationDir: '/p' }, { workspace: { codeLens: { refreshSupport: true } } }); expect(client.refreshCodeLens).toHaveBeenCalledTimes(1); }); @@ -320,18 +420,13 @@ describe('LSP Server', () => { data: { warnings: [], tree: [{ path: 'Stack1', id: 'Stack1', children: [] }] }, }), }); - await initializeClient(client, { applicationDir: '/p' }); - expect(client.refreshCodeLens).not.toHaveBeenCalled(); }); test('a watcher-detected re-synth refreshes diagnostics and lenses', async () => { const client = createTestClient({ readAssembly: readAssemblyResolvingAfterFirst() }); - - await initializeClient(client, { applicationDir: '/p' }, { - workspace: { codeLens: { refreshSupport: true } }, - }); + await initializeClient(client, { applicationDir: '/p' }, { workspace: { codeLens: { refreshSupport: true } } }); expect(client.published).toHaveLength(1); const violationUri = client.published[0].uri; expect(client.refreshCodeLens).toHaveBeenCalledTimes(1); @@ -347,9 +442,7 @@ describe('LSP Server', () => { test('closes the cdk.out watcher on shutdown', async () => { const client = createTestClient(); await initializeClient(client, { applicationDir: '/p' }); - client.handlers.onShutdown(); - expect(client.watcherClosed).toHaveBeenCalledTimes(1); }); @@ -378,13 +471,11 @@ describe('LSP Server', () => { }), }); await initializeClient(client, { applicationDir: dir }); - const uri = pathToFileURL(templateFile).toString(); const position = TextDocument.create(uri, 'json', 0, text).positionAt(text.indexOf('AWS::S3::Bucket')); const target = await client.handlers.onDefinition({ textDocument: { uri }, position }); - expect(target?.uri).toBe(pathToFileURL('/p/lib/stack.ts').toString()); - expect(target?.range.start).toEqual({ line: 4, character: 2 }); // 1-based (5,3) -> 0-based + expect(target?.range.start).toEqual({ line: 4, character: 2 }); } finally { fs.rmSync(dir, { recursive: true, force: true }); } @@ -393,21 +484,353 @@ describe('LSP Server', () => { test('onDefinition returns undefined for a non-template document', async () => { const client = createTestClient(); await initializeClient(client, { applicationDir: '/p' }); - const target = await client.handlers.onDefinition({ + expect(await client.handlers.onDefinition({ textDocument: { uri: pathToFileURL('/p/lib/stack.ts').toString() }, position: { line: 0, character: 0 }, - }); - expect(target).toBeUndefined(); + })).toBeUndefined(); }); test('onDefinition returns undefined (does not throw) for a non-file URI', async () => { const client = createTestClient(); await initializeClient(client, { applicationDir: '/p' }); - expect( - await client.handlers.onDefinition({ - textDocument: { uri: 'untitled:Untitled-1' }, - position: { line: 0, character: 0 }, + expect(await client.handlers.onDefinition({ + textDocument: { uri: 'untitled:Untitled-1' }, + position: { line: 0, character: 0 }, + })).toBeUndefined(); + }); +}); + +describe('LSP Server -- executeCommand', () => { + function createCommandClient(opts: Partial = {}): { + handlers: LspHandlers; + notify: NotifySink & { infoMessages: string[]; errorMessages: string[] }; + } { + const notify = makeNotifySink(); + const handlers = createLspHandlers({ + readAssembly: async () => ({ status: 'not-found' }), + acquireAssemblyLock: mockAssemblyLock, + notify, + startAssemblyWatcher: (o) => { + return { + close: async () => { + }, + }; + }, + ...opts, + }); + handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: {} }); + void handlers.onInitialized(); + return { handlers, notify }; + } + + test('onInitialize advertises executeCommandProvider with synthNow', () => { + const { handlers } = createCommandClient(); + const result = handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: {} }); + expect(result.capabilities.executeCommandProvider?.commands).toEqual(expect.arrayContaining([COMMAND_SYNTH_NOW])); + }); + + test('synthNow with no app surfaces an unavailable info message', async () => { + const synthRunner = jest.fn, [string]>().mockResolvedValue({ status: 'unavailable' }); + const { handlers, notify } = createCommandClient({ synthRunner }); + await handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + expect(notify.infoMessages.some((m) => m.includes('unavailable'))).toBe(true); + }); + + test('synthNow with success is silent', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'success' }); + const { handlers, notify } = createCommandClient({ synthRunner }); + await handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + expect(notify.infoMessages).toHaveLength(0); + expect(notify.errorMessages).toHaveLength(0); + }); + + test('synthNow with app-failure notifies error', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'app-failure', message: 'compile error' }); + const { handlers, notify } = createCommandClient({ synthRunner }); + await handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + expect(notify.errorMessages.some((m) => m.includes('compile error'))).toBe(true); + }); + + test('synthNow in-flight latch: second concurrent call is suppressed as lock-conflict', async () => { + let resolveFirst!: () => void; + const firstSynthDone = new Promise((res) => { + resolveFirst = res; + }); + const synthRunner = jest.fn, []>() + .mockImplementationOnce(() => firstSynthDone.then(() => ({ status: 'success' } as const))) + .mockResolvedValue({ status: 'success' }); + const { handlers, notify } = createCommandClient({ synthRunner }); + + const first = handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + await handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + resolveFirst(); + await first; + + expect(notify.infoMessages.some((m) => m.includes('in progress'))).toBe(true); + expect(synthRunner).toHaveBeenCalledTimes(1); + }); + + test('didSave and executeCommand share the same in-flight latch', async () => { + let resolveFirst!: () => void; + const firstSynthDone = new Promise((res) => { + resolveFirst = res; + }); + const synthRunner = jest.fn, []>() + .mockImplementationOnce(() => firstSynthDone.then(() => ({ status: 'success' } as const))) + .mockResolvedValue({ status: 'success' }); + const notify = makeNotifySink(); + const handlers = createLspHandlers({ + readAssembly: async () => ({ status: 'not-found' }), + acquireAssemblyLock: mockAssemblyLock, + synthRunner, + notify, + startAssemblyWatcher: () => ({ + close: async () => { + }, }), - ).toBeUndefined(); + }); + handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: { applicationDir: '/p' } }); + void handlers.onInitialized(); + + // Enable auto-synth so didSave triggers a synth + await handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + + // Start a save-triggered synth (first, holds the latch) + handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///p/lib/stack.ts' } }); + + // Manual command fires while save-synth is in progress + await handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + resolveFirst(); + await new Promise((r) => setTimeout(r, 0)); + + // The manual command hit the latch → lock-conflict info message + expect(notify.infoMessages.some((m) => m.includes('in progress'))).toBe(true); + expect(synthRunner).toHaveBeenCalledTimes(1); + }); +}); + +describe('LSP Server -- auto-synth toggle', () => { + function createToggleClient(synthAvailable = true) { + const synthRunner = synthAvailable + ? jest.fn, []>().mockResolvedValue({ status: 'success' }) + : undefined; + const log = { warn: jest.fn(), error: jest.fn() }; + const refreshCodeLens = jest.fn(); + const handlers = createLspHandlers({ + readAssembly: async () => ({ status: 'not-found' }), + acquireAssemblyLock: mockAssemblyLock, + synthRunner, + logger: log, + onRefreshCodeLenses: refreshCodeLens, + startAssemblyWatcher: () => ({ + close: async () => { + }, + }), + }); + handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: { applicationDir: '/p' } }); + void handlers.onInitialized(); + return { handlers, synthRunner, refreshCodeLens, log }; + } + + const stackTs = '/p/lib/stack.ts'; + const stackUri = pathToFileURL(stackTs).toString(); + const treeWithResource = [{ + path: 'Stack1', + id: 'Stack1', + children: [{ + path: 'Stack1/R', + id: 'R', + logicalId: 'R1', + type: 'AWS::S3::Bucket', + sourceLocation: { file: stackTs, line: 5, column: 0 }, + children: [], + }], + }]; + + test('toggle round-trip: onCodeLens reflects new state after enableAutoSynth', async () => { + const handlers = createLspHandlers({ + readAssembly: async () => ({ status: 'success', data: { warnings: [], tree: treeWithResource } }), + acquireAssemblyLock: mockAssemblyLock, + synthRunner: jest.fn, []>().mockResolvedValue({ status: 'success' }), + onRefreshCodeLenses: jest.fn(), + startAssemblyWatcher: () => ({ + close: async () => { + }, + }), + }); + handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: { applicationDir: '/p' } }); + await handlers.onInitialized(); + + // Before toggle: 2 header lenses (Synth now + Enable auto-synth) + const before = await handlers.onCodeLens({ textDocument: { uri: stackUri } }); + expect(before[0].command?.title).toBe('↻ Synth now'); + expect(before[1].command?.title).toBe('▶ Enable auto-synth'); + + // Toggle on + await handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + + // After toggle: 1 header lens (Disable auto-synth) + const after = await handlers.onCodeLens({ textDocument: { uri: stackUri } }); + expect(after[0].command?.title).toBe('⏹ Disable auto-synth'); + expect(after).toHaveLength(2); // 1 header + 1 L1 + }); + + test('toggle fires onRefreshCodeLenses', async () => { + const { handlers, refreshCodeLens } = createToggleClient(); + await handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + expect(refreshCodeLens).toHaveBeenCalledTimes(1); + await handlers.onExecuteCommand({ command: 'cdk.explorer.disableAutoSynth' }); + expect(refreshCodeLens).toHaveBeenCalledTimes(2); + }); + + test('save-path unavailable result is silent (no log output)', async () => { + const synthRunner = jest.fn, [string]>().mockResolvedValue({ status: 'unavailable' }); + const log = { warn: jest.fn(), error: jest.fn() }; + const handlers = createLspHandlers({ + readAssembly: async () => ({ status: 'not-found' }), + acquireAssemblyLock: mockAssemblyLock, + synthRunner, + logger: log, + startAssemblyWatcher: () => ({ + close: async () => { + }, + }), + }); + handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: { applicationDir: '/p' } }); + await handlers.onInitialized(); + await handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + + handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///p/lib/stack.ts' } }); + await new Promise((r) => setTimeout(r, 0)); + + // With no app the runner returns 'unavailable', a silent no-op on the save + // path: there is no app to synth, so nothing is reported. + expect(synthRunner).toHaveBeenCalledTimes(1); + expect(log.error).not.toHaveBeenCalled(); + expect(log.warn).not.toHaveBeenCalled(); + }); + + test('save-path lock-conflict is silent (no log output)', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'lock-conflict' }); + const log = { warn: jest.fn(), error: jest.fn() }; + const handlers = createLspHandlers({ + readAssembly: async () => ({ status: 'not-found' }), + acquireAssemblyLock: mockAssemblyLock, + synthRunner, + logger: log, + startAssemblyWatcher: () => ({ + close: async () => { + }, + }), + }); + handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: { applicationDir: '/p' } }); + await handlers.onInitialized(); + await handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + + handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///p/lib/stack.ts' } }); + await new Promise((r) => setTimeout(r, 0)); + + expect(log.error).not.toHaveBeenCalled(); + expect(log.warn).not.toHaveBeenCalled(); + }); + + test('save-path error result is logged', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ status: 'error', message: 'unexpected failure' }); + const log = { warn: jest.fn(), error: jest.fn() }; + const handlers = createLspHandlers({ + readAssembly: async () => ({ status: 'not-found' }), + acquireAssemblyLock: mockAssemblyLock, + synthRunner, + logger: log, + startAssemblyWatcher: () => ({ + close: async () => { + }, + }), + }); + handlers.onInitialize({ processId: null, capabilities: {}, rootUri: null, initializationOptions: { applicationDir: '/p' } }); + await handlers.onInitialized(); + await handlers.onExecuteCommand({ command: 'cdk.explorer.enableAutoSynth' }); + + handlers.onDidSaveTextDocument({ textDocument: { uri: 'file:///p/lib/stack.ts' } }); + await new Promise((r) => setTimeout(r, 0)); + + expect(log.error).toHaveBeenCalledWith(expect.stringContaining('unexpected failure')); + }); +}); + +describe('LSP Server -- synth-failure diagnostics', () => { + test('app-failure publishes a diagnostic on the failing source file', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ + status: 'app-failure', + message: 'Subprocess exited with error 1', + details: 'lib/stack.ts:12:5 - error TS2322: nope', + }); + const client = createTestClient({ synthRunner }); + await initializeClient(client, { applicationDir: '/p' }); + + await client.handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + + const onFile = client.published.find((p) => p.uri === pathToFileURL('/p/lib/stack.ts').toString()); + expect(onFile?.diagnostics).toHaveLength(1); + expect(onFile?.diagnostics[0].message).toContain('TS2322'); + }); + + test('publishes a diagnostic per failing file', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ + status: 'app-failure', + message: 'm', + details: 'lib/stack.ts(1,1): error TS1000: a\nbin/app.ts(2,2): error TS1001: b', + }); + const client = createTestClient({ synthRunner }); + await initializeClient(client, { applicationDir: '/p' }); + + await client.handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + + expect(client.published.find((p) => p.uri === pathToFileURL('/p/lib/stack.ts').toString())?.diagnostics).toHaveLength(1); + expect(client.published.find((p) => p.uri === pathToFileURL('/p/bin/app.ts').toString())?.diagnostics).toHaveLength(1); + }); + + test('app-failure without a parseable location falls back to cdk.json', async () => { + const synthRunner = jest.fn, []>().mockResolvedValue({ + status: 'app-failure', + message: 'context needed', + }); + const client = createTestClient({ synthRunner }); + await initializeClient(client, { applicationDir: '/p' }); + + await client.handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + + const onCdkJson = client.published.find((p) => p.uri === pathToFileURL('/p/cdk.json').toString()); + expect(onCdkJson?.diagnostics[0].message).toBe('context needed'); + }); + + test('a successful synth clears a prior synth-failure diagnostic', async () => { + const synthRunner = jest.fn, []>() + .mockResolvedValueOnce({ status: 'app-failure', message: 'm', details: 'lib/stack.ts:1:1 - error TS1000: x' }) + .mockResolvedValueOnce({ status: 'success' }); + const client = createTestClient({ synthRunner }); + await initializeClient(client, { applicationDir: '/p' }); + + await client.handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + await client.handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + + const uri = pathToFileURL('/p/lib/stack.ts').toString(); + const entriesForFile = client.published.filter((p) => p.uri === uri); + expect(entriesForFile[entriesForFile.length - 1].diagnostics).toEqual([]); + }); + + test('an unavailable result clears a prior synth-failure diagnostic', async () => { + const synthRunner = jest.fn, [string]>() + .mockResolvedValueOnce({ status: 'app-failure', message: 'm', details: 'lib/stack.ts:1:1 - error TS1000: x' }) + .mockResolvedValueOnce({ status: 'unavailable' }); + const client = createTestClient({ synthRunner }); + await initializeClient(client, { applicationDir: '/p' }); + + await client.handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + await client.handlers.onExecuteCommand({ command: COMMAND_SYNTH_NOW }); + + const uri = pathToFileURL('/p/lib/stack.ts').toString(); + const entriesForFile = client.published.filter((p) => p.uri === uri); + expect(entriesForFile[entriesForFile.length - 1].diagnostics).toEqual([]); }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/synth-diagnostics.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/synth-diagnostics.test.ts new file mode 100644 index 000000000..dec4543f7 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/synth-diagnostics.test.ts @@ -0,0 +1,90 @@ +import { pathToFileURL } from 'url'; +import { DiagnosticSeverity } from 'vscode-languageserver/node'; +import type { SynthRunResult } from '../../lib/core/synth-runner'; +import { parseSynthErrors, synthFailureDiagnostics } from '../../lib/lsp/synth-diagnostics'; + +describe('parseSynthErrors', () => { + test('parses multiple ts-node (pretty) errors', () => { + const locs = parseSynthErrors( + 'lib/stack.ts:12:5 - error TS2322: Type A.\nlib/stack.ts:20:9 - error TS2554: Type B.\n', + ); + expect(locs).toEqual([ + { file: 'lib/stack.ts', line: 12, column: 5, message: 'error TS2322: Type A.' }, + { file: 'lib/stack.ts', line: 20, column: 9, message: 'error TS2554: Type B.' }, + ]); + }); + + test('parses multiple tsc-format errors (ts-node default)', () => { + const locs = parseSynthErrors( + 'lib/stack.ts(1,7): error TS2322: x\nbin/app.ts(3,1): error TS1005: y\n', + ); + expect(locs).toEqual([ + { file: 'lib/stack.ts', line: 1, column: 7, message: 'error TS2322: x' }, + { file: 'bin/app.ts', line: 3, column: 1, message: 'error TS1005: y' }, + ]); + }); + + test('returns empty when nothing matches', () => { + expect(parseSynthErrors('unrelated output')).toEqual([]); + expect(parseSynthErrors(undefined)).toEqual([]); + }); +}); + +describe('synthFailureDiagnostics', () => { + test('groups multiple errors in the same file into one entry', () => { + const result: SynthRunResult = { + status: 'app-failure', + message: 'summary', + details: 'lib/stack.ts(1,7): error TS2322: a\nlib/stack.ts(2,3): error TS1005: b', + }; + + const out = synthFailureDiagnostics(result, '/p'); + + expect(out).toHaveLength(1); + expect(out[0].uri).toBe(pathToFileURL('/p/lib/stack.ts').toString()); + expect(out[0].diagnostics).toHaveLength(2); + expect(out[0].diagnostics[0]).toEqual({ + range: { start: { line: 0, character: 6 }, end: { line: 0, character: Number.MAX_VALUE } }, + severity: DiagnosticSeverity.Error, + source: 'cdk synth', + message: 'error TS2322: a', + }); + }); + + test('produces one entry per file', () => { + const result: SynthRunResult = { + status: 'app-failure', + message: 'summary', + details: 'lib/stack.ts(1,1): error TS1: a\nbin/app.ts(2,2): error TS2: b', + }; + + const uris = synthFailureDiagnostics(result, '/p').map((o) => o.uri).sort(); + + expect(uris).toEqual([ + pathToFileURL('/p/bin/app.ts').toString(), + pathToFileURL('/p/lib/stack.ts').toString(), + ].sort()); + }); + + test('falls back to cdk.json when nothing parses', () => { + const out = synthFailureDiagnostics({ status: 'app-failure', message: 'context needed' }, '/p'); + expect(out).toHaveLength(1); + expect(out[0].uri).toBe(pathToFileURL('/p/cdk.json').toString()); + expect(out[0].diagnostics[0].message).toBe('context needed'); + }); + + test('ignores errors outside the project (falls back to cdk.json)', () => { + const out = synthFailureDiagnostics( + { status: 'app-failure', message: 'summary', details: '../evil.ts(1,1): error TS1: x' }, + '/p', + ); + expect(out).toHaveLength(1); + expect(out[0].uri).toBe(pathToFileURL('/p/cdk.json').toString()); + }); + + test('returns empty for non-app-failure outcomes', () => { + expect(synthFailureDiagnostics({ status: 'success' }, '/p')).toEqual([]); + expect(synthFailureDiagnostics({ status: 'lock-conflict' }, '/p')).toEqual([]); + expect(synthFailureDiagnostics({ status: 'error', message: 'x' }, '/p')).toEqual([]); + }); +}); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts index 5f427417d..c2bcef2f9 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts @@ -1,7 +1,7 @@ import { AsyncDisposableBox } from './disposable-box'; import * as contextproviders from '../../../context-providers'; import type { ToolkitServices } from '../../../toolkit/private'; -import { ToolkitError } from '../../../toolkit/toolkit-error'; +import { ContextLookupsDisabledError } from '../../../toolkit/toolkit-error'; import type { IoHelper } from '../../io/private'; import { IO } from '../../io/private'; import type { IContextStore } from '../context-store'; @@ -77,8 +77,7 @@ export class ContextAwareCloudAssemblySource implements ICloudAssemblySource { const missingKeys = Array.from(missingKeysSet); if (!this.canLookup) { - throw new ToolkitError( - 'ContextLookupsDisabled', + throw new ContextLookupsDisabledError( 'Context lookups have been disabled. ' + 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. ' + `Missing context keys: '${missingKeys.join(', ')}'`); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts b/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts index f91f54315..cfbb18b5d 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'fs'; import * as path from 'path'; -import { ToolkitError } from '../toolkit/toolkit-error'; +import { LockError } from '../toolkit/toolkit-error'; /** * A single-writer/multi-reader lock on a directory @@ -34,7 +34,7 @@ export class RWLock { const readers = await this._currentReaders(); if (readers.length > 0) { - throw new ToolkitError('ConcurrentReadLock', `Other CLIs (PID=${readers}) are currently reading from ${this.directory}. Invoke the CLI in sequence, or use '--output' to synth into different directories.`); + throw new LockError('ConcurrentReadLock', `Other CLIs (PID=${readers}) are currently reading from ${this.directory}. Invoke the CLI in sequence, or use '--output' to synth into different directories.`); } await writeFileAtomic(this.writerFile, this.pidString); @@ -101,7 +101,7 @@ export class RWLock { private async assertNoOtherWriters() { const writer = await this._currentWriter(); if (writer) { - throw new ToolkitError('ConcurrentWriteLock', `Another CLI (PID=${writer}) is currently synthing to ${this.directory}. Invoke the CLI in sequence, or use '--output' to synth into different directories.`); + throw new LockError('ConcurrentWriteLock', `Another CLI (PID=${writer}) is currently synthing to ${this.directory}. Invoke the CLI in sequence, or use '--output' to synth into different directories.`); } } diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit-error.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit-error.ts index 695c01d58..876feaa72 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit-error.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit-error.ts @@ -7,6 +7,8 @@ const DEPLOYMENT_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.DeploymentError const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.AssemblyError'); const CONTEXT_PROVIDER_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ContextProviderError'); const NO_RESULTS_FOUND_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.NoResultsFoundError'); +const LOCK_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.LockError'); +const CONTEXT_LOOKUPS_DISABLED_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ContextLookupsDisabledError'); /** * Represents a general toolkit error in the AWS CDK Toolkit. @@ -47,6 +49,20 @@ export class ToolkitError extends Error { return ToolkitError.isToolkitError(x) && ASSEMBLY_ERROR_SYMBOL in x; } + /** + * Determines if a given error is an instance of LockError. + */ + public static isLockError(x: any): x is LockError { + return ToolkitError.isToolkitError(x) && LOCK_ERROR_SYMBOL in x; + } + + /** + * Determines if a given error is an instance of ContextLookupsDisabledError. + */ + public static isContextLookupsDisabledError(x: any): x is ContextLookupsDisabledError { + return ToolkitError.isToolkitError(x) && CONTEXT_LOOKUPS_DISABLED_ERROR_SYMBOL in x; + } + /** * Determines if a given error is an instance of ContextProviderError. */ @@ -125,6 +141,42 @@ export class AbortError extends ToolkitError { } } +/** + * Represents a failure to acquire the read/write lock on the cloud assembly + * output directory, because another CLI is reading from or writing to it. + */ +export class LockError extends ToolkitError { + /** + * Denotes the source of the error as user. + */ + public readonly source = 'user'; + + constructor(errorCode: string, message: string) { + super(errorCode, message, 'lock'); + Object.setPrototypeOf(this, LockError.prototype); + Object.defineProperty(this, LOCK_ERROR_SYMBOL, { value: true }); + } +} + +/** + * Represents synthesis that could not complete because the app needs context + * lookups that are not cached, and lookups are disabled. The fix is to run + * `cdk synth` in a terminal once (with AWS credentials) so the values are + * written to `cdk.context.json`. + */ +export class ContextLookupsDisabledError extends ToolkitError { + /** + * Denotes the source of the error as user. + */ + public readonly source = 'user'; + + constructor(message: string) { + super('ContextLookupsDisabled', message); + Object.setPrototypeOf(this, ContextLookupsDisabledError.prototype); + Object.defineProperty(this, CONTEXT_LOOKUPS_DISABLED_ERROR_SYMBOL, { value: true }); + } +} + /** * Represents an error causes by cloud assembly synthesis * diff --git a/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit-error.test.ts b/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit-error.test.ts index 2f409e6f3..eaae98234 100644 --- a/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit-error.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/toolkit/toolkit-error.test.ts @@ -1,4 +1,4 @@ -import { AssemblyError, AuthenticationError, ContextProviderError, NoResultsFoundError, ToolkitError, AbortError } from '../../lib/toolkit/toolkit-error'; +import { AbortError, AssemblyError, AuthenticationError, ContextLookupsDisabledError, ContextProviderError, LockError, NoResultsFoundError, ToolkitError } from '../../lib/toolkit/toolkit-error'; describe('toolkit error', () => { let toolkitError = new ToolkitError('TestError', 'Test toolkit error'); @@ -9,6 +9,8 @@ describe('toolkit error', () => { let assemblyCauseError = AssemblyError.withCause('Test authentication error', new Error('other error')); let noResultsError = new NoResultsFoundError('Test no results error'); let abortError = new AbortError('TestAborted'); + let lockError = new LockError('ConcurrentWriteLock', 'Test lock error'); + let contextLookupsDisabledError = new ContextLookupsDisabledError('Test context lookups disabled error'); test('types are correctly assigned', async () => { expect(toolkitError.type).toBe('toolkit'); @@ -17,6 +19,7 @@ describe('toolkit error', () => { expect(assemblyCauseError.type).toBe('assembly'); expect(contextProviderError.type).toBe('context-provider'); expect(noResultsError.type).toBe('context-provider'); + expect(lockError.type).toBe('lock'); expect(abortError.type).toBe('abort'); }); @@ -54,6 +57,22 @@ describe('toolkit error', () => { expect(ToolkitError.isAbortError(authError)).toBe(false); }); + test('isLockError works', () => { + expect(lockError.source).toBe('user'); + + expect(ToolkitError.isLockError(lockError)).toBe(true); + expect(ToolkitError.isLockError(toolkitError)).toBe(false); + expect(ToolkitError.isLockError(authError)).toBe(false); + }); + + test('isContextLookupsDisabledError works', () => { + expect(contextLookupsDisabledError.source).toBe('user'); + + expect(ToolkitError.isContextLookupsDisabledError(contextLookupsDisabledError)).toBe(true); + expect(ToolkitError.isContextLookupsDisabledError(toolkitError)).toBe(false); + expect(ToolkitError.isContextLookupsDisabledError(lockError)).toBe(false); + }); + describe('isAssemblyError works', () => { test('AssemblyError.fromStacks', () => { expect(assemblyError.source).toBe('user'); From 01f3ff60c0269a3cd97ccc5eadf1e8d4be8ec04e Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:07:11 -0400 Subject: [PATCH 16/28] feat: construct tree and violations (#1653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two read-only views to the `cdk explore` web UI. - **Construct tree** (`GET /api/tree`): the synthesized construct hierarchy. Each node is flagged with the highest severity of any policy violation on it, so problem areas are visible at a glance. - **Policy-validation panel** (`GET /api/policy-validation`): violations grouped by rule, sorted by severity, labeled with the originating plugin. The server reads the cloud assembly in `cdk.out` and returns a wire-stable, app-relative view. The backend owns the full transform from core construct node to displayed node, including default-child collapse and the per-node highest-severity join, so the frontend renders without re-deriving any of it. ## Out of scope - Navigation between a construct, its synthesized template, and its source. The wire model already carries `templateFile` and `sourceLocation`, but no navigation UI yet. - Frontend tests (adds deps, so future PR) frontend: Screenshot 2026-07-01 at 11 32
09 AM ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../@aws-cdk/cdk-explorer/frontend/App.tsx | 282 +- .../@aws-cdk/cdk-explorer/frontend/api.ts | 29 +- .../frontend/components/ConstructTree.tsx | 130 + .../frontend/components/ViolationsPanel.tsx | 72 + .../@aws-cdk/cdk-explorer/frontend/index.html | 4 + .../@aws-cdk/cdk-explorer/lib/web/protocol.ts | 111 + .../@aws-cdk/cdk-explorer/lib/web/routes.ts | 212 +- .../@aws-cdk/cdk-explorer/lib/web/server.ts | 11 +- .../@aws-cdk/cdk-explorer/lib/web/severity.ts | 67 + .../test/web/normalize-violations.test.ts | 127 + .../cdk-explorer/test/web/routes.test.ts | 144 + .../cdk-explorer/test/web/server.test.ts | 7 + .../cdk-explorer/test/web/to-web-node.test.ts | 289 ++ packages/aws-cdk/THIRD_PARTY_LICENSES | 3253 +++++++++++++---- 14 files changed, 4030 insertions(+), 708 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/severity.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/normalize-violations.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/to-web-node.test.ts diff --git a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx index ee3809586..d4511a73e 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx @@ -1,40 +1,272 @@ import Box from '@cloudscape-design/components/box'; import Container from '@cloudscape-design/components/container'; -import ContentLayout from '@cloudscape-design/components/content-layout'; import Grid from '@cloudscape-design/components/grid'; import Header from '@cloudscape-design/components/header'; -import SpaceBetween from '@cloudscape-design/components/space-between'; +import Spinner from '@cloudscape-design/components/spinner'; import * as React from 'react'; +import { api, type TreeResponse, type ViolationsResponse } from './api'; +import { ConstructTree } from './components/ConstructTree'; import { FilePane } from './components/FilePane'; +import { ViolationsPanel } from './components/ViolationsPanel'; -/** Web explorer shell: Resource Tree (left), two file panes, Violations (bottom). Tree/violations are placeholders until the cloud-assembly reader is wired in. */ +/** Web explorer shell. Two splits: horizontal between the construct tree and the file panes, vertical between the top row and the violations panel. Each split has a draggable resizer with an arrow toggle that collapses one side. */ export function App(): JSX.Element { + const [tree, setTree] = React.useState(); + const [violations, setViolations] = React.useState(); + const [error, setError] = React.useState(); + const [appDir, setAppDir] = React.useState(); + + React.useEffect(() => { + Promise.all([api.getTree(), api.getViolations(), api.getAppInfo()]) + .then(([t, v, info]) => { + setTree(t); + setViolations(v); + setAppDir(info.appDir); + setError(undefined); + }) + .catch((err) => setError(err instanceof Error ? err.message : String(err))); + }, []); + + // Vertical split: violations row's share of the page height (default 33%). + const vSplit = useSplit({ orientation: 'vertical', defaultFraction: 0.33, min: 0.15, max: 0.85 }); + // Horizontal split inside the top row: file panes' share of that row's width (default 75%; tree gets the remaining 25%). + const hSplit = useSplit({ orientation: 'horizontal', defaultFraction: 0.75, min: 0.4, max: 0.85 }); + return ( - - CDK Web Explorer - - } - > - - - Resource Tree}> - - Construct tree appears here once the cloud-assembly reader is wired in. - - +
+
+
CDK Web Explorer
+
+ {error && {error}} +
+
+ {!hSplit.collapsed && ( +
+ Construct Tree}> + + +
+ )} +
+ +
- - Violations}> - - Policy-validation violations appear here once the cloud-assembly reader is wired in. - - - - +
+
+
+ + {!vSplit.collapsed && ( +
+ Violations}> + + +
+ )} +
+
+ ); +} + +function ConstructTreeContent({ tree }: { readonly tree?: TreeResponse }): JSX.Element { + if (!tree) return ; + if (tree.status === 'not-synthesized') { + return No cloud assembly found. Run cdk synth first.; + } + return ; +} + +function ViolationsContent({ violations }: { readonly violations?: ViolationsResponse }): JSX.Element { + if (!violations) return ; + if (violations.status === 'not-synthesized') { + return No cloud assembly found.; + } + return ; +} + +interface SplitOptions { + /** 'horizontal' = the resizer moves left/right (separating columns). 'vertical' = the resizer moves up/down (separating rows). */ + readonly orientation: 'horizontal' | 'vertical'; + readonly defaultFraction: number; + readonly min: number; + readonly max: number; +} + +interface Split extends SplitOptions { + /** Fraction the *trailing* pane (right column / bottom row) takes of its container; 0..1. */ + readonly fraction: number; + readonly collapsed: boolean; + readonly toggleCollapsed: () => void; + readonly startDrag: (e: React.MouseEvent) => void; + readonly containerRef: React.RefObject; +} + +/** Drives a resizable / collapsible split. Drag updates the fraction live; the arrow toggles full collapse of the trailing pane. */ +function useSplit(opts: SplitOptions): Split { + const containerRef = React.useRef(null); + const [fraction, setFraction] = React.useState(opts.defaultFraction); + const [collapsed, setCollapsed] = React.useState(false); + + const startDrag = React.useCallback((e: React.MouseEvent) => { + if (collapsed) return; // Drag while collapsed makes no sense; user must expand first via the arrow. + e.preventDefault(); + const container = containerRef.current; + if (!container) return; + document.body.style.userSelect = 'none'; + document.body.style.cursor = opts.orientation === 'vertical' ? 'row-resize' : 'col-resize'; + const onMove = (ev: MouseEvent) => { + const rect = container.getBoundingClientRect(); + // Trailing pane share grows as the pointer moves toward the leading edge. + const frac = opts.orientation === 'vertical' + ? (rect.bottom - ev.clientY) / rect.height + : (rect.right - ev.clientX) / rect.width; + setFraction(Math.min(opts.max, Math.max(opts.min, frac))); + }; + const onUp = () => { + document.body.style.userSelect = ''; + document.body.style.cursor = ''; + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }, [collapsed, opts.orientation, opts.min, opts.max]); + + const toggleCollapsed = React.useCallback(() => setCollapsed((c) => !c), []); + + return { ...opts, fraction, collapsed, toggleCollapsed, startDrag, containerRef }; +} + +/** Thin strip pinned to the edge of the trailing pane. The whole strip is the drag handle; a centered arrow toggles collapse. */ +function Resizer({ split }: { readonly split: Split }): JSX.Element { + const isVertical = split.orientation === 'vertical'; + // Arrow points the way the boundary travels on collapse, and flips once collapsed. + const arrow = isVertical + ? (split.collapsed ? '▲' : '▼') + : (split.collapsed ? '▶' : '◀'); + const verb = isVertical + ? (split.collapsed ? 'Expand violations' : 'Collapse violations') + : (split.collapsed ? 'Expand construct tree' : 'Collapse construct tree'); + return ( +
+ +
); } + +function topRowStyle(vSplit: Split): React.CSSProperties { + return { + display: 'flex', + minHeight: 0, + alignItems: 'stretch', + flexDirection: 'row', + flex: vSplit.collapsed ? '1 1 auto' : `${1 - vSplit.fraction} 1 0`, + }; +} + +function bottomRowStyle(vSplit: Split): React.CSSProperties { + return { + display: 'flex', + flexDirection: 'column', + minHeight: 0, + flex: vSplit.collapsed ? '0 0 auto' : `${vSplit.fraction} 1 0`, + }; +} + +function treePaneStyle(hSplit: Split): React.CSSProperties { + return { + flex: hSplit.collapsed ? '0 0 0' : `0 0 ${(1 - hSplit.fraction) * 100}%`, + minWidth: 0, + minHeight: 0, + display: 'flex', + }; +} + +function filesPaneStyle(hSplit: Split): React.CSSProperties { + return { + flex: hSplit.collapsed ? '1 1 auto' : `0 0 ${hSplit.fraction * 100}%`, + minWidth: 0, + minHeight: 0, + }; +} + +function resizerStyle(split: Split): React.CSSProperties { + if (split.orientation === 'vertical') { + return { + flexShrink: 0, + height: split.collapsed ? '20px' : '10px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: split.collapsed ? 'pointer' : 'row-resize', + position: 'relative', + marginBottom: split.collapsed ? 0 : '-4px', + }; + } + return { + flexShrink: 0, + width: split.collapsed ? '20px' : '10px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: split.collapsed ? 'pointer' : 'col-resize', + position: 'relative', + // Slide 4px left so the pill straddles the tree pane's border. + marginLeft: split.collapsed ? 0 : '-4px', + }; +} + +const PAGE_STYLE: React.CSSProperties = { + height: '100vh', + display: 'flex', + flexDirection: 'column', + padding: '16px', + boxSizing: 'border-box', + overflow: 'hidden', +}; +const TITLE_BLOCK_STYLE: React.CSSProperties = { flexShrink: 0, marginBottom: '12px' }; +/** Wraps a Cloudscape Container so it stretches to fill its flex parent (Container is intrinsically sized). */ +const GROW_STYLE: React.CSSProperties = { flex: '1 1 0', minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }; + +const RESIZER_BUTTON_BASE: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + border: '1px solid #d1d5db', + background: '#ffffff', + color: '#5f6b7a', + fontSize: '10px', + lineHeight: 1, + cursor: 'pointer', + zIndex: 1, + padding: 0, +}; +const RESIZER_BUTTON_HORIZONTAL: React.CSSProperties = { + ...RESIZER_BUTTON_BASE, + width: '40px', + height: '14px', + borderRadius: '7px', +}; +const RESIZER_BUTTON_VERTICAL: React.CSSProperties = { + ...RESIZER_BUTTON_BASE, + width: '14px', + height: '40px', + borderRadius: '7px', +}; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/api.ts b/packages/@aws-cdk/cdk-explorer/frontend/api.ts index 3e1c9ed90..f23f22b3d 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/api.ts +++ b/packages/@aws-cdk/cdk-explorer/frontend/api.ts @@ -1,6 +1,24 @@ -import type { DirEntry, FilesResponse, FileResponse } from '../lib/web/protocol'; +import type { + DirEntry, + FilesResponse, + FileResponse, + TreeResponse, + ViolationsResponse, + WebConstructNode, + WebViolation, + WebViolationOccurrence, +} from '../lib/web/protocol'; -export type { DirEntry, FilesResponse, FileResponse }; +export type { + DirEntry, + FilesResponse, + FileResponse, + TreeResponse, + ViolationsResponse, + WebConstructNode, + WebViolation, + WebViolationOccurrence, +}; async function getJson(url: string): Promise { const res = await fetch(url); @@ -11,7 +29,14 @@ async function getJson(url: string): Promise { return res.json() as Promise; } +export interface AppInfoResponse { + readonly appDir: string; +} + export const api = { listFiles: (dir = ''): Promise => getJson(`/api/files?dir=${encodeURIComponent(dir)}`), readFile: (filePath: string): Promise => getJson(`/api/file?path=${encodeURIComponent(filePath)}`), + getTree: (): Promise => getJson('/api/tree'), + getViolations: (): Promise => getJson('/api/policy-validation'), + getAppInfo: (): Promise => getJson('/api/info'), }; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx new file mode 100644 index 000000000..fb7df3a50 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx @@ -0,0 +1,130 @@ +import Box from '@cloudscape-design/components/box'; +import Icon from '@cloudscape-design/components/icon'; +import * as React from 'react'; +import { severityHexColor } from '../../lib/web/severity'; +import type { WebConstructNode } from '../api'; + +interface ConstructTreeProps { + readonly nodes: readonly WebConstructNode[]; + readonly depth?: number; +} + +/** Auto-expand the tree through this depth on load (0 = root), so stacks and their top-level constructs are visible without manual clicks. */ +const AUTO_EXPAND_DEPTH = 2; + +/** + * Renders the construct hierarchy as a nested, indented list. Rows never spill + * past the panel: long labels truncate with an ellipsis and the tree clips + * horizontally. Nodes collapse via a caret (whole-row click). Display-only. + */ +export function ConstructTree({ nodes, depth = 0 }: ConstructTreeProps): JSX.Element { + if (nodes.length === 0) { + return No constructs.; + } + const list = ( +
    + {nodes.map((node) => ( + + ))} +
+ ); + // Root render: wrap once in the clipping viewport; nested calls return the bare list. + return depth === 0 ?
{list}
: list; +} + +function TreeNode({ node, depth }: { readonly node: WebConstructNode; readonly depth: number }): JSX.Element { + const hasChildren = node.children.length > 0; + const [expanded, setExpanded] = React.useState(depth < AUTO_EXPAND_DEPTH); + + const label = friendlyName(node); + const severity = node.highestSeverity; + const severityColor = severity ? severityHexColor(severity) : undefined; + const inherited = !severity ? node.inheritedSeverity : undefined; + const inheritedColor = inherited ? severityHexColor(inherited) : undefined; + + return ( +
  • +
    setExpanded((e) => !e) : undefined} + role={hasChildren ? 'button' : undefined} + aria-expanded={hasChildren ? expanded : undefined} + > + {hasChildren ? {expanded ? '\u25be' : '\u25b8'} : } + + {severityColor && ( + + )} + + {label} + + {node.type && ( + + {friendlyType(node.type)} + + )} +
    + {hasChildren && expanded && } +
  • + ); +} + +/** Friendlier default label: a generic CDK id ("Resource"/"Default") shows its resource type instead. */ +function friendlyName(node: WebConstructNode): string { + if ((node.id === 'Resource' || node.id === 'Default') && node.type) { + return friendlyType(node.type); + } + return node.id; +} + +/** "AWS::DynamoDB::Table" -> "DynamoDB Table". */ +function friendlyType(type: string): string { + return type.replace(/^AWS::/, '').split('::').join(' '); +} + +function labelStyle(severityColor: string | undefined, inheritedColor: string | undefined): React.CSSProperties { + if (severityColor) return { ...LABEL_STYLE, color: severityColor }; + if (inheritedColor) return { ...LABEL_STYLE, color: inheritedColor, opacity: 0.7 }; + return LABEL_STYLE; +} + +const TREE_VIEWPORT_STYLE: React.CSSProperties = { overflowX: 'hidden', overflowY: 'auto', height: '100%' }; +const SEVERITY_DOT_STYLE: React.CSSProperties = { + flexShrink: 0, + width: '8px', + height: '8px', + borderRadius: '50%', +}; +const LIST_STYLE: React.CSSProperties = { listStyle: 'none', margin: 0, paddingLeft: '12px', borderLeft: '1px solid #e9ebed' }; +const ITEM_STYLE: React.CSSProperties = { padding: '2px 0' }; +const ROW_STYLE: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: '4px', minWidth: 0, overflow: 'hidden' }; +const ROW_CLICKABLE_STYLE: React.CSSProperties = { ...ROW_STYLE, cursor: 'pointer' }; +const LABEL_STYLE: React.CSSProperties = { + flex: '1 1 auto', + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontWeight: 500, +}; +const TYPE_STYLE: React.CSSProperties = { + flex: '0 1 auto', + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + paddingLeft: '8px', + color: '#5f6b7a', + fontSize: '12px', +}; +const CARET_STYLE: React.CSSProperties = { + width: '12px', + flexShrink: 0, + fontSize: '10px', + lineHeight: 1, +}; +const CARET_SPACER: React.CSSProperties = { display: 'inline-block', width: '12px', flexShrink: 0 }; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx new file mode 100644 index 000000000..b77c6e87c --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx @@ -0,0 +1,72 @@ +import Box from '@cloudscape-design/components/box'; +import ExpandableSection from '@cloudscape-design/components/expandable-section'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import StatusIndicator from '@cloudscape-design/components/status-indicator'; +import * as React from 'react'; +import { displaySeverity, severityHexColor, severityRank } from '../../lib/web/severity'; +import type { WebViolation } from '../api'; + +interface ViolationsPanelProps { + readonly violations: readonly WebViolation[]; +} + +/** + * Renders policy-validation violations like a Problems panel: sorted by + * severity, each a collapsible row led by a colored severity indicator, with + * the rule, affected-construct count, and plugin source. + */ +export function ViolationsPanel({ violations }: ViolationsPanelProps): JSX.Element { + if (violations.length === 0) { + return No policy violations.; + } + const sorted = [...violations].sort((a, b) => severityRank(displaySeverity(a)) - severityRank(displaySeverity(b))); + return ( +
    + + {sorted.map((violation, i) => ( + + ))} + +
    + ); +} + +function ViolationItem({ violation }: { readonly violation: WebViolation }): JSX.Element { + const severity = displaySeverity(violation); + const count = violation.occurrences.length; + return ( + + [{severity.toUpperCase()}] + {violation.ruleName} + + {count} {count === 1 ? 'construct' : 'constructs'} {'·'} {violation.source} + + + } + > + + {violation.description} + {violation.suggestedFix && Suggested fix: {violation.suggestedFix}} + {violation.occurrences.map((occ, i) => ( + + {occ.constructPath} + {occ.logicalId ? ` → ${occ.logicalId}` : ''} + {occ.templateFile ? ` (${occ.templateFile})` : ''} + + ))} + + + ); +} + +function severityStyle(severity: string): React.CSSProperties { + return { color: severityHexColor(severity), fontWeight: 700 }; +} + +const SCROLL_STYLE: React.CSSProperties = { maxHeight: '100%', overflowY: 'auto' }; +const HEADER_STYLE: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: '8px' }; +const RULE_STYLE: React.CSSProperties = { fontWeight: 700 }; +const META_STYLE: React.CSSProperties = { color: '#5f6b7a', fontWeight: 400, fontSize: '12px' }; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/index.html b/packages/@aws-cdk/cdk-explorer/frontend/index.html index 8aacc9e5a..bfe35e541 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/index.html +++ b/packages/@aws-cdk/cdk-explorer/frontend/index.html @@ -5,6 +5,10 @@ CDK Web Explorer +
    diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts index a4f512935..c41cacd54 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts @@ -16,3 +16,114 @@ export interface FileResponse { readonly path: string; readonly content: string; } + +/** + * A construct's source location for the SPA. Mirrors the core `SourceLocation` + * but with `file` made app-relative (POSIX), so the client can feed it straight + * back into `/api/file`. Line and column are 1-based, or 0 when only the file + * is known. + */ +export interface WebSourceLocation { + /** Source file path relative to the app directory. POSIX separators. */ + readonly file: string; + readonly line: number; + readonly column: number; +} + +/** + * A construct tree node as served to the SPA. A trimmed, wire-stable view of + * the core `ConstructNode`: absolute source paths are relativized to the app + * directory and nothing internal leaks over the wire. + */ +export interface WebConstructNode { + /** Construct path, e.g. "MyStack/DataBucket". */ + readonly path: string; + readonly id: string; + /** CFN resource type (e.g. "AWS::S3::Bucket"), if this construct is a resource. */ + readonly type?: string; + /** CFN logical ID, if this construct maps to a resource. */ + readonly logicalId?: string; + /** + * Path to the synthesized template that declares this construct's CFN + * resource, relative to the cloud assembly (`cdk.out`) directory, with POSIX + * separators. Usually a bare name like "MyStack.template.json", but includes + * the sub-assembly directory for staged stacks, e.g. + * "assembly-Prod/Prod-MyStack.template.json". Only set for CFN resources. The + * core's absolute path is relativized before it crosses the wire. + */ + readonly templateFile?: string; + readonly sourceLocation?: WebSourceLocation; + /** + * Highest-severity policy-violation label affecting this construct. A folded + * default child's violations count toward its parent. Absent when the + * construct has no violation. Used to flag the node in the tree. + */ + readonly highestSeverity?: string; + /** + * Highest severity inherited from any descendant. Set when a child (or + * deeper) has a violation but this node does not. Lets the tree color + * ancestor labels so users can drill down to the offending construct. + */ + readonly inheritedSeverity?: string; + readonly children: readonly WebConstructNode[]; +} + +/** + * Response for `GET /api/tree`. `not-synthesized` means no cloud assembly was + * found (the user has not run `cdk synth`) + */ +export type TreeResponse = + | { readonly status: 'ok'; readonly tree: readonly WebConstructNode[]; readonly warnings: readonly string[] } + | { readonly status: 'not-synthesized' }; + +/** + * Severity exactly as reported by CDK policy validation. Unlike the LSP, which + * collapses these onto its three diagnostic levels, the SPA keeps the full set + * so the violations panel can distinguish (for example) fatal from error. + */ +export type WebViolationSeverity = 'fatal' | 'error' | 'warning' | 'info' | 'custom'; + +/** + * A single construct that triggered a violation, joined to construct-tree data + * (resolved source location and template file) so a future navigation feature + * can link to the resource and its source. + */ +export interface WebViolationOccurrence { + /** Construct path of the offending construct, e.g. "MyStack/MyBucket". */ + readonly constructPath: string; + /** CFN logical ID of the offending resource, if known. */ + readonly logicalId?: string; + /** + * Template that declares the resource, relative to the cloud assembly + * (`cdk.out`) directory with POSIX separators (see {@link WebConstructNode.templateFile}). + */ + readonly templateFile?: string; + /** Resolved user source location; absent for non-TypeScript apps. */ + readonly sourceLocation?: WebSourceLocation; + /** JSON property paths within the resource that violate the rule. */ + readonly propertyPaths?: readonly string[]; +} + +/** A policy-validation violation, normalized for the SPA. */ +export interface WebViolation { + readonly ruleName: string; + readonly description: string; + /** Severity as reported; absent for plugins (e.g. CfnGuard) that don't emit one. */ + readonly severity?: WebViolationSeverity; + /** Plugin-specific label when `severity` is "custom". */ + readonly customSeverity?: string; + /** Validation plugin that produced the violation. */ + readonly source: string; + /** Suggested fix text, when the plugin provides one. */ + readonly suggestedFix?: string; + readonly occurrences: readonly WebViolationOccurrence[]; +} + +/** + * Response for `GET /api/policy-validation`. `not-synthesized` mirrors the tree + * endpoint: no cloud assembly was found. When the assembly exists, `violations` + * is the normalized list (empty when the report is clean or absent). + */ +export type ViolationsResponse = + | { readonly status: 'ok'; readonly violations: readonly WebViolation[] } + | { readonly status: 'not-synthesized' }; diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts index 433400733..4fd6685ca 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts @@ -1,27 +1,48 @@ import * as fs from 'fs'; import * as path from 'path'; -import { type Router, type Express } from 'express'; +import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import type { PolicyValidationReportJson, ViolatingConstructJson } from '@aws-cdk/cloud-assembly-schema'; +import { type Router, type Express, type Response } from 'express'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); -import type { DirEntry } from './protocol'; +import type { DirEntry, TreeResponse, ViolationsResponse, WebConstructNode, WebSourceLocation, WebViolation, WebViolationOccurrence } from './protocol'; import { resolveWithinRoot } from './safe-path'; +import { classifyReportSeverity, displaySeverity, severityRank } from './severity'; +import { readAssembly as defaultReadAssembly, type AssemblyData, type AssemblyReadResult, type ConstructNode } from '../core/assembly-reader'; +import type { SourceLocation } from '../core/source-resolver'; -/** Largest file the viewer will return inline, to avoid streaming huge artifacts. */ +/** Largest file the viewer returns inline, to avoid buffering huge artifacts into memory and the response. */ const MAX_FILE_BYTES = 2 * 1024 * 1024; export interface ApiOptions { /** Root of the CDK app; all file listing/reading is confined to this directory. */ readonly appDir: string; + /** + * Cloud assembly directory the construct tree and violations are read from. + * Defaults to `/cdk.out`. + */ + readonly assemblyDir?: string; + /** + * Reader for the cloud assembly. Injectable for tests; defaults to the real + * `readAssembly` against {@link assemblyDir}. + */ + readonly readAssembly?: (assemblyDir: string) => AssemblyReadResult | Promise; } export function createApiRouter(options: ApiOptions): Router { const appDir = canonicalDir(options.appDir); + const assemblyDir = options.assemblyDir ?? path.join(options.appDir, 'cdk.out'); + const readAssembly = options.readAssembly ?? defaultReadAssembly; const router = express.Router(); router.get('/health', (_req, res) => { res.json({ status: 'ok' }); }); + router.get('/info', (_req, res) => { + res.json({ appDir }); + }); + router.get('/files', (req, res) => { const dir = typeof req.query.dir === 'string' ? req.query.dir : ''; const resolved = resolveWithinRoot(appDir, dir); @@ -68,6 +89,24 @@ export function createApiRouter(options: ApiOptions): Router { return res.json({ path: toPosix(path.relative(appDir, resolved)), content: buffer.toString('utf-8') }); }); + router.get('/tree', async (_req, res) => { + withAssembly(await readAssembly(assemblyDir), res, (data) => { + const severityByPath = highestSeverityByPath(data.violations); + const tree = data.tree.map((node) => toWebNode(node, severityByPath, assemblyDir, appDir)); + const body: TreeResponse = { status: 'ok', tree, warnings: data.warnings }; + res.json(body); + }); + }); + + router.get('/policy-validation', async (_req, res) => { + withAssembly(await readAssembly(assemblyDir), res, (data) => { + const index = ConstructIndex.fromTree(data.tree); + const violations = normalizeViolations(data.violations, index, assemblyDir, appDir); + const body: ViolationsResponse = { status: 'ok', violations }; + res.json(body); + }); + }); + return router; } @@ -75,6 +114,173 @@ export function registerApi(app: Express, options: ApiOptions): void { app.use('/api', createApiRouter(options)); } +/** + * Read the cloud assembly and send the shared not-synthesized / error responses, + * invoking `onReady` with the assembly data only on success. + */ +function withAssembly( + result: AssemblyReadResult, + res: Response, + onReady: (data: AssemblyData) => void, +): void { + if (result.status === 'not-found') { + res.json({ status: 'not-synthesized' }); + return; + } + if (result.status === 'error') { + res.status(500).json({ error: result.message }); + return; + } + onReady(result.data); +} + +/** Ids CDK gives a construct's synthetic default child (the L1 resource it wraps). */ +const DEFAULT_CHILD_IDS = new Set(['Resource', 'Default']); + +/** + * Map a core construct node to its wire form, owning every transform the + * displayed tree needs: + * - Relativizes paths the client can't resolve (`templateFile` to the cloud + * assembly, `sourceLocation.file` to the app dir; a source location outside + * the app dir is dropped, since `/api/file` cannot serve it). + * - Folds a synthetic default child (the leaf L1 resource CDK names "Resource" + * or "Default", carrying a CFN type) up into its parent, so an L2 like + * `ItemsTable` shows its CFN type directly instead of nesting a redundant + * leaf. Only a leaf default child with a type collapses; a resource that + * nests further children is left intact. + * - Annotates each node with the highest severity of any violation on it, + * folding an absorbed default child's severity into the parent so the dot + * tracks the displayed node. Because the join happens here, the client never + * re-derives the collapse rule. + * Recurses depth-first. + */ +export function toWebNode( + node: ConstructNode, + severityByPath: ReadonlyMap, + assemblyDir: string, + appDir: string, +): WebConstructNode { + const children = node.children.map((child) => toWebNode(child, severityByPath, assemblyDir, appDir)); + const ownSeverity = severityByPath.get(node.path); + const defaultChild = children.find( + (child) => DEFAULT_CHILD_IDS.has(child.id) && child.children.length === 0 && child.type !== undefined, + ); + if (!defaultChild) { + const highestSeverity = ownSeverity; + const inheritedSeverity = highestSeverity ? undefined : worstChildSeverity(children); + return { + path: node.path, + id: node.id, + type: node.type, + logicalId: node.logicalId, + templateFile: node.templateFile ? toPosix(path.relative(assemblyDir, node.templateFile)) : undefined, + sourceLocation: toWebSourceLocation(node.sourceLocation, appDir), + highestSeverity, + ...(inheritedSeverity && { inheritedSeverity }), + children, + }; + } + const highestSeverity = moreSevere(ownSeverity, defaultChild.highestSeverity); + const remainingChildren = children.filter((child) => child !== defaultChild); + const inheritedSeverity = highestSeverity ? undefined : worstChildSeverity(remainingChildren); + return { + path: node.path, + id: node.id, + type: defaultChild.type, + logicalId: defaultChild.logicalId, + templateFile: defaultChild.templateFile, + sourceLocation: toWebSourceLocation(node.sourceLocation, appDir) ?? defaultChild.sourceLocation, + highestSeverity, + ...(inheritedSeverity && { inheritedSeverity }), + children: remainingChildren, + }; +} + +/** Build a construct-path to highest-severity-label map from a (raw) validation report. */ +function highestSeverityByPath(report: PolicyValidationReportJson | undefined): Map { + const byPath = new Map(); + for (const plugin of report?.pluginReports ?? []) { + for (const violation of plugin.violations ?? []) { + const label = displaySeverity(classifyReportSeverity(violation.severity, violation.customSeverity)); + for (const vc of violation.violatingConstructs ?? []) { + const existing = byPath.get(vc.constructPath); + if (existing === undefined || severityRank(label) < severityRank(existing)) { + byPath.set(vc.constructPath, label); + } + } + } + } + return byPath; +} + +/** The more severe of two severity labels (lower rank = more severe); undefined loses to any defined label. */ +function moreSevere(a: string | undefined, b: string | undefined): string | undefined { + if (a === undefined) return b; + if (b === undefined) return a; + return severityRank(a) <= severityRank(b) ? a : b; +} + +/** The worst severity across all children (direct or inherited). */ +function worstChildSeverity(children: readonly WebConstructNode[]): string | undefined { + let worst: string | undefined; + for (const child of children) { + worst = moreSevere(worst, child.highestSeverity ?? child.inheritedSeverity); + } + return worst; +} + +/** Relativize a source location to the app dir, dropping any that escape it. */ +function toWebSourceLocation(loc: SourceLocation | undefined, appDir: string): WebSourceLocation | undefined { + if (!loc) return undefined; + const file = toPosix(path.relative(appDir, loc.file)); + if (file === '..' || file.startsWith('../')) return undefined; + return { file, line: loc.line, column: loc.column }; +} + +/** + * Normalize a policy-validation report into the SPA's flat violation model. + * Each violating construct is joined to the construct tree (by path): the + * resolved `sourceLocation` and `cdk.out`-relative `templateFile` come from the + * tree node when present, falling back to the report's own resource fields. + * These carry the data a future navigation feature would link from. + */ +export function normalizeViolations( + report: PolicyValidationReportJson | undefined, + index: ConstructIndex, + assemblyDir: string, + appDir: string, +): WebViolation[] { + return (report?.pluginReports ?? []).flatMap((plugin) => + (plugin.violations ?? []).map((violation) => ({ + ruleName: violation.ruleName, + description: violation.description, + ...classifyReportSeverity(violation.severity, violation.customSeverity), + source: plugin.pluginName, + suggestedFix: violation.suggestedFix, + occurrences: (violation.violatingConstructs ?? []).map((vc) => toOccurrence(vc, index, assemblyDir, appDir)), + }))); +} + +/** Join one violating construct to its tree node, preferring resolved tree data. */ +function toOccurrence( + vc: ViolatingConstructJson, + index: ConstructIndex, + assemblyDir: string, + appDir: string, +): WebViolationOccurrence { + const node = index.byPath(vc.constructPath); + const templateFile = node?.templateFile + ? toPosix(path.relative(assemblyDir, node.templateFile)) + : vc.cloudFormationResource?.templatePath; + return { + constructPath: vc.constructPath, + logicalId: node?.logicalId ?? vc.cloudFormationResource?.logicalId, + templateFile, + sourceLocation: toWebSourceLocation(node?.sourceLocation, appDir), + propertyPaths: vc.cloudFormationResource?.propertyPaths, + }; +} + function listDir(appDir: string, dir: string): DirEntry[] { return fs.readdirSync(dir, { withFileTypes: true }) .map((entry): DirEntry => ({ diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts index 56021d169..6fd2b606d 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts @@ -15,6 +15,11 @@ export interface WebServerOptions { * `process.cwd()`. */ readonly appDir?: string; + /** + * Cloud assembly directory to read the construct tree and violations from. + * Defaults to `/cdk.out`. + */ + readonly assemblyDir?: string; } export interface WebServer { @@ -36,20 +41,24 @@ export async function startWebServer(options: WebServerOptions = {}): Promise res.status(404).json({ error: 'unknown endpoint' })); // Serve the SPA from the embedded bundle (survives CLI bundling). Named assets // by path; any other GET falls back to index.html for client-side routing. + // The bundle filename is unversioned, so disable caching to ensure a rebuilt + // explorer is always picked up on reload rather than served stale by the browser. app.get('/:asset', (req, res, next) => { const asset = webAsset(req.params.asset); if (!asset) return next(); + res.set('Cache-Control', 'no-store'); return res.type(asset.contentType).send(asset.body); }); app.get('*', (_req, res) => { const index = indexHtml(); + res.set('Cache-Control', 'no-store'); res.type(index.contentType).send(index.body); }); diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/severity.ts b/packages/@aws-cdk/cdk-explorer/lib/web/severity.ts new file mode 100644 index 000000000..678b0e9ce --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/severity.ts @@ -0,0 +1,67 @@ +/** + * Severity vocabulary for CDK policy violations, shared by the server (ranking + * violations onto construct-tree nodes) and the SPA (sorting and coloring the + * violations panel) so the rules live in exactly one place. + */ + +import type { WebViolation, WebViolationSeverity } from './protocol'; + +/** Severities CDK policy validation reports natively. */ +const KNOWN_SEVERITIES = new Set(['fatal', 'error', 'warning', 'info', 'custom']); + +/** + * Coerce a report's severity into the wire model. aws-cdk-lib already emits standard + * levels as-is and non-standard ones as `severity: 'custom'` with the raw label in + * `customSeverity`. Since we read the report as untyped JSON, this also folds any + * unrecognized `severity` string into `'custom'` (keeping the raw value as its label), + * so the result can never be a value outside {@link WebViolationSeverity}. + */ +export function classifyReportSeverity( + severity: string | undefined, + customSeverity: string | undefined, +): { readonly severity?: WebViolationSeverity; readonly customSeverity?: string } { + if (severity !== undefined && !KNOWN_SEVERITIES.has(severity)) { + return { severity: 'custom', customSeverity: customSeverity ?? severity }; + } + return { severity: severity as WebViolationSeverity | undefined, customSeverity }; +} + +/** Lower-cased severity so 'Error'/'ERROR'/'error' all match (mirroring cdk validate). */ +function normalize(severity: string): string { + return severity.toLowerCase(); +} + +/** + * Severity label for display: a plugin's custom label, else the reported + * severity, else 'warning' (cdk validate's default for severity-less plugins + * like CfnGuard). + */ +export function displaySeverity(violation: Pick): string { + return violation.customSeverity ?? violation.severity ?? 'warning'; +} + +const SEVERITY_RANK: Record = { fatal: 0, error: 1, warning: 2, info: 3 }; + +/** + * Sort rank for a severity label; lower sorts first. Custom and unrecognized + * labels rank just after `info`, matching cdk validate's handling of unknown + * severities. + */ +export function severityRank(severity: string): number { + return SEVERITY_RANK[normalize(severity)] ?? 4; +} + +const SEVERITY_HEX: Record = { + fatal: '#d91515', + error: '#e07700', + warning: '#8d6605', + info: '#0972d3', +}; + +/** + * Text color for a severity label, mirroring cdk validate (fatal red, error + * orange, warning amber, info blue); custom and unrecognized labels render gray. + */ +export function severityHexColor(severity: string): string { + return SEVERITY_HEX[normalize(severity)] ?? '#5f6b7a'; +} diff --git a/packages/@aws-cdk/cdk-explorer/test/web/normalize-violations.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/normalize-violations.test.ts new file mode 100644 index 000000000..e529537da --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/normalize-violations.test.ts @@ -0,0 +1,127 @@ +import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import type { PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; +import type { ConstructNode } from '../../lib/core/assembly-reader'; +import { normalizeViolations } from '../../lib/web/routes'; + +const ASSEMBLY_DIR = '/abs/cdk.out'; +const APP_DIR = '/abs/app'; + +function makeNode(props: { + path: string; + id: string; + type?: string; + logicalId?: string; + templateFile?: string; + sourceLocation?: { file: string; line: number; column: number }; + children?: ConstructNode[]; +}): ConstructNode { + return { ...props, children: props.children ?? [] }; +} + +function report(violatingConstructs: PolicyValidationReportJson['pluginReports'][number]['violations'][number]['violatingConstructs']): PolicyValidationReportJson { + return { + version: '1.0', + pluginReports: [ + { + pluginName: 'cdk-validator', + conclusion: 'failure', + violations: [ + { + ruleName: 'no-public-access', + description: 'S3 bucket should not allow public access', + severity: 'error', + suggestedFix: 'set blockPublicAccess', + violatingConstructs, + }, + ], + }, + ], + }; +} + +describe('normalizeViolations', () => { + test('prefers resolved tree data over the report when the construct is in the index', () => { + const index = ConstructIndex.fromTree([ + makeNode({ + path: 'MyStack/Bucket', + id: 'Bucket', + type: 'AWS::S3::Bucket', + logicalId: 'Bucket123', + templateFile: `${ASSEMBLY_DIR}/MyStack.template.json`, + sourceLocation: { file: `${APP_DIR}/lib/stack.ts`, line: 12, column: 5 }, + }), + ]); + const r = report([ + { + constructPath: 'MyStack/Bucket', + cloudFormationResource: { templatePath: 'IGNORED.json', logicalId: 'IGNORED', propertyPaths: ['Properties.PublicAccessBlock'] }, + }, + ]); + + const out = normalizeViolations(r, index, ASSEMBLY_DIR, APP_DIR); + + expect(out).toHaveLength(1); + expect(out[0]).toMatchObject({ + ruleName: 'no-public-access', + description: 'S3 bucket should not allow public access', + severity: 'error', + source: 'cdk-validator', + suggestedFix: 'set blockPublicAccess', + }); + expect(out[0].occurrences[0]).toEqual({ + constructPath: 'MyStack/Bucket', + logicalId: 'Bucket123', + templateFile: 'MyStack.template.json', + sourceLocation: { file: 'lib/stack.ts', line: 12, column: 5 }, + propertyPaths: ['Properties.PublicAccessBlock'], + }); + }); + + test('falls back to the report resource fields when the construct is not in the index', () => { + const index = ConstructIndex.fromTree([]); + const r = report([ + { + constructPath: 'Other/X', + cloudFormationResource: { templatePath: 'Other.template.json', logicalId: 'X9', propertyPaths: ['P'] }, + }, + ]); + + const occ = normalizeViolations(r, index, ASSEMBLY_DIR, APP_DIR)[0].occurrences[0]; + + expect(occ.constructPath).toBe('Other/X'); + expect(occ.logicalId).toBe('X9'); + expect(occ.templateFile).toBe('Other.template.json'); + expect(occ.sourceLocation).toBeUndefined(); + expect(occ.propertyPaths).toEqual(['P']); + }); + + test('leaves resource fields undefined when neither the index nor the report has them', () => { + const index = ConstructIndex.fromTree([]); + const occ = normalizeViolations(report([{ constructPath: 'Ghost' }]), index, ASSEMBLY_DIR, APP_DIR)[0].occurrences[0]; + expect(occ.logicalId).toBeUndefined(); + expect(occ.templateFile).toBeUndefined(); + expect(occ.sourceLocation).toBeUndefined(); + expect(occ.propertyPaths).toBeUndefined(); + }); + + test('flattens violations across plugins', () => { + const index = ConstructIndex.fromTree([]); + const r: PolicyValidationReportJson = { + version: '1.0', + pluginReports: [ + { pluginName: 'a', conclusion: 'failure', violations: [{ ruleName: 'r1', description: 'd1', severity: 'warning', violatingConstructs: [] }] }, + { pluginName: 'b', conclusion: 'failure', violations: [{ ruleName: 'r2', description: 'd2', severity: 'fatal', customSeverity: 'BLOCKER', violatingConstructs: [] }] }, + ], + }; + + const out = normalizeViolations(r, index, ASSEMBLY_DIR, APP_DIR); + + expect(out).toHaveLength(2); + expect(out.map((v) => v.source)).toEqual(['a', 'b']); + expect(out[1]).toMatchObject({ severity: 'fatal', customSeverity: 'BLOCKER' }); + }); + + test('returns an empty list for an undefined report', () => { + expect(normalizeViolations(undefined, ConstructIndex.fromTree([]), ASSEMBLY_DIR, APP_DIR)).toEqual([]); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts index 9028bfbe0..c35d00a94 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts @@ -1,10 +1,12 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import type { PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); // eslint-disable-next-line @typescript-eslint/no-require-imports import request = require('supertest'); +import type { AssemblyReadResult, ConstructNode } from '../../lib/core/assembly-reader'; import { createApiRouter } from '../../lib/web/routes'; let appDir: string; @@ -111,3 +113,145 @@ describe('symlink containment', () => { } }); }); + +describe('GET /api/tree', () => { + /** Build an app whose /api router uses an injected assembly reader. */ + function appWith( + readAssembly: (dir: string) => AssemblyReadResult, + opts: { assemblyDir?: string } = {}, + ): express.Express { + const a = express(); + a.use('/api', createApiRouter({ appDir, assemblyDir: opts.assemblyDir, readAssembly })); + return a; + } + + test('maps a successful read into an ok TreeResponse with relativized paths', async () => { + const realAppDir = fs.realpathSync(appDir); + const reader = (dir: string): AssemblyReadResult => { + const node: ConstructNode = { + path: 'MyStack/Bucket', + id: 'Bucket', + type: 'AWS::S3::Bucket', + logicalId: 'Bucket123', + templateFile: path.join(dir, 'MyStack.template.json'), + sourceLocation: { file: path.join(realAppDir, 'lib', 'stack.ts'), line: 12, column: 5 }, + children: [], + }; + return { status: 'success', data: { tree: [node], warnings: ['heads up'] } }; + }; + + const res = await request(appWith(reader)).get('/api/tree'); + + expect(res.status).toBe(200); + expect(res.body.status).toBe('ok'); + expect(res.body.warnings).toEqual(['heads up']); + expect(res.body.tree).toHaveLength(1); + expect(res.body.tree[0]).toMatchObject({ + path: 'MyStack/Bucket', + logicalId: 'Bucket123', + templateFile: 'MyStack.template.json', + sourceLocation: { file: 'lib/stack.ts', line: 12, column: 5 }, + }); + }); + + test('returns not-synthesized (200) when no assembly is found', async () => { + const res = await request(appWith(() => ({ status: 'not-found' }))).get('/api/tree'); + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'not-synthesized' }); + }); + + test('returns 500 when the read errors', async () => { + const res = await request(appWith(() => ({ status: 'error', message: 'bad manifest' }))).get('/api/tree'); + expect(res.status).toBe(500); + expect(res.body).toEqual({ error: 'bad manifest' }); + }); + + test('defaults the assembly dir to /cdk.out', async () => { + let seen: string | undefined; + const reader = (dir: string): AssemblyReadResult => { + seen = dir; + return { status: 'not-found' }; + }; + await request(appWith(reader)).get('/api/tree'); + expect(seen).toBe(path.join(appDir, 'cdk.out')); + }); + + test('honors an explicit assemblyDir override', async () => { + let seen: string | undefined; + const reader = (dir: string): AssemblyReadResult => { + seen = dir; + return { status: 'not-found' }; + }; + await request(appWith(reader, { assemblyDir: '/custom/out' })).get('/api/tree'); + expect(seen).toBe('/custom/out'); + }); +}); + +describe('GET /api/policy-validation', () => { + function appWith(readAssembly: (dir: string) => AssemblyReadResult): express.Express { + const a = express(); + a.use('/api', createApiRouter({ appDir, readAssembly })); + return a; + } + + test('normalizes violations joined to the construct tree', async () => { + const realAppDir = fs.realpathSync(appDir); + const reader = (dir: string): AssemblyReadResult => { + const node: ConstructNode = { + path: 'MyStack/Bucket', + id: 'Bucket', + type: 'AWS::S3::Bucket', + logicalId: 'Bucket123', + templateFile: path.join(dir, 'MyStack.template.json'), + sourceLocation: { file: path.join(realAppDir, 'lib', 'stack.ts'), line: 7, column: 3 }, + children: [], + }; + const violations: PolicyValidationReportJson = { + version: '1.0', + pluginReports: [ + { + pluginName: 'cdk-validator', + conclusion: 'failure', + violations: [ + { + ruleName: 'no-public-access', + description: 'no public access', + severity: 'error', + violatingConstructs: [ + { constructPath: 'MyStack/Bucket', cloudFormationResource: { templatePath: 'x', logicalId: 'y', propertyPaths: ['Properties.Public'] } }, + ], + }, + ], + }, + ], + }; + return { status: 'success', data: { tree: [node], violations, warnings: [] } }; + }; + + const res = await request(appWith(reader)).get('/api/policy-validation'); + + expect(res.status).toBe(200); + expect(res.body.status).toBe('ok'); + expect(res.body.violations).toHaveLength(1); + expect(res.body.violations[0]).toMatchObject({ ruleName: 'no-public-access', severity: 'error', source: 'cdk-validator' }); + expect(res.body.violations[0].occurrences[0]).toMatchObject({ + constructPath: 'MyStack/Bucket', + logicalId: 'Bucket123', + templateFile: 'MyStack.template.json', + sourceLocation: { file: 'lib/stack.ts', line: 7, column: 3 }, + propertyPaths: ['Properties.Public'], + }); + }); + + test('returns not-synthesized (200) when no assembly is found', async () => { + const res = await request(appWith(() => ({ status: 'not-found' }))).get('/api/policy-validation'); + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'not-synthesized' }); + }); + + test('returns 500 when the read errors', async () => { + const res = await request(appWith(() => ({ status: 'error', message: 'boom' }))).get('/api/policy-validation'); + expect(res.status).toBe(500); + expect(res.body).toEqual({ error: 'boom' }); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts index ce05239ab..0dc76d138 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts @@ -64,4 +64,11 @@ describe('Web Server', () => { expect(res.headers.get('content-type')).toMatch(/application\/json/); expect((await res.json()).error).toBeDefined(); }); + + test('serves the SPA index with Cache-Control: no-store so a rebuilt bundle is not served stale', async () => { + server = await startWebServer(); + const res = await fetch(`${server.url}/`); + expect(res.status).toBe(200); + expect(res.headers.get('cache-control')).toBe('no-store'); + }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/to-web-node.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/to-web-node.test.ts new file mode 100644 index 000000000..3f28b7e8d --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/to-web-node.test.ts @@ -0,0 +1,289 @@ +import type { ConstructNode } from '../../lib/core/assembly-reader'; +import { toWebNode } from '../../lib/web/routes'; + +const ASSEMBLY_DIR = '/abs/cdk.out'; +const APP_DIR = '/abs/app'; +const NO_SEVERITY = new Map(); + +function makeNode(props: { + path: string; + id: string; + type?: string; + logicalId?: string; + templateFile?: string; + sourceLocation?: { file: string; line: number; column: number }; + children?: ConstructNode[]; +}): ConstructNode { + return { ...props, children: props.children ?? [] }; +} + +describe('toWebNode', () => { + test('relativizes a flat template to its bare name', () => { + const web = toWebNode( + makeNode({ path: 'S/B', id: 'B', logicalId: 'B123', templateFile: `${ASSEMBLY_DIR}/MyStack.template.json` }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.templateFile).toBe('MyStack.template.json'); + }); + + test('keeps the sub-assembly directory for staged stacks', () => { + const web = toWebNode( + makeNode({ + path: 'Prod/S', + id: 'S', + logicalId: 'S1', + templateFile: `${ASSEMBLY_DIR}/assembly-Prod/Prod-MyStack.template.json`, + }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.templateFile).toBe('assembly-Prod/Prod-MyStack.template.json'); + }); + + test('relativizes a source location inside the app dir', () => { + const web = toWebNode( + makeNode({ path: 'S/B', id: 'B', sourceLocation: { file: `${APP_DIR}/lib/stack.ts`, line: 12, column: 5 } }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.sourceLocation).toEqual({ file: 'lib/stack.ts', line: 12, column: 5 }); + }); + + test('drops a source location that escapes the app dir', () => { + const web = toWebNode( + makeNode({ path: 'S/B', id: 'B', sourceLocation: { file: '/abs/other/lib.ts', line: 1, column: 1 } }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.sourceLocation).toBeUndefined(); + }); + + test('leaves template and source undefined for a non-resource, non-TS node', () => { + const web = toWebNode(makeNode({ path: 'S', id: 'S' }), NO_SEVERITY, ASSEMBLY_DIR, APP_DIR); + expect(web.templateFile).toBeUndefined(); + expect(web.sourceLocation).toBeUndefined(); + }); + + test('passes through type and logicalId', () => { + const web = toWebNode( + makeNode({ path: 'S/B', id: 'B', type: 'AWS::S3::Bucket', logicalId: 'B123' }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.type).toBe('AWS::S3::Bucket'); + expect(web.logicalId).toBe('B123'); + }); + + test('recurses over children', () => { + const tree = makeNode({ + path: 'S', + id: 'S', + children: [ + makeNode({ path: 'S/B', id: 'B', logicalId: 'B1', templateFile: `${ASSEMBLY_DIR}/S.template.json` }), + ], + }); + const web = toWebNode(tree, NO_SEVERITY, ASSEMBLY_DIR, APP_DIR); + expect(web.children).toHaveLength(1); + expect(web.children[0].path).toBe('S/B'); + expect(web.children[0].templateFile).toBe('S.template.json'); + }); + + test('annotates a node with its violation severity', () => { + const web = toWebNode(makeNode({ path: 'S/B', id: 'B' }), new Map([['S/B', 'error']]), ASSEMBLY_DIR, APP_DIR); + expect(web.highestSeverity).toBe('error'); + }); + + test('leaves highestSeverity undefined when the node has no violation', () => { + const web = toWebNode(makeNode({ path: 'S/B', id: 'B' }), NO_SEVERITY, ASSEMBLY_DIR, APP_DIR); + expect(web.highestSeverity).toBeUndefined(); + }); +}); + +describe('toWebNode default-child collapse', () => { + test('folds a leaf "Resource" child into its parent', () => { + const web = toWebNode( + makeNode({ + path: 'S/ItemsTable', + id: 'ItemsTable', + children: [ + makeNode({ + path: 'S/ItemsTable/Resource', + id: 'Resource', + type: 'AWS::DynamoDB::Table', + logicalId: 'ItemsTable0ABC', + templateFile: `${ASSEMBLY_DIR}/S.template.json`, + }), + ], + }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.type).toBe('AWS::DynamoDB::Table'); + expect(web.logicalId).toBe('ItemsTable0ABC'); + expect(web.templateFile).toBe('S.template.json'); + expect(web.children).toHaveLength(0); + }); + + test('folds a leaf "Default" child too', () => { + const web = toWebNode( + makeNode({ + path: 'S/Custom', + id: 'Custom', + children: [makeNode({ path: 'S/Custom/Default', id: 'Default', type: 'AWS::CloudFormation::CustomResource' })], + }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.type).toBe('AWS::CloudFormation::CustomResource'); + expect(web.children).toHaveLength(0); + }); + + test('collapses each construct independently, keeping siblings', () => { + const web = toWebNode( + makeNode({ + path: 'S/Bucket', + id: 'Bucket', + children: [ + makeNode({ path: 'S/Bucket/Resource', id: 'Resource', type: 'AWS::S3::Bucket', logicalId: 'Bucket1' }), + makeNode({ + path: 'S/Bucket/Policy', + id: 'Policy', + children: [ + makeNode({ path: 'S/Bucket/Policy/Resource', id: 'Resource', type: 'AWS::S3::BucketPolicy', logicalId: 'Policy1' }), + ], + }), + ], + }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.type).toBe('AWS::S3::Bucket'); + expect(web.children).toHaveLength(1); + expect(web.children[0].id).toBe('Policy'); + expect(web.children[0].type).toBe('AWS::S3::BucketPolicy'); + expect(web.children[0].children).toHaveLength(0); + }); + + test('does not collapse a "Resource" child that has its own children', () => { + const web = toWebNode( + makeNode({ + path: 'S/Weird', + id: 'Weird', + children: [ + makeNode({ + path: 'S/Weird/Resource', + id: 'Resource', + type: 'AWS::Some::Thing', + children: [makeNode({ path: 'S/Weird/Resource/Inner', id: 'Inner' })], + }), + ], + }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.type).toBeUndefined(); + expect(web.children).toHaveLength(1); + expect(web.children[0].id).toBe('Resource'); + }); + + test('does not collapse a "Resource" child with no CFN type', () => { + const web = toWebNode( + makeNode({ path: 'S/X', id: 'X', children: [makeNode({ path: 'S/X/Resource', id: 'Resource' })] }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.type).toBeUndefined(); + expect(web.children).toHaveLength(1); + }); + + test('keeps the parent source location (relativized), falling back to the child when absent', () => { + const parentLoc = { file: `${APP_DIR}/lib/s.ts`, line: 5, column: 2 }; + const childLoc = { file: `${APP_DIR}/lib/s.ts`, line: 99, column: 9 }; + const kept = toWebNode( + makeNode({ + path: 'S/T', + id: 'T', + sourceLocation: parentLoc, + children: [makeNode({ path: 'S/T/Resource', id: 'Resource', type: 'AWS::X::Y', sourceLocation: childLoc })], + }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(kept.sourceLocation).toEqual({ file: 'lib/s.ts', line: 5, column: 2 }); + + const fellBack = toWebNode( + makeNode({ + path: 'S/T', + id: 'T', + children: [makeNode({ path: 'S/T/Resource', id: 'Resource', type: 'AWS::X::Y', sourceLocation: childLoc })], + }), + NO_SEVERITY, + ASSEMBLY_DIR, + APP_DIR, + ); + expect(fellBack.sourceLocation).toEqual({ file: 'lib/s.ts', line: 99, column: 9 }); + }); + + test("folds an absorbed default child's severity into the parent", () => { + const web = toWebNode( + makeNode({ + path: 'S/T', + id: 'T', + children: [makeNode({ path: 'S/T/Resource', id: 'Resource', type: 'AWS::X::Y' })], + }), + new Map([['S/T/Resource', 'error']]), + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.highestSeverity).toBe('error'); + expect(web.children).toHaveLength(0); + }); + + test('takes the more severe of the parent and the absorbed child', () => { + const web = toWebNode( + makeNode({ + path: 'S/T', + id: 'T', + children: [makeNode({ path: 'S/T/Resource', id: 'Resource', type: 'AWS::X::Y' })], + }), + new Map([['S/T', 'warning'], ['S/T/Resource', 'error']]), + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.highestSeverity).toBe('error'); + }); + + test("does not attribute an uncollapsed Resource child's severity to its parent", () => { + const web = toWebNode( + makeNode({ + path: 'S/Weird', + id: 'Weird', + children: [ + makeNode({ + path: 'S/Weird/Resource', + id: 'Resource', + type: 'AWS::Some::Thing', + children: [makeNode({ path: 'S/Weird/Resource/Inner', id: 'Inner' })], + }), + ], + }), + new Map([['S/Weird/Resource', 'error']]), + ASSEMBLY_DIR, + APP_DIR, + ); + expect(web.highestSeverity).toBeUndefined(); + expect(web.children[0].highestSeverity).toBe('error'); + }); +}); diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index de9075f5b..ad0af131e 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -16039,6 +16039,77 @@ Apache License of your accepting any such warranty or additional liability. +---------------- + +** @jridgewell/resolve-uri@3.1.2 - https://www.npmjs.com/package/@jridgewell/resolve-uri/v/3.1.2 | MIT +Copyright 2019 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------- + +** @jridgewell/sourcemap-codec@1.5.5 - https://www.npmjs.com/package/@jridgewell/sourcemap-codec/v/1.5.5 | MIT +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** @jridgewell/trace-mapping@0.3.31 - https://www.npmjs.com/package/@jridgewell/trace-mapping/v/0.3.31 | MIT +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** @nodelib/fs.scandir@2.1.5 - https://www.npmjs.com/package/@nodelib/fs.scandir/v/2.1.5 | MIT @@ -27644,6 +27715,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** accepts@1.3.8 - https://www.npmjs.com/package/accepts/v/1.3.8 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** agent-base@7.1.4 - https://www.npmjs.com/package/agent-base/v/7.1.4 | MIT @@ -27751,6 +27850,32 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** array-flatten@1.1.1 - https://www.npmjs.com/package/array-flatten/v/1.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** ast-types@0.13.4 - https://www.npmjs.com/package/ast-types/v/0.13.4 | MIT @@ -27813,6 +27938,34 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** body-parser@1.20.5 - https://www.npmjs.com/package/body-parser/v/1.20.5 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** bowser@2.14.1 - https://www.npmjs.com/package/bowser/v/2.14.1 | MIT @@ -27907,6 +28060,86 @@ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TOR ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** bytes@3.1.2 - https://www.npmjs.com/package/bytes/v/3.1.2 | MIT +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** call-bind-apply-helpers@1.0.2 - https://www.npmjs.com/package/call-bind-apply-helpers/v/1.0.2 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** call-bound@1.0.4 - https://www.npmjs.com/package/call-bound/v/1.0.4 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** camelcase@5.3.1 - https://www.npmjs.com/package/camelcase/v/5.3.1 | MIT @@ -28012,10 +28245,10 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** data-uri-to-buffer@6.0.2 - https://www.npmjs.com/package/data-uri-to-buffer/v/6.0.2 | MIT +** content-disposition@0.5.4 - https://www.npmjs.com/package/content-disposition/v/0.5.4 | MIT (The MIT License) -Copyright (c) 2014 Nathan Rajlich +Copyright (c) 2014-2017 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -28036,83 +28269,105 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ---------------- -** debug@4.4.3 - https://www.npmjs.com/package/debug/v/4.4.3 | MIT +** content-type@1.0.5 - https://www.npmjs.com/package/content-type/v/1.0.5 | MIT (The MIT License) -Copyright (c) 2014-2017 TJ Holowaychuk -Copyright (c) 2018-2021 Josh Junon +Copyright (c) 2015 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a copy of this software -and associated documentation files (the 'Software'), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------- -** decamelize@1.2.0 - https://www.npmjs.com/package/decamelize/v/1.2.0 | MIT -The MIT License (MIT) - -Copyright (c) Sindre Sorhus (sindresorhus.com) +** convert-source-map@2.0.0 - https://www.npmjs.com/package/convert-source-map/v/2.0.0 | MIT +Copyright 2013 Thorsten Lorenz. +All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. ---------------- -** decamelize@5.0.1 - https://www.npmjs.com/package/decamelize/v/5.0.1 | MIT -MIT License +** cookie-signature@1.0.7 - https://www.npmjs.com/package/cookie-signature/v/1.0.7 | MIT -Copyright (c) Sindre Sorhus (sindresorhus.com) +---------------- -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +** cookie@0.7.2 - https://www.npmjs.com/package/cookie/v/0.7.2 | MIT +(The MIT License) -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- -** degenerator@5.0.1 - https://www.npmjs.com/package/degenerator/v/5.0.1 | MIT ---------------- -** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT -Copyright Mathias Bynens +** data-uri-to-buffer@6.0.2 - https://www.npmjs.com/package/data-uri-to-buffer/v/6.0.2 | MIT +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -28121,21 +28376,69 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** debug@2.6.9 - https://www.npmjs.com/package/debug/v/2.6.9 | MIT +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ---------------- -** enquirer@2.4.1 - https://www.npmjs.com/package/enquirer/v/2.4.1 | MIT +** debug@4.4.3 - https://www.npmjs.com/package/debug/v/4.4.3 | MIT +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** decamelize@1.2.0 - https://www.npmjs.com/package/decamelize/v/1.2.0 | MIT The MIT License (MIT) -Copyright (c) 2016-present, Jon Schlinkert. +Copyright (c) Sindre Sorhus (sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28158,110 +28461,83 @@ THE SOFTWARE. ---------------- -** escodegen@2.1.0 - https://www.npmjs.com/package/escodegen/v/2.1.0 | BSD-2-Clause -Copyright (C) 2012 Yusuke Suzuki (twitter: @Constellation) and other contributors. +** decamelize@5.0.1 - https://www.npmjs.com/package/decamelize/v/5.0.1 | MIT +MIT License -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Copyright (c) Sindre Sorhus (sindresorhus.com) - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** esprima@4.0.1 - https://www.npmjs.com/package/esprima/v/4.0.1 | BSD-2-Clause -Copyright JS Foundation and other contributors, https://js.foundation/ +** degenerator@5.0.1 - https://www.npmjs.com/package/degenerator/v/5.0.1 | MIT -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +---------------- - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +** depd@2.0.0 - https://www.npmjs.com/package/depd/v/2.0.0 | MIT +(The MIT License) -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Copyright (c) 2014-2018 Douglas Christopher Wilson +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: ----------------- +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -** estraverse@5.3.0 - https://www.npmjs.com/package/estraverse/v/5.3.0 | BSD-2-Clause -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +---------------- +** destroy@1.2.0 - https://www.npmjs.com/package/destroy/v/1.2.0 | MIT ----------------- +The MIT License (MIT) -** esutils@2.0.3 - https://www.npmjs.com/package/esutils/v/2.0.3 | BSD-2-Clause -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** eventemitter3@4.0.7 - https://www.npmjs.com/package/eventemitter3/v/4.0.7 | MIT -The MIT License (MIT) +** dunder-proto@1.0.1 - https://www.npmjs.com/package/dunder-proto/v/1.0.1 | MIT +MIT License -Copyright (c) 2014 Arnout Kazemier +Copyright (c) 2024 ECMAScript Shims Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28284,10 +28560,11 @@ SOFTWARE. ---------------- -** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT -MIT License +** ee-first@1.1.1 - https://www.npmjs.com/package/ee-first/v/1.1.1 | MIT -Copyright (c) 2017 Evgeny Poberezkin +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28296,24 +28573,1346 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** encodeurl@2.0.0 - https://www.npmjs.com/package/encodeurl/v/2.0.0 | MIT +(The MIT License) + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** enquirer@2.4.1 - https://www.npmjs.com/package/enquirer/v/2.4.1 | MIT +The MIT License (MIT) + +Copyright (c) 2016-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** es-define-property@1.0.1 - https://www.npmjs.com/package/es-define-property/v/1.0.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** es-errors@1.3.0 - https://www.npmjs.com/package/es-errors/v/1.3.0 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** es-object-atoms@1.1.1 - https://www.npmjs.com/package/es-object-atoms/v/1.1.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** escape-html@1.0.3 - https://www.npmjs.com/package/escape-html/v/1.0.3 | MIT +(The MIT License) + +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** escodegen@2.1.0 - https://www.npmjs.com/package/escodegen/v/2.1.0 | BSD-2-Clause +Copyright (C) 2012 Yusuke Suzuki (twitter: @Constellation) and other contributors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** esprima@4.0.1 - https://www.npmjs.com/package/esprima/v/4.0.1 | BSD-2-Clause +Copyright JS Foundation and other contributors, https://js.foundation/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** estraverse@5.3.0 - https://www.npmjs.com/package/estraverse/v/5.3.0 | BSD-2-Clause +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** esutils@2.0.3 - https://www.npmjs.com/package/esutils/v/2.0.3 | BSD-2-Clause +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------------- + +** etag@1.8.1 - https://www.npmjs.com/package/etag/v/1.8.1 | MIT +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** eventemitter3@4.0.7 - https://www.npmjs.com/package/eventemitter3/v/4.0.7 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** express@4.22.2 - https://www.npmjs.com/package/express/v/4.22.2 | MIT +(The MIT License) + +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fast-glob@3.3.3 - https://www.npmjs.com/package/fast-glob/v/3.3.3 | MIT +The MIT License (MIT) + +Copyright (c) Denis Malinochkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fast-xml-parser@5.7.1 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.1 | MIT +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fast-xml-parser@5.7.2 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.2 | MIT +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fast-xml-parser@5.7.3 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.3 | MIT +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** fastq@1.20.1 - https://www.npmjs.com/package/fastq/v/1.20.1 | ISC +Copyright (c) 2015-2020, Matteo Collina + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** fill-range@7.1.1 - https://www.npmjs.com/package/fill-range/v/7.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** finalhandler@1.3.2 - https://www.npmjs.com/package/finalhandler/v/1.3.2 | MIT +(The MIT License) + +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** find-up@4.1.0 - https://www.npmjs.com/package/find-up/v/4.1.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** forwarded@0.2.0 - https://www.npmjs.com/package/forwarded/v/0.2.0 | MIT +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fresh@0.5.2 - https://www.npmjs.com/package/fresh/v/0.5.2 | MIT +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** fs-extra@11.3.4 - https://www.npmjs.com/package/fs-extra/v/11.3.4 | MIT +(The MIT License) + +Copyright (c) 2011-2024 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** function-bind@1.1.2 - https://www.npmjs.com/package/function-bind/v/1.1.2 | MIT +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +---------------- + +** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC + +---------------- + +** get-intrinsic@1.3.0 - https://www.npmjs.com/package/get-intrinsic/v/1.3.0 | MIT +MIT License + +Copyright (c) 2020 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** get-proto@1.0.1 - https://www.npmjs.com/package/get-proto/v/1.0.1 | MIT +MIT License + +Copyright (c) 2025 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** get-uri@6.0.5 - https://www.npmjs.com/package/get-uri/v/6.0.5 | MIT +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** glob-parent@5.1.2 - https://www.npmjs.com/package/glob-parent/v/5.1.2 | ISC +The ISC License + +Copyright (c) 2015, 2019 Elan Shanker + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** gopd@1.2.0 - https://www.npmjs.com/package/gopd/v/1.2.0 | MIT +MIT License + +Copyright (c) 2022 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** graceful-fs@4.2.11 - https://www.npmjs.com/package/graceful-fs/v/4.2.11 | ISC +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** has-symbols@1.1.0 - https://www.npmjs.com/package/has-symbols/v/1.1.0 | MIT +MIT License + +Copyright (c) 2016 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** hasown@2.0.2 - https://www.npmjs.com/package/hasown/v/2.0.2 | MIT +MIT License + +Copyright (c) Jordan Harband and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** http-errors@2.0.1 - https://www.npmjs.com/package/http-errors/v/2.0.1 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** http-proxy-agent@7.0.2 - https://www.npmjs.com/package/http-proxy-agent/v/7.0.2 | MIT +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** https-proxy-agent@7.0.6 - https://www.npmjs.com/package/https-proxy-agent/v/7.0.6 | MIT +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** iconv-lite@0.4.24 - https://www.npmjs.com/package/iconv-lite/v/0.4.24 | MIT +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** inherits@2.0.4 - https://www.npmjs.com/package/inherits/v/2.0.4 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +---------------- + +** ip-address@10.2.0 - https://www.npmjs.com/package/ip-address/v/10.2.0 | MIT +Copyright (C) 2011 by Beau Gunderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** ipaddr.js@1.9.1 - https://www.npmjs.com/package/ipaddr.js/v/1.9.1 | MIT +Copyright (C) 2011-2017 whitequark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** is-extglob@2.1.1 - https://www.npmjs.com/package/is-extglob/v/2.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** is-fullwidth-code-point@3.0.0 - https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** is-glob@4.0.3 - https://www.npmjs.com/package/is-glob/v/4.0.3 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** is-number@7.0.0 - https://www.npmjs.com/package/is-number/v/7.0.0 | MIT +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** json-source-map@0.6.1 - https://www.npmjs.com/package/json-source-map/v/0.6.1 | MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** jsonfile@6.2.0 - https://www.npmjs.com/package/jsonfile/v/6.2.0 | MIT +(The MIT License) + +Copyright (c) 2012-2015, JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** jsonschema@1.5.0 - https://www.npmjs.com/package/jsonschema/v/1.5.0 | MIT +jsonschema is licensed under MIT license. + +Copyright (C) 2012-2015 Tom de Grunt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** locate-path@5.0.0 - https://www.npmjs.com/package/locate-path/v/5.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** lodash.truncate@4.4.2 - https://www.npmjs.com/package/lodash.truncate/v/4.4.2 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lru-cache@7.18.3 - https://www.npmjs.com/package/lru-cache/v/7.18.3 | ISC +The ISC License + +Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** fast-glob@3.3.3 - https://www.npmjs.com/package/fast-glob/v/3.3.3 | MIT -The MIT License (MIT) +** math-intrinsics@1.1.0 - https://www.npmjs.com/package/math-intrinsics/v/1.1.0 | MIT +MIT License -Copyright (c) Denis Malinochkin +Copyright (c) 2024 ECMAScript Shims Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28336,62 +29935,65 @@ SOFTWARE. ---------------- -** fast-xml-parser@5.7.1 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.1 | MIT -MIT License +** media-typer@0.3.0 - https://www.npmjs.com/package/media-typer/v/0.3.0 | MIT +(The MIT License) -Copyright (c) 2017 Amit Kumar Gupta +Copyright (c) 2014 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** fast-xml-parser@5.7.2 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.2 | MIT -MIT License +** merge-descriptors@1.0.3 - https://www.npmjs.com/package/merge-descriptors/v/1.0.3 | MIT +(The MIT License) -Copyright (c) 2017 Amit Kumar Gupta +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** fast-xml-parser@5.7.3 - https://www.npmjs.com/package/fast-xml-parser/v/5.7.3 | MIT -MIT License +** merge2@1.4.1 - https://www.npmjs.com/package/merge2/v/1.4.1 | MIT +The MIT License (MIT) -Copyright (c) 2017 Amit Kumar Gupta +Copyright (c) 2014-2020 Teambition Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28414,25 +30016,36 @@ SOFTWARE. ---------------- -** fastq@1.20.1 - https://www.npmjs.com/package/fastq/v/1.20.1 | ISC -Copyright (c) 2015-2020, Matteo Collina +** methods@1.1.2 - https://www.npmjs.com/package/methods/v/1.1.2 | MIT +(The MIT License) -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** fill-range@7.1.1 - https://www.npmjs.com/package/fill-range/v/7.1.1 | MIT +** micromatch@4.0.8 - https://www.npmjs.com/package/micromatch/v/4.0.8 | MIT The MIT License (MIT) Copyright (c) 2014-present, Jon Schlinkert. @@ -28458,48 +30071,39 @@ THE SOFTWARE. ---------------- -** find-up@4.1.0 - https://www.npmjs.com/package/find-up/v/4.1.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** fs-extra@11.3.4 - https://www.npmjs.com/package/fs-extra/v/11.3.4 | MIT +** mime-db@1.52.0 - https://www.npmjs.com/package/mime-db/v/1.52.0 | MIT (The MIT License) -Copyright (c) 2011-2024 JP Richardson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. ----------------- +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC ---------------- -** get-uri@6.0.5 - https://www.npmjs.com/package/get-uri/v/6.0.5 | MIT +** mime-types@2.1.35 - https://www.npmjs.com/package/mime-types/v/2.1.35 | MIT (The MIT License) -Copyright (c) 2014 Nathan Rajlich +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -28520,66 +30124,95 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ---------------- -** glob-parent@5.1.2 - https://www.npmjs.com/package/glob-parent/v/5.1.2 | ISC -The ISC License +** mime@1.6.0 - https://www.npmjs.com/package/mime/v/1.6.0 | MIT +The MIT License (MIT) -Copyright (c) 2015, 2019 Elan Shanker +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** graceful-fs@4.2.11 - https://www.npmjs.com/package/graceful-fs/v/4.2.11 | ISC -The ISC License +** mime@2.6.0 - https://www.npmjs.com/package/mime/v/2.6.0 | MIT +The MIT License (MIT) -Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT -MIT License +** ms@2.0.0 - https://www.npmjs.com/package/ms/v/2.0.0 | MIT -Copyright (c) Sindre Sorhus (sindresorhus.com) +---------------- -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +** ms@2.1.3 - https://www.npmjs.com/package/ms/v/2.1.3 | MIT -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +---------------- + +** mute-stream@0.0.8 - https://www.npmjs.com/package/mute-stream/v/0.0.8 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** http-proxy-agent@7.0.2 - https://www.npmjs.com/package/http-proxy-agent/v/7.0.2 | MIT +** negotiator@0.6.3 - https://www.npmjs.com/package/negotiator/v/0.6.3 | MIT (The MIT License) -Copyright (c) 2013 Nathan Rajlich +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -28603,10 +30236,41 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** https-proxy-agent@7.0.6 - https://www.npmjs.com/package/https-proxy-agent/v/7.0.6 | MIT +** netmask@2.1.1 - https://www.npmjs.com/package/netmask/v/2.1.1 | MIT + +---------------- + +** object-inspect@1.13.4 - https://www.npmjs.com/package/object-inspect/v/1.13.4 | MIT +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** on-finished@2.4.1 - https://www.npmjs.com/package/on-finished/v/2.4.1 | MIT (The MIT License) -Copyright (c) 2013 Nathan Rajlich +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -28627,36 +30291,13 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** ip-address@10.2.0 - https://www.npmjs.com/package/ip-address/v/10.2.0 | MIT -Copyright (C) 2011 by Beau Gunderson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ---------------- -** is-extglob@2.1.1 - https://www.npmjs.com/package/is-extglob/v/2.1.1 | MIT +** p-finally@1.0.0 - https://www.npmjs.com/package/p-finally/v/1.0.0 | MIT The MIT License (MIT) -Copyright (c) 2014-2016, Jon Schlinkert +Copyright (c) Sindre Sorhus (sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28679,7 +30320,7 @@ THE SOFTWARE. ---------------- -** is-fullwidth-code-point@3.0.0 - https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 | MIT +** p-limit@2.3.0 - https://www.npmjs.com/package/p-limit/v/2.3.0 | MIT MIT License Copyright (c) Sindre Sorhus (sindresorhus.com) @@ -28693,131 +30334,63 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** is-glob@4.0.3 - https://www.npmjs.com/package/is-glob/v/4.0.3 | MIT -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ----------------- - -** is-number@7.0.0 - https://www.npmjs.com/package/is-number/v/7.0.0 | MIT -The MIT License (MIT) +** p-limit@3.1.0 - https://www.npmjs.com/package/p-limit/v/3.1.0 | MIT +MIT License -Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) Sindre Sorhus (https://sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** json-source-map@0.6.1 - https://www.npmjs.com/package/json-source-map/v/0.6.1 | MIT +** p-locate@4.1.0 - https://www.npmjs.com/package/p-locate/v/4.1.0 | MIT MIT License -Copyright (c) 2017 Evgeny Poberezkin +Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** jsonfile@6.2.0 - https://www.npmjs.com/package/jsonfile/v/6.2.0 | MIT -(The MIT License) +** p-queue@6.6.2 - https://www.npmjs.com/package/p-queue/v/6.6.2 | MIT +MIT License -Copyright (c) 2012-2015, JP Richardson +Copyright (c) Sindre Sorhus (https://sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** jsonschema@1.5.0 - https://www.npmjs.com/package/jsonschema/v/1.5.0 | MIT -jsonschema is licensed under MIT license. +** p-timeout@3.2.0 - https://www.npmjs.com/package/p-timeout/v/3.2.0 | MIT +MIT License -Copyright (C) 2012-2015 Tom de Grunt +Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** locate-path@5.0.0 - https://www.npmjs.com/package/locate-path/v/5.0.0 | MIT +** p-try@2.2.0 - https://www.npmjs.com/package/p-try/v/2.2.0 | MIT MIT License Copyright (c) Sindre Sorhus (sindresorhus.com) @@ -28831,24 +30404,41 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** lodash.truncate@4.4.2 - https://www.npmjs.com/package/lodash.truncate/v/4.4.2 | MIT -Copyright jQuery Foundation and other contributors +** pac-proxy-agent@7.2.0 - https://www.npmjs.com/package/pac-proxy-agent/v/7.2.0 | MIT +(The MIT License) -Based on Underscore.js, copyright Jeremy Ashkenas, -DocumentCloud and Investigative Reporters & Editors +Copyright (c) 2014 Nathan Rajlich -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/lodash/lodash +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The following license applies to all parts of this software except as -documented below: +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -==== +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** pac-resolver@7.0.1 - https://www.npmjs.com/package/pac-resolver/v/7.0.1 | MIT +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -28857,56 +30447,63 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -==== +---------------- -Copyright and related rights for sample code are waived via CC0. Sample -code is defined as all source code displayed within the prose of the -documentation. +** parseurl@1.3.3 - https://www.npmjs.com/package/parseurl/v/1.3.3 | MIT -CC0: http://creativecommons.org/publicdomain/zero/1.0/ +(The MIT License) -==== +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson -Files located in the node_modules and vendor directories are externally -maintained libraries used by this software which have their own -licenses; we recommend you read them, as their terms may differ from the -terms above. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** lru-cache@7.18.3 - https://www.npmjs.com/package/lru-cache/v/7.18.3 | ISC -The ISC License +** path-exists@4.0.0 - https://www.npmjs.com/package/path-exists/v/4.0.0 | MIT +MIT License -Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors +Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** merge2@1.4.1 - https://www.npmjs.com/package/merge2/v/1.4.1 | MIT +** path-to-regexp@0.1.13 - https://www.npmjs.com/package/path-to-regexp/v/0.1.13 | MIT The MIT License (MIT) -Copyright (c) 2014-2020 Teambition +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28915,24 +30512,24 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** micromatch@4.0.8 - https://www.npmjs.com/package/micromatch/v/4.0.8 | MIT +** picomatch@2.3.2 - https://www.npmjs.com/package/picomatch/v/2.3.2 | MIT The MIT License (MIT) -Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) 2017-present, Jon Schlinkert. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28955,10 +30552,10 @@ THE SOFTWARE. ---------------- -** mime@2.6.0 - https://www.npmjs.com/package/mime/v/2.6.0 | MIT +** picomatch@4.0.4 - https://www.npmjs.com/package/picomatch/v/4.0.4 | MIT The MIT License (MIT) -Copyright (c) 2010 Benjamin Thomas, Robert Kieffer +Copyright (c) 2017-present, Jon Schlinkert. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -28981,38 +30578,10 @@ THE SOFTWARE. ---------------- -** ms@2.1.3 - https://www.npmjs.com/package/ms/v/2.1.3 | MIT - ----------------- - -** mute-stream@0.0.8 - https://www.npmjs.com/package/mute-stream/v/0.0.8 | ISC -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ----------------- - -** netmask@2.1.1 - https://www.npmjs.com/package/netmask/v/2.1.1 | MIT - ----------------- - -** p-finally@1.0.0 - https://www.npmjs.com/package/p-finally/v/1.0.0 | MIT +** promptly@3.2.0 - https://www.npmjs.com/package/promptly/v/3.2.0 | MIT The MIT License (MIT) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2018 Made With MOXY Lda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29035,94 +30604,118 @@ THE SOFTWARE. ---------------- -** p-limit@2.3.0 - https://www.npmjs.com/package/p-limit/v/2.3.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** p-limit@3.1.0 - https://www.npmjs.com/package/p-limit/v/3.1.0 | MIT -MIT License +** proxy-addr@2.0.7 - https://www.npmjs.com/package/proxy-addr/v/2.0.7 | MIT +(The MIT License) -Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) 2014-2016 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** p-locate@4.1.0 - https://www.npmjs.com/package/p-locate/v/4.1.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) +** proxy-agent@6.5.0 - https://www.npmjs.com/package/proxy-agent/v/6.5.0 | MIT +(The MIT License) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2013 Nathan Rajlich -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** p-queue@6.6.2 - https://www.npmjs.com/package/p-queue/v/6.6.2 | MIT -MIT License +** proxy-from-env@1.1.0 - https://www.npmjs.com/package/proxy-from-env/v/1.1.0 | MIT +The MIT License -Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (C) 2016-2018 Rob Wu -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** p-timeout@3.2.0 - https://www.npmjs.com/package/p-timeout/v/3.2.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - +** qs@6.15.3 - https://www.npmjs.com/package/qs/v/6.15.3 | BSD-3-Clause ---------------- -** p-try@2.2.0 - https://www.npmjs.com/package/p-try/v/2.2.0 | MIT -MIT License +** queue-microtask@1.2.3 - https://www.npmjs.com/package/queue-microtask/v/1.2.3 | MIT +The MIT License (MIT) -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) Feross Aboukhadijeh -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** pac-proxy-agent@7.2.0 - https://www.npmjs.com/package/pac-proxy-agent/v/7.2.0 | MIT +** range-parser@1.2.1 - https://www.npmjs.com/package/range-parser/v/1.2.1 | MIT (The MIT License) -Copyright (c) 2014 Nathan Rajlich +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) 2013-2014 Jonathan Ong +Copyright (c) 2014-2022 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** read@1.0.7 - https://www.npmjs.com/package/read/v/1.0.7 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** require-directory@2.1.1 - https://www.npmjs.com/package/require-directory/v/2.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2011 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** path-exists@4.0.0 - https://www.npmjs.com/package/path-exists/v/4.0.0 | MIT -MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +---------------- -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +** require-main-filename@2.0.0 - https://www.npmjs.com/package/require-main-filename/v/2.0.0 | ISC +Copyright (c) 2016, Contributors -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** picomatch@2.3.2 - https://www.npmjs.com/package/picomatch/v/2.3.2 | MIT +** reusify@1.1.0 - https://www.npmjs.com/package/reusify/v/1.1.0 | MIT The MIT License (MIT) -Copyright (c) 2017-present, Jon Schlinkert. +Copyright (c) 2015-2024 Matteo Collina Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29198,24 +30844,50 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ---------------- -** picomatch@4.0.4 - https://www.npmjs.com/package/picomatch/v/4.0.4 | MIT +** run-parallel@1.2.0 - https://www.npmjs.com/package/run-parallel/v/1.2.0 | MIT The MIT License (MIT) -Copyright (c) 2017-present, Jon Schlinkert. +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** safe-buffer@5.2.1 - https://www.npmjs.com/package/safe-buffer/v/5.2.1 | MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29238,10 +30910,10 @@ THE SOFTWARE. ---------------- -** promptly@3.2.0 - https://www.npmjs.com/package/promptly/v/3.2.0 | MIT -The MIT License (MIT) +** safer-buffer@2.1.2 - https://www.npmjs.com/package/safer-buffer/v/2.1.2 | MIT +MIT License -Copyright (c) 2018 Made With MOXY Lda +Copyright (c) 2018 Nikita Skovoroda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29250,24 +30922,45 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** proxy-agent@6.5.0 - https://www.npmjs.com/package/proxy-agent/v/6.5.0 | MIT +** semver@7.8.4 - https://www.npmjs.com/package/semver/v/7.8.4 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** send@0.19.2 - https://www.npmjs.com/package/send/v/0.19.2 | MIT (The MIT License) -Copyright (c) 2013 Nathan Rajlich +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2014-2022 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -29288,96 +30981,30 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- - -** proxy-from-env@1.1.0 - https://www.npmjs.com/package/proxy-from-env/v/1.1.0 | MIT -The MIT License - -Copyright (C) 2016-2018 Rob Wu - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** queue-microtask@1.2.3 - https://www.npmjs.com/package/queue-microtask/v/1.2.3 | MIT -The MIT License (MIT) - -Copyright (c) Feross Aboukhadijeh - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** read@1.0.7 - https://www.npmjs.com/package/read/v/1.0.7 | ISC -The ISC License - -Copyright (c) Isaac Z. Schlueter and Contributors - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ---------------- -** require-directory@2.1.1 - https://www.npmjs.com/package/require-directory/v/2.1.1 | MIT -The MIT License (MIT) +** serve-static@1.16.3 - https://www.npmjs.com/package/serve-static/v/1.16.3 | MIT +(The MIT License) -Copyright (c) 2011 Troy Goode +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2016 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, @@ -29387,7 +31014,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** require-main-filename@2.0.0 - https://www.npmjs.com/package/require-main-filename/v/2.0.0 | ISC +** set-blocking@2.0.0 - https://www.npmjs.com/package/set-blocking/v/2.0.0 | ISC Copyright (c) 2016, Contributors Permission to use, copy, modify, and/or distribute this software @@ -29406,10 +31033,28 @@ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** reusify@1.1.0 - https://www.npmjs.com/package/reusify/v/1.1.0 | MIT -The MIT License (MIT) +** setprototypeof@1.2.0 - https://www.npmjs.com/package/setprototypeof/v/1.2.0 | ISC +Copyright (c) 2015, Wes Todd -Copyright (c) 2015-2024 Matteo Collina +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** side-channel-list@1.0.1 - https://www.npmjs.com/package/side-channel-list/v/1.0.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -29430,69 +31075,82 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------- -** run-parallel@1.2.0 - https://www.npmjs.com/package/run-parallel/v/1.2.0 | MIT -The MIT License (MIT) +** side-channel-map@1.0.1 - https://www.npmjs.com/package/side-channel-map/v/1.0.1 | MIT +MIT License -Copyright (c) Feross Aboukhadijeh +Copyright (c) 2024 Jordan Harband -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** semver@7.8.4 - https://www.npmjs.com/package/semver/v/7.8.4 | ISC -The ISC License +** side-channel-weakmap@1.0.2 - https://www.npmjs.com/package/side-channel-weakmap/v/1.0.2 | MIT +MIT License -Copyright (c) Isaac Z. Schlueter and Contributors +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- +** side-channel@1.1.1 - https://www.npmjs.com/package/side-channel/v/1.1.1 | MIT +MIT License ----------------- +Copyright (c) 2019 Jordan Harband -** set-blocking@2.0.0 - https://www.npmjs.com/package/set-blocking/v/2.0.0 | ISC -Copyright (c) 2016, Contributors +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Permission to use, copy, modify, and/or distribute this software -for any purpose with or without fee is hereby granted, provided -that the above copyright notice and this permission notice -appear in all copies. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE -LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- @@ -29637,6 +31295,34 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** statuses@2.0.2 - https://www.npmjs.com/package/statuses/v/2.0.2 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** string-width@4.2.3 - https://www.npmjs.com/package/string-width/v/4.2.3 | MIT @@ -29734,6 +31420,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** toidentifier@1.0.1 - https://www.npmjs.com/package/toidentifier/v/1.0.1 | MIT +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** tslib@2.8.1 - https://www.npmjs.com/package/tslib/v/2.8.1 | 0BSD @@ -29750,6 +31462,34 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** type-is@1.6.18 - https://www.npmjs.com/package/type-is/v/1.6.18 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** universalify@2.0.1 - https://www.npmjs.com/package/universalify/v/2.0.1 | MIT @@ -29775,6 +31515,165 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** unpipe@1.0.0 - https://www.npmjs.com/package/unpipe/v/1.0.0 | MIT +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** utils-merge@1.0.1 - https://www.npmjs.com/package/utils-merge/v/1.0.1 | MIT +The MIT License (MIT) + +Copyright (c) 2013-2017 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vary@1.1.2 - https://www.npmjs.com/package/vary/v/1.1.2 | MIT +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-jsonrpc@8.2.0 - https://www.npmjs.com/package/vscode-jsonrpc/v/8.2.0 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-jsonrpc@8.2.1 - https://www.npmjs.com/package/vscode-jsonrpc/v/8.2.1 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver-protocol@3.17.5 - https://www.npmjs.com/package/vscode-languageserver-protocol/v/3.17.5 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver-types@3.17.5 - https://www.npmjs.com/package/vscode-languageserver-types/v/3.17.5 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver@9.0.1 - https://www.npmjs.com/package/vscode-languageserver/v/9.0.1 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** which-module@2.0.1 - https://www.npmjs.com/package/which-module/v/2.0.1 | ISC From 24b828f3367c4312ac97afbc1b1fcb01c7186d34 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:44:38 -0400 Subject: [PATCH 17/28] feat: run synth with JSII source traces (#1697) Setting JSII_HOST_STACK_TRACES=1 on the synth subprocess makes jsii forward the host-language frames, so resolveFrames finds the .py/.java source. ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts | 2 ++ packages/@aws-cdk/cdk-explorer/test/core/synth-runner.test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts b/packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts index f496910d9..838e1b912 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/synth-runner.ts @@ -58,6 +58,8 @@ export async function runSynth(options: SynthRunnerOptions): Promise { const result = await run(toolkit, projectDir); expect(result).toEqual({ status: 'success' }); - expect(toolkit.fromCdkApp).toHaveBeenCalledWith(APP, { workingDirectory: projectDir, lookups: false }); + expect(toolkit.fromCdkApp).toHaveBeenCalledWith(APP, { workingDirectory: projectDir, lookups: false, env: { JSII_HOST_STACK_TRACES: '1' } }); expect(cached.dispose).toHaveBeenCalledTimes(1); }); From 9aa91684e3cef0485c665215a7c5b707be9a3cb8 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:15:28 -0400 Subject: [PATCH 18/28] feat: LSP hover with resolved CFN properties and template locations (#1669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds LSP hover on construct creation lines showing the resolved CloudFormation logical ID, resource type, construct path, and top-level CFN properties with values - Each property value links to its exact line in the synthesized template; the logical ID header links to the resource block - Multi-resource constructs show auxiliaries inline (≤5 listed individually, >5 collapsed to a type histogram like "6× Subnet, 3× RouteTable") - Extends `template-ranges.ts` with `resolveResourceRanges` / `indexTemplateRanges` to resolve per-property character ranges in a single parse - Exports `cfnProperties` from the construct tree builder so the hover has access to resolved property values example hover: Screenshot 2026-06-24 at 12 12
52 PM Screenshot 2026-06-24 at 12 13
44 PM Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../@aws-cdk/cdk-explorer/lib/lsp/codelens.ts | 4 +- .../@aws-cdk/cdk-explorer/lib/lsp/hover.ts | 315 ++++++++++++++++++ .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 11 + .../cdk-explorer/test/lsp/hover.test.ts | 208 ++++++++++++ .../cloud-assembly-api/lib/construct-tree.ts | 13 +- .../cloud-assembly-api/lib/template-ranges.ts | 74 +++- .../test/template-ranges.test.ts | 53 ++- .../lib/cloud-assembly/metadata-schema.ts | 7 + 8 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/lsp/hover.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/lsp/hover.test.ts diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts index ef217b1af..f12e0b5d1 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/codelens.ts @@ -88,7 +88,7 @@ async function commandFor(nodes: readonly ResourceConstruct[]): Promise } /** A construct that produces a CFN resource and carries a source location. */ -interface ResourceConstruct extends ConstructNode { +export interface ResourceConstruct extends ConstructNode { readonly sourceLocation: SourceLocation; readonly logicalId: string; readonly type: string; @@ -99,7 +99,7 @@ interface ResourceConstruct extends ConstructNode { * and has a source location in the requested file. Wrapper nodes and non-TS * constructs are excluded. */ -function isResourceOnFile(node: ConstructNode, fileUri: string): node is ResourceConstruct { +export function isResourceOnFile(node: ConstructNode, fileUri: string): node is ResourceConstruct { return ( node.sourceLocation !== undefined && node.logicalId !== undefined && diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/hover.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/hover.ts new file mode 100644 index 000000000..d53174b3e --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/hover.ts @@ -0,0 +1,315 @@ +import { pathToFileURL } from 'url'; +import { type ConstructIndex, indexTemplateRanges, type OffsetRange, type TemplateRanges } from '@aws-cdk/cloud-assembly-api'; +import { type Hover, MarkupKind, type Position, type Range } from 'vscode-languageserver/node'; +import { isResourceOnFile, type ResourceConstruct } from './codelens'; +import { offsetsToRange } from './positions'; +import type { ConstructNode } from '../core/assembly-reader'; + +/** A clickable template target: a file URI and a 1-based line. */ +export interface LinkTarget { + readonly uri: string; + readonly line: number; +} + +/** + * Template line targets for a hover, resolved from the synthesized template(s). + * `resourceLocations` is keyed by construct path (the hovered resource and its auxiliaries); + * paths are globally unique, where stack-relative logical ids can collide across + * templates. `properties` is keyed by the lower-cased property name of the + * primary resource, so the L1 camelCase name and the template PascalCase name + * match regardless of acronym casing. Absent when the template can't be read, + * in which case values render without links. + */ +export interface HoverLinks { + readonly resourceLocations: Record; + readonly properties: Record; +} + +/** A construct line's primary resource (the one the hover leads with) and its other resources. */ +export interface PrimarySelection { + readonly primary: ResourceConstruct; + readonly others: readonly ResourceConstruct[]; +} + +/** Top-level properties shown before truncating with a "+N more" line. */ +const MAX_PROPERTIES = 12; +/** Auxiliary resources listed individually before collapsing to a type histogram. */ +const MAX_LINKED_AUX = 5; +/** Characters of a string value shown before truncating with an ellipsis. */ +const MAX_STRING_LENGTH = 60; +/** Object keys previewed inline before collapsing with an ellipsis. */ +const MAX_OBJECT_KEYS = 4; +/** Distinct CFN types shown in an auxiliary histogram before "+N more". */ +const MAX_HISTOGRAM_TYPES = 6; + +/** + * Resource nodes whose creation line is the hovered position in `uri`. Reuses + * the CodeLens resource predicate, narrowed to the single hovered line. + */ +export function resourceNodesOnLine( + index: ConstructIndex, + uri: string, + position: Position, +): ResourceConstruct[] { + // sourceLocation is 1-based; LSP positions are 0-based. + const line = position.line + 1; + return [...index].filter( + (node): node is ResourceConstruct => isResourceOnFile(node, uri) && node.sourceLocation.line === line, + ); +} + +/** + * Resolves the resource(s) on the hovered line and builds their hover, reading + * each referenced template once (via the injected seam) to resolve block and + * property link targets. Returns undefined when no resource maps to the line. + */ +export async function hoverForPosition( + index: ConstructIndex, + uri: string, + position: Position, + readTemplate: (file: string) => Promise, +): Promise { + const nodes = resourceNodesOnLine(index, uri, position); + if (nodes.length === 0) { + return undefined; + } + // Select the primary once and thread it through both link resolution and + // rendering, so a single place owns "which resource is canonical". + const primaryResource = selectPrimary(nodes); + const links = await resolveHoverLinks(nodes, primaryResource?.primary, readTemplate); + const range: Range = { + start: { line: position.line, character: 0 }, + end: { line: position.line, character: Number.MAX_VALUE }, + }; + return buildHover(nodes, primaryResource, links, range); +} + +/** A template read and parsed once: its text (for offset->line) and its range index. */ +interface ResolvedTemplate { + readonly text: string; + readonly ranges: TemplateRanges; +} + +/** + * Resolves the link targets for a hover: every resource's block, plus the + * primary resource's per-property lines. Each referenced template is read and + * parsed at most once, so a multi-resource construct whose resources share one + * template (for example a VPC and its dozens of auxiliaries) pays a single + * parse. Blocks are keyed by construct path, which is globally unique, because + * stack-relative logical ids can collide across templates. + */ +async function resolveHoverLinks( + nodes: readonly ResourceConstruct[], + primary: ResourceConstruct | undefined, + readTemplate: (file: string) => Promise, +): Promise { + const templates = new Map(); + const resolveOnce = async (file: string): Promise => { + if (!templates.has(file)) { + const text = await readTemplate(file); + const ranges = text === undefined ? undefined : indexTemplateRanges(text); + templates.set(file, text !== undefined && ranges !== undefined ? { text, ranges } : undefined); + } + return templates.get(file); + }; + + const resourceLocations: Record = {}; + const properties: Record = {}; + for (const node of nodes) { + if (node.templateFile === undefined) { + continue; + } + const template = await resolveOnce(node.templateFile); + if (template === undefined) { + continue; + } + const uri = pathToFileURL(node.templateFile).toString(); + if (node === primary) { + const resource = template.ranges.resource(node.logicalId); + if (resource === undefined) { + continue; + } + resourceLocations[node.path] = { uri, line: lineOf(template.text, resource.block) }; + for (const [name, range] of Object.entries(resource.properties)) { + // Lower-case the key so the tree's L1 camelCase prop name matches the + // template's PascalCase name even on an acronym (sseSpecification vs + // SSESpecification); the two only differ in letter case. + properties[name.toLowerCase()] = { uri, line: lineOf(template.text, range) }; + } + } else { + const block = template.ranges.block(node.logicalId); + if (block !== undefined) { + resourceLocations[node.path] = { uri, line: lineOf(template.text, block) }; + } + } + } + return { resourceLocations, properties }; +} + +/** 1-based template line of a range's start (the file: `#L` fragment is 1-based). */ +function lineOf(text: string, range: OffsetRange): number { + return offsetsToRange(text, range).start.line + 1; +} + +/** + * Builds the hover for the resource(s) created on a line, given the selected + * primary resource. When one resource is the clear primary, its resolved + * properties are shown and the rest are listed under "Also creates"; when + * several resources tie at the shallowest depth with no single primary, + * `primaryResource` is undefined and a resource summary is shown instead. + * Returns undefined when no resource maps to the line. + */ +export function buildHover( + nodes: readonly ResourceConstruct[], + primaryResource: PrimarySelection | undefined, + links: HoverLinks | undefined, + range: Range, +): Hover | undefined { + if (nodes.length === 0) { + return undefined; + } + const value = primaryResource === undefined + ? renderSummary(nodes, links) + : renderResource(primaryResource.primary, primaryResource.others, links); + return { contents: { kind: MarkupKind.Markdown, value }, range }; +} + +/** + * Selects the line's primary resource: the uniquely shallowest resource on the + * line, which is usually but not always the construct's default child. When + * several resources tie at the shallowest depth, a single default child + * (`Resource`/`Default`) breaks the tie; if none or more than one qualifies, + * there is no clear primary and this returns undefined. `others` holds the rest. + */ +export function selectPrimary(nodes: readonly ResourceConstruct[]): PrimarySelection | undefined { + const byDepth = [...nodes].sort((a, b) => segments(a) - segments(b)); + if (byDepth.length === 1) { + return { primary: byDepth[0], others: [] }; + } + // Uniquely shallowest → clear primary. + if (segments(byDepth[0]) !== segments(byDepth[1])) { + return { primary: byDepth[0], others: byDepth.slice(1) }; + } + // Tied at shallowest depth: check if exactly one is the default child. + const shallowest = byDepth.filter((n) => segments(n) === segments(byDepth[0])); + const defaults = shallowest.filter((n) => n.id === 'Resource' || n.id === 'Default'); + if (defaults.length === 1) { + const primary = defaults[0]; + return { primary, others: byDepth.filter((n) => n !== primary) }; + } + return undefined; +} + +function segments(node: ConstructNode): number { + return node.path.split('/').length; +} + +function renderResource( + primary: ResourceConstruct, + others: readonly ResourceConstruct[], + links: HoverLinks | undefined, +): string { + const lines = [ + `${linked(`**${primary.logicalId}**`, links?.resourceLocations[primary.path])} · \`${primary.type}\``, + `\`${primary.path}\``, + '', + ...propertyLines(primary, links), + ]; + if (others.length > 0) { + lines.push('', alsoCreates(others, links)); + } + return lines.join('\n'); +} + +function propertyLines(primary: ResourceConstruct, links: HoverLinks | undefined): string[] { + const properties = primary.cfnProperties ?? {}; + const keys = Object.keys(properties); + const lines = keys.slice(0, MAX_PROPERTIES).map((key) => { + const value = `\`${renderValue(properties[key])}\``; + return `- \`${key}\`: ${linked(value, links?.properties[key.toLowerCase()])}`; + }); + if (keys.length > MAX_PROPERTIES) { + lines.push(`- +${keys.length - MAX_PROPERTIES} more`); + } + return lines; +} + +function alsoCreates(others: readonly ResourceConstruct[], links: HoverLinks | undefined): string { + if (others.length > MAX_LINKED_AUX) { + return `Also creates ${others.length} resources: ${histogram(others)}`; + } + const items = others.map((node) => linked(`\`${node.type}\``, links?.resourceLocations[node.path])); + return `Also creates: ${items.join(' · ')}`; +} + +function renderSummary(nodes: readonly ResourceConstruct[], links: HoverLinks | undefined): string { + const items = nodes.map((node) => `- ${linked(`\`${node.type}\``, links?.resourceLocations[node.path])} \`${node.path}\``); + return [`**${nodes.length} resources on this line**`, '', ...items].join('\n'); +} + +/** Wrap `text` in a markdown link to `target`, or return it unchanged when absent. */ +function linked(text: string, target: LinkTarget | undefined): string { + return target === undefined ? text : `[${text}](${target.uri}#L${target.line})`; +} + +/** Compact, single-line rendering of a resolved CloudFormation property value. */ +export function renderValue(value: unknown): string { + if (typeof value === 'string') { + return `"${truncate(value, MAX_STRING_LENGTH)}"`; + } + if (typeof value === 'number' || typeof value === 'boolean' || value === null) { + return String(value); + } + if (Array.isArray(value)) { + return `[${value.length} ${value.length === 1 ? 'item' : 'items'}]`; + } + if (typeof value === 'object') { + const record = value as Record; + const intrinsic = renderIntrinsic(record); + if (intrinsic !== undefined) { + return intrinsic; + } + const keys = Object.keys(record); + if (keys.length === 0) { + return '{}'; + } + return `{ ${keys.slice(0, MAX_OBJECT_KEYS).join(', ')}${keys.length > MAX_OBJECT_KEYS ? ', …' : ''} }`; + } + return String(value); +} + +/** CFN intrinsics ({Ref}, {Fn::GetAtt}, other Fn::*) rendered compactly, else undefined. */ +function renderIntrinsic(record: Record): string | undefined { + const keys = Object.keys(record); + if (keys.length !== 1) { + return undefined; + } + const key = keys[0]; + if (key === 'Ref') { + return `{Ref ${String(record[key])}}`; + } + if (key === 'Fn::GetAtt') { + const target = record[key]; + return `{Fn::GetAtt ${Array.isArray(target) ? target.join('.') : String(target)}}`; + } + if (key.startsWith('Fn::')) { + return `{${key} …}`; + } + return undefined; +} + +/** Group auxiliary resources by short CFN type, most common first, e.g. "8× Subnet". */ +function histogram(nodes: readonly ResourceConstruct[]): string { + const counts = new Map(); + for (const node of nodes) { + const short = node.type.split('::').pop() ?? node.type; + counts.set(short, (counts.get(short) ?? 0) + 1); + } + const parts = [...counts].sort((a, b) => b[1] - a[1]).map(([type, count]) => `${count}× ${type}`); + const shown = parts.slice(0, MAX_HISTOGRAM_TYPES); + return shown.join(', ') + (parts.length > shown.length ? `, … +${parts.length - shown.length} more` : ''); +} + +function truncate(text: string, max: number): string { + return text.length <= max ? text : `${text.slice(0, max - 1)}…`; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index a333d3930..7b2f7f8be 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -18,6 +18,8 @@ import { type DidSaveTextDocumentParams, type Diagnostic, type ExecuteCommandParams, + type Hover, + type HoverParams, type InitializeParams, type InitializeResult, type Location, @@ -27,6 +29,7 @@ import { import { codeLensesForFile } from './codelens'; import { executeCommand, SUPPORTED_COMMANDS, type NotifySink } from './commands'; import { mapViolationsToDiagnostics } from './diagnostics'; +import { hoverForPosition } from './hover'; import { offsetAtPosition } from './positions'; import { synthFailureDiagnostics } from './synth-diagnostics'; import { sourceTargetAtTemplateOffset } from './template-locator'; @@ -124,6 +127,7 @@ export interface LspHandlers { onCodeLens(params: CodeLensParams): Promise; onDefinition(params: DefinitionParams): Promise; onExecuteCommand(params: ExecuteCommandParams): Promise; + onHover(params: HoverParams): Promise; onShutdown(): void; } @@ -342,6 +346,8 @@ export function createLspHandlers(options: LspHandlerOptions): LspHandlers { // Go-to-definition from a synthesized template back to construct source. definitionProvider: true, executeCommandProvider: { commands: [...SUPPORTED_COMMANDS] }, + // Hover a construct's creation line to see its resolved CFN properties. + hoverProvider: true, }, }; }, @@ -419,6 +425,10 @@ export function createLspHandlers(options: LspHandlerOptions): LspHandlers { notify, }); }, + onHover(params) { + return hoverForPosition(cachedIndex, params.textDocument.uri, params.position, + (file) => fs.promises.readFile(file, 'utf-8').catch(() => undefined)); + }, onShutdown() { shutdownRequested = true; void assemblyWatcher?.close(); @@ -493,6 +503,7 @@ export function startServer(options: LspServerOptions): void { connection.onCodeLens((params) => handlers.onCodeLens(params)); connection.onDefinition((params) => handlers.onDefinition(params)); connection.onExecuteCommand((params) => handlers.onExecuteCommand(params)); + connection.onHover((params) => handlers.onHover(params)); connection.onShutdown(() => handlers.onShutdown()); connection.onExit(() => process.exit(0)); diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/hover.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/hover.test.ts new file mode 100644 index 000000000..127c4ff74 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/hover.test.ts @@ -0,0 +1,208 @@ +import { pathToFileURL } from 'url'; +import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import { type Range } from 'vscode-languageserver/node'; +import type { ConstructNode } from '../../lib/core/assembly-reader'; +import type { ResourceConstruct } from '../../lib/lsp/codelens'; +import { buildHover, hoverForPosition, type HoverLinks, renderValue, resourceNodesOnLine, selectPrimary } from '../../lib/lsp/hover'; + +const RANGE: Range = { start: { line: 9, character: 0 }, end: { line: 9, character: 0 } }; +const FILE = '/app/lib/app.ts'; +const FILE_URI = pathToFileURL(FILE).toString(); + +function res( + path: string, + logicalId: string, + type: string, + opts: { cfnProperties?: Record; file?: string; line?: number } = {}, +): ResourceConstruct { + return { + path, + id: path.split('/').slice(-1)[0], + logicalId, + type, + sourceLocation: { file: opts.file ?? FILE, line: opts.line ?? 10, column: 3 }, + cfnProperties: opts.cfnProperties, + children: [], + }; +} + +describe('renderValue', () => { + test('quotes strings and truncates long ones', () => { + expect(renderValue('nodejs16.x')).toBe('"nodejs16.x"'); + expect(renderValue('x'.repeat(80))).toBe(`"${'x'.repeat(59)}…"`); + }); + + test('renders primitives directly', () => { + expect(renderValue(512)).toBe('512'); + expect(renderValue(true)).toBe('true'); + expect(renderValue(null)).toBe('null'); + }); + + test('collapses arrays to a count', () => { + expect(renderValue([])).toBe('[0 items]'); + expect(renderValue([1])).toBe('[1 item]'); + expect(renderValue([1, 2, 3])).toBe('[3 items]'); + }); + + test('collapses objects to their first keys', () => { + expect(renderValue({})).toBe('{}'); + expect(renderValue({ a: 1, b: 2 })).toBe('{ a, b }'); + expect(renderValue({ a: 1, b: 2, c: 3, d: 4, e: 5 })).toBe('{ a, b, c, d, … }'); + }); + + test('renders intrinsics compactly', () => { + expect(renderValue({ Ref: 'AppVpc80F1F7F9' })).toBe('{Ref AppVpc80F1F7F9}'); + expect(renderValue({ 'Fn::GetAtt': ['Role592E70E9', 'Arn'] })).toBe('{Fn::GetAtt Role592E70E9.Arn}'); + expect(renderValue({ 'Fn::Sub': 'arn:${X}' })).toBe('{Fn::Sub …}'); + }); +}); + +describe('selectPrimary', () => { + test('a single resource is its own primary', () => { + const only = res('Stack/Bucket/Resource', 'B1', 'AWS::S3::Bucket'); + expect(selectPrimary([only])).toEqual({ primary: only, others: [] }); + }); + + test('the uniquely shallowest resource is primary, the rest are others', () => { + const fn = res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function'); + const role = res('Stack/Fn/ServiceRole/Resource', 'Role1', 'AWS::IAM::Role'); + const policy = res('Stack/Fn/ServiceRole/DefaultPolicy/Resource', 'Pol1', 'AWS::IAM::Policy'); + expect(selectPrimary([role, fn, policy])).toEqual({ primary: fn, others: [role, policy] }); + }); + + test('a tie at the shallowest depth has no single primary', () => { + const lb = res('Stack/Svc/LB/Resource', 'Lb1', 'AWS::ElasticLoadBalancingV2::LoadBalancer'); + const task = res('Stack/Svc/Task/Resource', 'Task1', 'AWS::ECS::TaskDefinition'); + expect(selectPrimary([lb, task])).toBeUndefined(); + }); + + test('the default child is primary even when tied at the shallowest depth', () => { + // A Vpc creates its CfnVPC (default child `Resource`) and an InternetGateway + // as siblings at the same depth; the default child is still the primary. + const vpc = res('Stack/Vpc/Resource', 'Vpc1', 'AWS::EC2::VPC'); + const igw = res('Stack/Vpc/IGW', 'Igw1', 'AWS::EC2::InternetGateway'); + expect(selectPrimary([igw, vpc])).toEqual({ primary: vpc, others: [igw] }); + }); +}); + +describe('buildHover', () => { + const links: HoverLinks = { + resourceLocations: { + 'Stack/Fn/Resource': { uri: FILE_URI, line: 5 }, + 'Stack/Fn/ServiceRole/Resource': { uri: FILE_URI, line: 40 }, + }, + // resolveHoverLinks keys properties by lower-cased name (see HoverLinks). + properties: { runtime: { uri: FILE_URI, line: 8 } }, + }; + + test('returns undefined when no resource maps to the line', () => { + expect(buildHover([], undefined, links, RANGE)).toBeUndefined(); + }); + + test('links the logical id to its block and the value to its property line', () => { + const fn = res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function', { cfnProperties: { runtime: 'nodejs16.x' } }); + const value = buildHover([fn], selectPrimary([fn]), links, RANGE)!.contents as { value: string }; + expect(value.value).toContain(`[**Fn1**](${FILE_URI}#L5) · \`AWS::Lambda::Function\``); + expect(value.value).toContain('`Stack/Fn/Resource`'); + expect(value.value).toContain(`- \`runtime\`: [\`"nodejs16.x"\`](${FILE_URI}#L8)`); + }); + + test('renders a value without a link when no range resolves for it', () => { + const fn = res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function', { cfnProperties: { memorySize: 512 } }); + const value = (buildHover([fn], selectPrimary([fn]), links, RANGE)!.contents as { value: string }).value; + expect(value).toContain('- `memorySize`: `512`'); + expect(value).not.toContain('memorySize`]'); + }); + + test('renders everything plainly when links are absent', () => { + const fn = res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function', { cfnProperties: { runtime: 'nodejs16.x' } }); + const value = (buildHover([fn], selectPrimary([fn]), undefined, RANGE)!.contents as { value: string }).value; + expect(value).toContain('**Fn1** · `AWS::Lambda::Function`'); + expect(value).not.toContain(']('); + }); + + test('caps properties and appends a "+N more" line', () => { + const cfnProperties = Object.fromEntries(Array.from({ length: 15 }, (_, i) => [`p${i}`, i])); + const fn = res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function', { cfnProperties }); + const value = (buildHover([fn], selectPrimary([fn]), undefined, RANGE)!.contents as { value: string }).value; + expect(value).toContain('- `p11`: `11`'); + expect(value).not.toContain('- `p12`:'); + expect(value).toContain('- +3 more'); + }); + + test('shows only the primary values and lists the others under "Also creates"', () => { + const fn = res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function', { cfnProperties: { runtime: 'nodejs16.x' } }); + const role = res('Stack/Fn/ServiceRole/Resource', 'Role1', 'AWS::IAM::Role', { cfnProperties: { roleName: 'x' } }); + const nodes = [fn, role]; + const value = (buildHover(nodes, selectPrimary(nodes), links, RANGE)!.contents as { value: string }).value; + expect(value).toContain('- `runtime`:'); + expect(value).not.toContain('roleName'); + expect(value).toContain(`Also creates: [\`AWS::IAM::Role\`](${FILE_URI}#L40)`); + }); + + test('collapses a large auxiliary set to a type histogram', () => { + const vpc = res('Stack/Vpc/Resource', 'Vpc1', 'AWS::EC2::VPC'); + const aux = [ + ...Array.from({ length: 3 }, (_, i) => res(`Stack/Vpc/S${i}/Subnet`, `S${i}`, 'AWS::EC2::Subnet')), + ...Array.from({ length: 2 }, (_, i) => res(`Stack/Vpc/R${i}/RouteTable`, `R${i}`, 'AWS::EC2::RouteTable')), + res('Stack/Vpc/Igw/Resource', 'Igw1', 'AWS::EC2::InternetGateway'), + ]; + const nodes = [vpc, ...aux]; + const value = (buildHover(nodes, selectPrimary(nodes), undefined, RANGE)!.contents as { value: string }).value; + expect(value).toContain('Also creates 6 resources:'); + expect(value).toContain('3× Subnet'); + expect(value).toContain('2× RouteTable'); + }); + + test('shows a resource summary when several resources tie at the shallowest depth', () => { + const lb = res('Stack/Svc/LB/Resource', 'Lb1', 'AWS::ElasticLoadBalancingV2::LoadBalancer'); + const task = res('Stack/Svc/Task/Resource', 'Task1', 'AWS::ECS::TaskDefinition'); + const nodes = [lb, task]; + const value = (buildHover(nodes, selectPrimary(nodes), undefined, RANGE)!.contents as { value: string }).value; + expect(value).toContain('**2 resources on this line**'); + expect(value).not.toContain('Also creates'); + }); +}); + +describe('resourceNodesOnLine', () => { + test('returns only resource nodes on the hovered file and line', () => { + const onLine = res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function', { line: 10 }); + const otherLine = res('Stack/Other/Resource', 'Ot1', 'AWS::S3::Bucket', { line: 20 }); + const otherFile = res('Stack/Elsewhere/Resource', 'El1', 'AWS::S3::Bucket', { file: '/app/lib/other.ts', line: 10 }); + const wrapper: ConstructNode = { + path: 'Stack/Wrapper', + id: 'Wrapper', + sourceLocation: { file: FILE, line: 10, column: 1 }, + children: [], + }; + const index = ConstructIndex.fromTree([onLine, otherLine, otherFile, wrapper]); + + const found = resourceNodesOnLine(index, FILE_URI, { line: 9, character: 0 }); + expect(found.map((n) => n.logicalId)).toEqual(['Fn1']); + }); +}); + +describe('hoverForPosition', () => { + const TEMPLATE = JSON.stringify( + { Resources: { Fn1: { Type: 'AWS::Lambda::Function', Properties: { Runtime: 'nodejs16.x' } } } }, + undefined, + 1, + ); + + test('resolves block and per-property links from the template', async () => { + const fn: ResourceConstruct = { + ...res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function', { cfnProperties: { runtime: 'nodejs16.x' } }), + templateFile: '/app/cdk.out/Stack.template.json', + }; + const index = ConstructIndex.fromTree([fn]); + const hover = await hoverForPosition(index, FILE_URI, { line: 9, character: 0 }, async () => TEMPLATE); + const value = (hover!.contents as { value: string }).value; + expect(value).toMatch(/\[\*\*Fn1\*\*\]\(file:.*#L\d+\) · `AWS::Lambda::Function`/); + expect(value).toMatch(/- `runtime`: \[`"nodejs16\.x"`\]\(file:.*#L\d+\)/); + }); + + test('returns undefined when no resource is on the hovered line', async () => { + const index = ConstructIndex.fromTree([res('Stack/Fn/Resource', 'Fn1', 'AWS::Lambda::Function')]); + expect(await hoverForPosition(index, FILE_URI, { line: 99, character: 0 }, async () => '')).toBeUndefined(); + }); +}); diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts index 930edfa0a..12b6f07ec 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/construct-tree.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { ArtifactMetadataEntryType, CFN_RESOURCE_TYPE_ATTRIBUTE, type MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; +import { ArtifactMetadataEntryType, CFN_RESOURCE_PROPS_ATTRIBUTE, CFN_RESOURCE_TYPE_ATTRIBUTE, type MetadataEntry } from '@aws-cdk/cloud-assembly-schema'; import type { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; import { ASSET_RESOURCE_METADATA_PATH_KEY } from './assets'; import type { CloudAssembly } from './cloud-assembly'; @@ -27,6 +27,11 @@ export interface ConstructTreeNode { * only for CFN resources whose template is resolvable. */ readonly templateFile?: string; + /** + * Resolved CloudFormation resource properties (the synthesized values from + * the `aws:cdk:cloudformation:props` attribute), if this is a CFN resource. + */ + readonly cfnProperties?: Record; readonly children: readonly ConstructTreeNode[]; } @@ -40,6 +45,7 @@ export interface ConstructTreeNodeFields { readonly type?: string; readonly logicalId?: string; readonly templateFile?: string; + readonly cfnProperties?: Record; readonly children: readonly T[]; } @@ -302,6 +308,9 @@ function prepareNode( const cfnTypeRaw = raw.attributes?.[CFN_RESOURCE_TYPE_ATTRIBUTE]; const cfnType = typeof cfnTypeRaw === 'string' ? cfnTypeRaw : undefined; + const cfnPropsRaw = raw.attributes?.[CFN_RESOURCE_PROPS_ATTRIBUTE]; + const cfnProperties = + typeof cfnPropsRaw === 'object' && cfnPropsRaw !== null ? (cfnPropsRaw as Record) : undefined; // A NestedStack switches its subtree to the nested scope; other nodes inherit. const childInputs: ChildInput[] = Object.values(raw.children ?? {}) @@ -311,7 +320,7 @@ function prepareNode( // Only CFN resources (those with a logical ID) carry a templateFile. const nodeTemplateFile = logicalId !== undefined ? scope.file : undefined; return { - base: { path: raw.path, id: raw.id, type: cfnType, logicalId, templateFile: nodeTemplateFile }, + base: { path: raw.path, id: raw.id, type: cfnType, logicalId, templateFile: nodeTemplateFile, cfnProperties }, owner, constructPath: raw.path, childInputs, diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts b/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts index 73793b754..730c9c162 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts @@ -23,12 +23,80 @@ export interface OffsetRange { * resource. */ export function resolveResourceRange(templateText: string, logicalId: string): OffsetRange | undefined { + return indexTemplateRanges(templateText)?.block(logicalId); +} + +/** A resource's block range plus the range of each of its top-level properties. */ +export interface ResourceRanges { + /** The resource's value block `{ ... }` (value-only, like {@link resolveResourceRange}). */ + readonly block: OffsetRange; + /** Each top-level property keyed by name, covering the whole `"Key": value` entry. */ + readonly properties: Record; +} + +/** + * Resolves, in a single parse, a resource's block range and the range of each + * of its top-level properties. Property ranges span `"Key": value` so a + * navigation lands on the named property. + * + * Returns `undefined` when the text is not valid JSON or there is no such resource. + */ +export function resolveResourceRanges(templateText: string, logicalId: string): ResourceRanges | undefined { + return indexTemplateRanges(templateText)?.resource(logicalId); +} + +/** + * A template parsed once into a queryable range index, so a caller resolving + * several resources (for example a hover over a multi-resource construct) pays + * the parse cost once instead of per resource. + */ +export interface TemplateRanges { + /** The resource's value-block range, or `undefined` if there is no such resource. */ + block(logicalId: string): OffsetRange | undefined; + /** The resource's block plus its top-level property ranges, or `undefined` if absent. */ + resource(logicalId: string): ResourceRanges | undefined; +} + +/** + * Parse a template once into a {@link TemplateRanges} index. Returns `undefined` + * when the text is not valid JSON. The single-resource {@link resolveResourceRange} + * and {@link resolveResourceRanges} are thin wrappers over this. + */ +export function indexTemplateRanges(templateText: string): TemplateRanges | undefined { const pointers = parsePointers(templateText); - const mapping = pointers?.[`/Resources/${escapePointerSegment(logicalId)}`]; - if (mapping === undefined) { + if (pointers === undefined) { return undefined; } - return { start: mapping.value.pos, end: mapping.valueEnd.pos }; + return { + block: (logicalId) => blockRange(pointers, escapePointerSegment(logicalId)), + resource: (logicalId) => { + const escaped = escapePointerSegment(logicalId); + const block = blockRange(pointers, escaped); + return block === undefined ? undefined : { block, properties: propertyRanges(pointers, escaped) }; + }, + }; +} + +/** The value-block range of `/Resources/`, or `undefined` if absent. */ +function blockRange(pointers: Pointers, escapedId: string): OffsetRange | undefined { + const mapping = pointers[`/Resources/${escapedId}`]; + return mapping === undefined ? undefined : { start: mapping.value.pos, end: mapping.valueEnd.pos }; +} + +/** Key+value ranges of each top-level property of `/Resources/`. */ +function propertyRanges(pointers: Pointers, escapedId: string): Record { + const properties: Record = {}; + for (const [pointer, mapping] of Object.entries(pointers)) { + // This resource's top-level properties only. json-source-map types the key + // position as optional (absent for array items); a /Properties/ pointer + // is always an object member, so this narrows the type, not an impossible case. + const match = /^\/Resources\/([^/]+)\/Properties\/([^/]+)$/.exec(pointer); + if (match === null || match[1] !== escapedId || mapping.key === undefined) { + continue; + } + properties[unescapePointerSegment(match[2])] = { start: mapping.key.pos, end: mapping.valueEnd.pos }; + } + return properties; } /** diff --git a/packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts b/packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts index c3ee2b95e..f09bdfdda 100644 --- a/packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts +++ b/packages/@aws-cdk/cloud-assembly-api/test/template-ranges.test.ts @@ -1,4 +1,4 @@ -import { resolveLogicalIdAtOffset, resolveResourceRange } from '../lib'; +import { indexTemplateRanges, resolveLogicalIdAtOffset, resolveResourceRange, resolveResourceRanges } from '../lib'; // A template object is the single source of truth: the text under test is its // 1-space serialization (exactly how synth writes `*.template.json`), and the @@ -111,3 +111,54 @@ describe('resolveLogicalIdAtOffset', () => { expect(resolveLogicalIdAtOffset('not json at all', 3)).toBeUndefined(); }); }); + +describe('resolveResourceRanges', () => { + // Wrapping a key+value slice in braces makes it a valid object, so the same + // re-parse oracle proves each property range covers exactly `"Key": value`. + const sliceParseEntry = (range: { start: number; end: number }) => + JSON.parse('{' + TEMPLATE_TEXT.slice(range.start, range.end) + '}'); + + test.each(LOGICAL_IDS)('the block range for %s re-parses to that exact resource', (logicalId) => { + const { block } = resolveResourceRanges(TEMPLATE_TEXT, logicalId)!; + expect(sliceParse(block)).toEqual(RESOURCES[logicalId]); + }); + + test('each Policy property range re-parses to its exact `"Key": value` entry', () => { + // Policy's values include an Fn::Sub, an InlineJson string with braces and + // escaped quotes, and a Description with a lone brace -- hazards for anything + // but a real parse. + const { properties } = resolveResourceRanges(TEMPLATE_TEXT, 'Policy')!; + const expected = (RESOURCES.Policy as { Properties: Record }).Properties; + expect(Object.keys(properties).sort()).toEqual(Object.keys(expected).sort()); + for (const [name, range] of Object.entries(properties)) { + expect(sliceParseEntry(range)).toEqual({ [name]: expected[name] }); + } + }); + + test('property ranges include the key, not just the value', () => { + const { properties } = resolveResourceRanges(TEMPLATE_TEXT, 'MyBucket')!; + const slice = TEMPLATE_TEXT.slice(properties.BucketName.start, properties.BucketName.end); + expect(slice.startsWith('"BucketName"')).toBe(true); + expect(sliceParseEntry(properties.BucketName)).toEqual({ BucketName: 'plain-bucket' }); + }); + + test('enumerates only top-level properties, not nested keys', () => { + // PolicyDocument is one entry; its nested Statement/Action are not separate. + const { properties } = resolveResourceRanges(TEMPLATE_TEXT, 'Policy')!; + expect(Object.keys(properties).sort()).toEqual(['Description', 'InlineJson', 'PolicyDocument']); + }); + + test('a resource with no Properties yields an empty property map', () => { + const ranges = resolveResourceRanges(TEMPLATE_TEXT, 'Topic')!; + expect(ranges.properties).toEqual({}); + expect(sliceParse(ranges.block)).toEqual(RESOURCES.Topic); + }); + + test('returns undefined for an unknown logical id', () => { + expect(resolveResourceRanges(TEMPLATE_TEXT, 'DoesNotExist')).toBeUndefined(); + }); + + test('returns undefined when the text cannot be parsed into a tree', () => { + expect(resolveResourceRanges('not json at all', 'MyBucket')).toBeUndefined(); + }); +}); diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts index 0e2c681e8..5f591800c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/metadata-schema.ts @@ -360,6 +360,13 @@ export enum ArtifactMetadataEntryType { */ export const CFN_RESOURCE_TYPE_ATTRIBUTE = 'aws:cdk:cloudformation:type'; +/** + * tree.json node attribute carrying a construct's resolved CloudFormation + * resource properties (the synthesized property values). A construct-tree node + * attribute, like {@link CFN_RESOURCE_TYPE_ATTRIBUTE}. + */ +export const CFN_RESOURCE_PROPS_ATTRIBUTE = 'aws:cdk:cloudformation:props'; + /** * A metadata entry in a cloud assembly artifact. */ From a3dca62184aa59dc376407d243f4ddb79d0afe75 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:23:04 -0400 Subject: [PATCH 19/28] feat(cli): add 'cdk lsp' command to start the CDK Language Server over stdio (#1670) Editors and AI agents start a language server by running a command over stdio, but the CDK Language Server (in `@aws-cdk/cdk-explorer`) has no such entry point. This adds one, so any LSP-capable editor or agent can get CDK diagnostics and CodeLens by pointing at `cdk lsp`. Wires up `cdk lsp` as a CLI subcommand that starts the server on stdin/stdout and runs until the client closes the channel. - Adds `@aws-cdk/cdk-explorer` as a runtime dependency so the server ships in the CLI bundle, version-aligned with the user's CDK toolchain. - Writes nothing to stdout (CLI logs already go to stderr), keeping the JSON-RPC channel clean. `applicationDir` comes from the LSP `initialize` options, so the command takes no arguments. - Mirrors the `cdk explore` wiring from #1598 Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 2 + packages/aws-cdk/.projen/deps.json | 4 + packages/aws-cdk/.projen/tasks.json | 2 +- packages/aws-cdk/THIRD_PARTY_LICENSES | 179 ++++++++++++++++++ packages/aws-cdk/lib/cli/cli-config.ts | 3 + .../aws-cdk/lib/cli/cli-type-registry.json | 3 + packages/aws-cdk/lib/cli/cli.ts | 5 + .../aws-cdk/lib/cli/convert-to-user-input.ts | 6 + .../aws-cdk/lib/cli/io-host/cli-io-host.ts | 1 + .../lib/cli/parse-command-line-arguments.ts | 1 + .../aws-cdk/lib/cli/user-configuration.ts | 1 + packages/aws-cdk/lib/cli/user-input.ts | 5 + packages/aws-cdk/lib/commands/lsp.ts | 22 +++ packages/aws-cdk/package.json | 1 + .../aws-cdk/test/cli/cli-arguments.test.ts | 1 + packages/aws-cdk/test/commands/lsp.test.ts | 30 +++ yarn.lock | 3 +- 17 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 packages/aws-cdk/lib/commands/lsp.ts create mode 100644 packages/aws-cdk/test/commands/lsp.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index d387d5fda..ac077f72c 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1747,6 +1747,8 @@ const cdkExplorer = configureProject( fixupTestTask(cdkExplorer); void cdkExplorer; +cli.deps.addDependency('@aws-cdk/cdk-explorer', pj.DependencyType.RUNTIME); + // #endregion ////////////////////////////////////////////////////////////////////// // #region shared setup diff --git a/packages/aws-cdk/.projen/deps.json b/packages/aws-cdk/.projen/deps.json index fe552e010..325ffcced 100644 --- a/packages/aws-cdk/.projen/deps.json +++ b/packages/aws-cdk/.projen/deps.json @@ -197,6 +197,10 @@ "name": "@aws-cdk/cdk-assets-lib", "type": "runtime" }, + { + "name": "@aws-cdk/cdk-explorer", + "type": "runtime" + }, { "name": "@aws-cdk/cloud-assembly-api", "type": "runtime" diff --git a/packages/aws-cdk/.projen/tasks.json b/packages/aws-cdk/.projen/tasks.json index 9743923ba..0b81a3546 100644 --- a/packages/aws-cdk/.projen/tasks.json +++ b/packages/aws-cdk/.projen/tasks.json @@ -55,7 +55,7 @@ }, "steps": [ { - "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/jest,@types/mockery,@types/picomatch,@types/promptly,@types/semver,@types/sinon,@types/yazl,aws-sdk-client-mock,aws-sdk-client-mock-jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,jest-environment-node,jest-mock,license-checker,node-backpack,nx,projen,sinon,ts-jest,ts-mock-imports,ts-node,tsx,cdk-from-cfn,enquirer,fast-glob,picomatch,promptly,proxy-agent,semver,yazl" + "exec": "yarn dlx npm-check-updates@20 --upgrade --target=minor --cooldown=3 --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/jest,@types/mockery,@types/picomatch,@types/promptly,@types/semver,@types/sinon,@types/yazl,aws-sdk-client-mock,aws-sdk-client-mock-jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,fast-check,jest,jest-environment-node,jest-mock,license-checker,node-backpack,nx,projen,sinon,ts-jest,ts-mock-imports,ts-node,tsx,@aws-cdk/cdk-explorer,cdk-from-cfn,enquirer,fast-glob,picomatch,promptly,proxy-agent,semver,yazl" } ] }, diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index de9075f5b..1adb7b453 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -16039,6 +16039,77 @@ Apache License of your accepting any such warranty or additional liability. +---------------- + +** @jridgewell/resolve-uri@3.1.2 - https://www.npmjs.com/package/@jridgewell/resolve-uri/v/3.1.2 | MIT +Copyright 2019 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------- + +** @jridgewell/sourcemap-codec@1.5.5 - https://www.npmjs.com/package/@jridgewell/sourcemap-codec/v/1.5.5 | MIT +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** @jridgewell/trace-mapping@0.3.31 - https://www.npmjs.com/package/@jridgewell/trace-mapping/v/0.3.31 | MIT +Copyright 2024 Justin Ridgewell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** @nodelib/fs.scandir@2.1.5 - https://www.npmjs.com/package/@nodelib/fs.scandir/v/2.1.5 | MIT @@ -28010,6 +28081,34 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** convert-source-map@2.0.0 - https://www.npmjs.com/package/convert-source-map/v/2.0.0 | MIT +Copyright 2013 Thorsten Lorenz. +All rights reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** data-uri-to-buffer@6.0.2 - https://www.npmjs.com/package/data-uri-to-buffer/v/6.0.2 | MIT @@ -29775,6 +29874,86 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** vscode-jsonrpc@8.2.0 - https://www.npmjs.com/package/vscode-jsonrpc/v/8.2.0 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-jsonrpc@8.2.1 - https://www.npmjs.com/package/vscode-jsonrpc/v/8.2.1 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver-protocol@3.17.5 - https://www.npmjs.com/package/vscode-languageserver-protocol/v/3.17.5 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver-types@3.17.5 - https://www.npmjs.com/package/vscode-languageserver-types/v/3.17.5 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vscode-languageserver@9.0.1 - https://www.npmjs.com/package/vscode-languageserver/v/9.0.1 | MIT +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** which-module@2.0.1 - https://www.npmjs.com/package/which-module/v/2.0.1 | ISC diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 581778a4b..161811a49 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -524,6 +524,9 @@ export async function makeConfig(): Promise { 'doctor': { description: 'Check your set-up for potential problems', }, + 'lsp': { + description: 'Start the CDK Language Server (LSP) over stdio for editor and AI-agent integration', + }, 'orphan': { arg: { name: 'PATHS', diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index a348d1b88..d4f767e5c 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -1149,6 +1149,9 @@ "doctor": { "description": "Check your set-up for potential problems" }, + "lsp": { + "description": "Start the CDK Language Server (LSP) over stdio for editor and AI-agent integration" + }, "orphan": { "arg": { "name": "PATHS", diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 20de99422..78aa76b42 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -35,6 +35,7 @@ import { doctor } from '../commands/doctor'; import { FlagCommandHandler } from '../commands/flags/flags'; import { cliInit, printAvailableTemplates } from '../commands/init'; import { getLanguageFromAlias } from '../commands/language'; +import { lsp } from '../commands/lsp'; import { getMigrateScanType } from '../commands/migrate'; import { execProgram, CloudExecutable } from '../cxapp'; import type { StackSelector, Synthesizer } from '../cxapp'; @@ -324,6 +325,10 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { }), ) .command('doctor', 'Check your set-up for potential problems') + .command('lsp', 'Start the CDK Language Server (LSP) over stdio for editor and AI-agent integration') .command('orphan [PATHS..]', 'Detach resources from a CloudFormation stack without deleting them', (yargs: Argv) => yargs) .command('refactor [STACKS..]', 'Moves resources between stacks or within the same stack', (yargs: Argv) => yargs diff --git a/packages/aws-cdk/lib/cli/user-configuration.ts b/packages/aws-cdk/lib/cli/user-configuration.ts index 6092535b3..92738d9d1 100644 --- a/packages/aws-cdk/lib/cli/user-configuration.ts +++ b/packages/aws-cdk/lib/cli/user-configuration.ts @@ -38,6 +38,7 @@ export enum Command { DOCS = 'docs', DOC = 'doc', DOCTOR = 'doctor', + LSP = 'lsp', ORPHAN = 'orphan', REFACTOR = 'refactor', DRIFT = 'drift', diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 6afd6ce4f..d85fc026e 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -144,6 +144,11 @@ export interface UserInput { */ readonly doctor?: {}; + /** + * Start the CDK Language Server (LSP) over stdio for editor and AI-agent integration + */ + readonly lsp?: {}; + /** * Detach resources from a CloudFormation stack without deleting them */ diff --git a/packages/aws-cdk/lib/commands/lsp.ts b/packages/aws-cdk/lib/commands/lsp.ts new file mode 100644 index 000000000..d5e08527e --- /dev/null +++ b/packages/aws-cdk/lib/commands/lsp.ts @@ -0,0 +1,22 @@ +import * as process from 'process'; +import { startServer } from '@aws-cdk/cdk-explorer'; + +/** + * Starts the CDK Language Server over stdio. + * + * The server speaks LSP/JSON-RPC on stdin/stdout, so this command must not + * write anything else to stdout. CDK CLI logs go to stderr by default, which + * keeps the protocol channel clean. The process runs until the LSP client + * closes the stdio channel (stdin end), then exits 0. + */ +export async function lsp(): Promise { + startServer({ readable: process.stdin, writable: process.stdout }); + + await new Promise((resolve) => { + const done = () => resolve(); + process.stdin.once('end', done); + process.stdin.once('close', done); + }); + + return 0; +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 728d8cf49..c07ba0efd 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -84,6 +84,7 @@ }, "dependencies": { "@aws-cdk/cdk-assets-lib": "^0.0.0", + "@aws-cdk/cdk-explorer": "^0.0.0", "@aws-cdk/cloud-assembly-api": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", diff --git a/packages/aws-cdk/test/cli/cli-arguments.test.ts b/packages/aws-cdk/test/cli/cli-arguments.test.ts index 01ce8dc01..17d3e9b49 100644 --- a/packages/aws-cdk/test/cli/cli-arguments.test.ts +++ b/packages/aws-cdk/test/cli/cli-arguments.test.ts @@ -185,6 +185,7 @@ describe('config', () => { flags: expect.anything(), doctor: expect.anything(), docs: expect.anything(), + lsp: expect.anything(), orphan: expect.anything(), refactor: expect.anything(), cliTelemetry: expect.anything(), diff --git a/packages/aws-cdk/test/commands/lsp.test.ts b/packages/aws-cdk/test/commands/lsp.test.ts new file mode 100644 index 000000000..51237094f --- /dev/null +++ b/packages/aws-cdk/test/commands/lsp.test.ts @@ -0,0 +1,30 @@ +import * as cdkExplorer from '@aws-cdk/cdk-explorer'; +import { lsp } from '../../lib/commands/lsp'; + +jest.mock('@aws-cdk/cdk-explorer', () => ({ + startServer: jest.fn(), +})); + +describe('lsp command', () => { + const startServer = cdkExplorer.startServer as jest.Mock; + + afterEach(() => { + startServer.mockClear(); + }); + + test('starts the language server on stdio and exits 0 when stdin closes', async () => { + const resultPromise = lsp(); + + // The server is started over the process stdio streams (the LSP transport). + expect(startServer).toHaveBeenCalledTimes(1); + expect(startServer).toHaveBeenCalledWith({ + readable: process.stdin, + writable: process.stdout, + }); + + // Simulate the LSP client closing the stdio channel. + process.stdin.emit('end'); + + await expect(resultPromise).resolves.toBe(0); + }); +}); diff --git a/yarn.lock b/yarn.lock index bfe97139d..a7ffad63b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -159,7 +159,7 @@ __metadata: languageName: unknown linkType: soft -"@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer": +"@aws-cdk/cdk-explorer@npm:^0.0.0, @aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer": version: 0.0.0-use.local resolution: "@aws-cdk/cdk-explorer@workspace:packages/@aws-cdk/cdk-explorer" dependencies: @@ -8169,6 +8169,7 @@ __metadata: resolution: "aws-cdk@workspace:packages/aws-cdk" dependencies: "@aws-cdk/cdk-assets-lib": "npm:^0.0.0" + "@aws-cdk/cdk-explorer": "npm:^0.0.0" "@aws-cdk/cli-plugin-contract": "npm:^0.0.0" "@aws-cdk/cloud-assembly-api": "npm:^0.0.0" "@aws-cdk/cloud-assembly-schema": "npm:^0.0.0" From 13c5bce1250858e1204fd4a68abd41032428d101 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:14:13 -0400 Subject: [PATCH 20/28] fix(cli): wire 'cdk lsp' through a complete startLspServer entrypoint (#1711) startServer now requires toolkitBindingsFactory (added in #1634/#1669), but the cdk lsp command still called it with only { readable, writable }, breaking the build. Expose startLspServer() from cdk-explorer (wires the Toolkit bindings and starts the server) and call it from the CLI. main.ts becomes that exported function, making cdk lsp the single LSP entrypoint. Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- packages/@aws-cdk/cdk-explorer/lib/index.ts | 1 + packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts | 11 ++++++----- packages/aws-cdk/lib/commands/lsp.ts | 4 ++-- packages/aws-cdk/test/commands/lsp.test.ts | 14 +++++--------- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/cdk-explorer/lib/index.ts b/packages/@aws-cdk/cdk-explorer/lib/index.ts index c0fd2de4e..e9aa0fdb8 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/index.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/index.ts @@ -4,5 +4,6 @@ export { readAssembly } from './core/assembly-reader'; export type { AssemblyData, AssemblyReadResult, ConstructNode } from './core/assembly-reader'; export type { SourceLocation } from './core/source-resolver'; +export { startLspServer } from './lsp/main'; export { startServer, createLspHandlers } from './lsp/server'; export type { LspHandlers, LspHandlerOptions, LspServerOptions } from './lsp/server'; diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts index 097be72b2..262030761 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts @@ -3,7 +3,12 @@ import { LspIoHost } from './io-host'; import { startServer } from './server'; import { runSynth } from '../core/synth-runner'; -try { +/** + * Starts the CDK Language Server over the process's stdin/stdout. Wires the + * Toolkit-backed bindings the handlers need. This is the single entrypoint + * used by the `cdk lsp` CLI command. + */ +export function startLspServer(): void { startServer({ readable: process.stdin, writable: process.stdout, @@ -22,8 +27,4 @@ try { }; }, }); -} catch (err) { - const e = err as Error; - process.stderr.write(`CDK LSP startup fatal: ${e.stack ?? e.message}\n`); - process.exit(1); } diff --git a/packages/aws-cdk/lib/commands/lsp.ts b/packages/aws-cdk/lib/commands/lsp.ts index d5e08527e..fa9d7b217 100644 --- a/packages/aws-cdk/lib/commands/lsp.ts +++ b/packages/aws-cdk/lib/commands/lsp.ts @@ -1,5 +1,5 @@ import * as process from 'process'; -import { startServer } from '@aws-cdk/cdk-explorer'; +import { startLspServer } from '@aws-cdk/cdk-explorer'; /** * Starts the CDK Language Server over stdio. @@ -10,7 +10,7 @@ import { startServer } from '@aws-cdk/cdk-explorer'; * closes the stdio channel (stdin end), then exits 0. */ export async function lsp(): Promise { - startServer({ readable: process.stdin, writable: process.stdout }); + startLspServer(); await new Promise((resolve) => { const done = () => resolve(); diff --git a/packages/aws-cdk/test/commands/lsp.test.ts b/packages/aws-cdk/test/commands/lsp.test.ts index 51237094f..a6a55a4ad 100644 --- a/packages/aws-cdk/test/commands/lsp.test.ts +++ b/packages/aws-cdk/test/commands/lsp.test.ts @@ -2,25 +2,21 @@ import * as cdkExplorer from '@aws-cdk/cdk-explorer'; import { lsp } from '../../lib/commands/lsp'; jest.mock('@aws-cdk/cdk-explorer', () => ({ - startServer: jest.fn(), + startLspServer: jest.fn(), })); describe('lsp command', () => { - const startServer = cdkExplorer.startServer as jest.Mock; + const startLspServer = cdkExplorer.startLspServer as jest.Mock; afterEach(() => { - startServer.mockClear(); + startLspServer.mockClear(); }); test('starts the language server on stdio and exits 0 when stdin closes', async () => { const resultPromise = lsp(); - // The server is started over the process stdio streams (the LSP transport). - expect(startServer).toHaveBeenCalledTimes(1); - expect(startServer).toHaveBeenCalledWith({ - readable: process.stdin, - writable: process.stdout, - }); + // The command delegates to the fully-wired stdio entrypoint. + expect(startLspServer).toHaveBeenCalledTimes(1); // Simulate the LSP client closing the stdio channel. process.stdin.emit('end'); From e26222a53d3c8120a353275a4d8f1f0aca637a69 Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:55:50 -0400 Subject: [PATCH 21/28] feat: explorer web live refresh (#1698) Adds live refresh to the cdk explore web UI. When the cloud assembly on disk changes, the server pushes an event and the SPA re-fetches, so the construct tree and violations stay current without a manual reload. - SSE stream (GET /api/events): a long-lived server-sent events channel. SseBroadcaster tracks connected clients and pushes to all of them, evicting any that disconnect. - cdk.out watcher: same watcher as LSP - SPA: subscribes on load and re-fetches the tree and violations on each event. - The event carries no payload. The server holds no assembly state, so the client just re-reads /api/tree and /api/policy-validation on receipt. Also narrows the web routes' injectable readAssembly seam to Promise to match the real reader. ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .../@aws-cdk/cdk-explorer/frontend/App.tsx | 9 +- .../@aws-cdk/cdk-explorer/frontend/api.ts | 29 +++-- .../cdk-explorer/lib/core/assembly-watcher.ts | 6 +- .../@aws-cdk/cdk-explorer/lib/web/events.ts | 55 +++++++++ .../@aws-cdk/cdk-explorer/lib/web/protocol.ts | 12 +- .../@aws-cdk/cdk-explorer/lib/web/routes.ts | 2 +- .../@aws-cdk/cdk-explorer/lib/web/server.ts | 60 ++++++++-- .../test/core/assembly-watcher.test.ts | 4 +- .../cdk-explorer/test/web/events.test.ts | 106 ++++++++++++++++++ .../cdk-explorer/test/web/routes.test.ts | 20 ++-- .../cdk-explorer/test/web/server.test.ts | 52 ++++++++- packages/aws-cdk/lib/commands/explore.ts | 7 +- .../aws-cdk/test/commands/explore.test.ts | 2 +- 13 files changed, 321 insertions(+), 43 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/events.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/events.test.ts diff --git a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx index d4511a73e..e2618675b 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx @@ -16,7 +16,7 @@ export function App(): JSX.Element { const [error, setError] = React.useState(); const [appDir, setAppDir] = React.useState(); - React.useEffect(() => { + const reload = React.useCallback((): void => { Promise.all([api.getTree(), api.getViolations(), api.getAppInfo()]) .then(([t, v, info]) => { setTree(t); @@ -24,9 +24,16 @@ export function App(): JSX.Element { setAppDir(info.appDir); setError(undefined); }) + // Keep the last good render on a transient read (e.g. a mid-synth write); + // the next assembly-changed event re-fetches once the write has settled. .catch((err) => setError(err instanceof Error ? err.message : String(err))); }, []); + React.useEffect(() => { + reload(); + return api.subscribe(reload); + }, [reload]); + // Vertical split: violations row's share of the page height (default 33%). const vSplit = useSplit({ orientation: 'vertical', defaultFraction: 0.33, min: 0.15, max: 0.85 }); // Horizontal split inside the top row: file panes' share of that row's width (default 75%; tree gets the remaining 25%). diff --git a/packages/@aws-cdk/cdk-explorer/frontend/api.ts b/packages/@aws-cdk/cdk-explorer/frontend/api.ts index f23f22b3d..145274a03 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/api.ts +++ b/packages/@aws-cdk/cdk-explorer/frontend/api.ts @@ -1,12 +1,13 @@ -import type { - DirEntry, - FilesResponse, - FileResponse, - TreeResponse, - ViolationsResponse, - WebConstructNode, - WebViolation, - WebViolationOccurrence, +import { + ASSEMBLY_CHANGED, + type DirEntry, + type FilesResponse, + type FileResponse, + type TreeResponse, + type ViolationsResponse, + type WebConstructNode, + type WebViolation, + type WebViolationOccurrence, } from '../lib/web/protocol'; export type { @@ -39,4 +40,14 @@ export const api = { getTree: (): Promise => getJson('/api/tree'), getViolations: (): Promise => getJson('/api/policy-validation'), getAppInfo: (): Promise => getJson('/api/info'), + /** + * Subscribe to assembly-changed events from the server, invoking `onChange` + * whenever the cloud assembly is rewritten. Returns an unsubscribe that closes + * the underlying EventSource. + */ + subscribe: (onChange: () => void): (() => void) => { + const source = new EventSource('/api/events'); + source.addEventListener(ASSEMBLY_CHANGED, () => onChange()); + return () => source.close(); + }, }; diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts index ce065f21e..4b494f97b 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts @@ -52,7 +52,7 @@ export interface AssemblyWatcherOptions { /** Invoked (debounced) when the assembly's signal files change. */ readonly onChange: () => void; /** Receives non-fatal watcher errors. */ - readonly onError?: (error: unknown) => void; + readonly onError: (error: unknown) => void; /** * Factory for the underlying file watcher. Defaults to chokidar; overridden * in tests with a fake so behavior is verified without real file IO. @@ -101,13 +101,13 @@ export function startAssemblyWatcher(options: AssemblyWatcherOptions): AssemblyW try { options.onChange(); } catch (error) { - options.onError?.(error); + options.onError(error); } }, DEBOUNCE_MS); }); watcher.on('error', (error) => { - options.onError?.(error); + options.onError(error); }); return { diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/events.ts b/packages/@aws-cdk/cdk-explorer/lib/web/events.ts new file mode 100644 index 000000000..46355b4cd --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/events.ts @@ -0,0 +1,55 @@ +import type { Request, Response } from 'express'; +import type { SseEventName } from './protocol'; + +/** + * Tracks connected Server-Sent Events clients and pushes events to all of them. + * One instance lives per web server. `close()` ends every open stream on + * shutdown so the HTTP server can stop cleanly. + */ +export class SseBroadcaster { + private readonly clients = new Set(); + + /** + * Express handler for `GET /api/events`. Opens a long-lived SSE stream and + * registers the client, removing it when the request closes or the socket + * errors so a vanished client is never written to. + */ + public handle(req: Request, res: Response): void { + res.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-store', + 'Connection': 'keep-alive', + }); + res.flushHeaders(); + this.clients.add(res); + + const remove = (): void => { + this.clients.delete(res); + }; + req.on('close', remove); + // Evict on socket error too, so a vanished client is never written to. + res.on('error', remove); + } + + /** + * Push an event to every connected client. The `data: {}` line is required: + * EventSource does not dispatch a named event whose data buffer is empty, so + * the empty payload is what makes the client's listener fire. Writing to a + * client that already disconnected is a harmless no-op (returns false, does + * not throw); the `close`/`error` handlers in `handle` do the eviction. + */ + public broadcast(event: SseEventName): void { + const frame = `event: ${event}\ndata: {}\n\n`; + for (const client of this.clients) { + client.write(frame); + } + } + + /** End every open stream and forget the clients. Called on server shutdown. */ + public close(): void { + for (const client of this.clients) { + client.end(); + } + this.clients.clear(); + } +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts index c41cacd54..81be6a4fa 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts @@ -1,4 +1,14 @@ -/** HTTP contract shared between the web server and the SPA. Types only. */ +/** HTTP and SSE contract shared between the web server and the SPA. */ + +/** + * SSE event name the server sends (and the SPA listens for) when the cloud + * assembly is rewritten. It carries no meaningful payload: the server holds no + * assembly state, so the SPA re-fetches the tree and violations on receipt. + */ +export const ASSEMBLY_CHANGED = 'assembly-changed'; + +/** The SSE event names the server may send. One for now. */ +export type SseEventName = typeof ASSEMBLY_CHANGED; export interface DirEntry { readonly name: string; diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts index 4fd6685ca..8f6d54a13 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts @@ -26,7 +26,7 @@ export interface ApiOptions { * Reader for the cloud assembly. Injectable for tests; defaults to the real * `readAssembly` against {@link assemblyDir}. */ - readonly readAssembly?: (assemblyDir: string) => AssemblyReadResult | Promise; + readonly readAssembly?: (assemblyDir: string) => Promise; } export function createApiRouter(options: ApiOptions): Router { diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts index 6fd2b606d..3627224e8 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts @@ -1,15 +1,23 @@ import * as http from 'http'; +import * as path from 'path'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); +import { SseBroadcaster } from './events'; +import { ASSEMBLY_CHANGED } from './protocol'; import { registerApi } from './routes'; import { indexHtml, webAsset } from './web-assets'; +import { + startAssemblyWatcher as defaultStartAssemblyWatcher, + type AssemblyWatcher, + type AssemblyWatcherOptions, +} from '../core/assembly-watcher'; export const DEFAULT_PORT = 4200; const MAX_PORT_ATTEMPTS = 100; +const HOST = 'localhost'; export interface WebServerOptions { readonly port?: number; - readonly host?: string; /** * Root of the CDK app. File listing/reading is confined here. Defaults to * `process.cwd()`. @@ -20,6 +28,16 @@ export interface WebServerOptions { * Defaults to `/cdk.out`. */ readonly assemblyDir?: string; + /** + * Starts the cdk.out watcher. Defaults to the real chokidar-backed watcher; + * overridden in tests with a fake to drive change events deterministically. + */ + readonly startAssemblyWatcher?: (options: AssemblyWatcherOptions) => AssemblyWatcher; + /** + * Reports a non-fatal watcher error (live refresh stops updating). Defaults to + * writing to stderr; the CLI command passes a sink that routes to its IoHost. + */ + readonly onWatcherError?: (err: unknown) => void; } export interface WebServer { @@ -36,12 +54,19 @@ export interface WebServer { * @returns A handle to the running server with its URL and a stop function. */ export async function startWebServer(options: WebServerOptions = {}): Promise { - const host = options.host ?? '127.0.0.1'; const appDir = options.appDir ?? process.cwd(); + // Single owner of where the cloud assembly lives: the same resolved path feeds + // both the read endpoints and the change watcher, so the two never disagree. + const assemblyDir = options.assemblyDir ?? path.join(appDir, 'cdk.out'); const app = express(); - registerApi(app, { appDir, assemblyDir: options.assemblyDir }); + registerApi(app, { appDir, assemblyDir }); + + // Live-refresh stream: browsers subscribe here and re-fetch when the assembly + // changes. Registered before the /api catch-all so it is not treated as unknown. + const events = new SseBroadcaster(); + app.get('/api/events', events.handle.bind(events)); // Unknown /api routes must return JSON 404, not fall through to the SPA. app.use('/api', (_req, res) => res.status(404).json({ error: 'unknown endpoint' })); @@ -65,16 +90,29 @@ export async function startWebServer(options: WebServerOptions = {}): Promise events.broadcast(ASSEMBLY_CHANGED), + onError: options.onWatcherError ?? ((err) => + process.stderr.write(`assembly watcher error: ${err instanceof Error ? err.message : String(err)}\n`)), + }); let stopped = false; return { - url: `http://${host}:${port}`, - stop: () => { - if (stopped) return Promise.resolve(); + url: `http://${HOST}:${port}`, + stop: async () => { + if (stopped) return; stopped = true; - return new Promise((resolve) => { + await watcher.close(); + events.close(); + await new Promise((resolve) => { server.close(() => resolve()); server.closeAllConnections(); }); @@ -84,8 +122,8 @@ export async function startWebServer(options: WebServerOptions = {}): Promise { await new Promise((resolve, reject) => { server.once('error', reject); @@ -99,8 +137,8 @@ async function listenOnPort( async function listenWithPortSearch( server: http.Server, - host: string, startPort: number, + host: string, ): Promise { for (let port = startPort; port < startPort + MAX_PORT_ATTEMPTS; port++) { try { diff --git a/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts index 89b414f54..2896b9ad7 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts @@ -36,12 +36,14 @@ class FakeWatcher implements FileWatcher { function setup() { const fake = new FakeWatcher(); const onChange = jest.fn(); + const onError = jest.fn(); const watcher = startAssemblyWatcher({ assemblyDir: '/p/cdk.out', onChange, + onError, createWatcher: () => fake, }); - return { fake, onChange, watcher }; + return { fake, onChange, onError, watcher }; } describe('Assembly Watcher', () => { diff --git a/packages/@aws-cdk/cdk-explorer/test/web/events.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/events.test.ts new file mode 100644 index 000000000..869a07f34 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/events.test.ts @@ -0,0 +1,106 @@ +import type { Request, Response } from 'express'; +import { SseBroadcaster } from '../../lib/web/events'; +import { ASSEMBLY_CHANGED } from '../../lib/web/protocol'; + +/** + * Minimal Request/Response doubles that capture writes and expose the close and + * error handlers the broadcaster registers, so behavior is verified without a + * real socket. + */ +function fakeClient() { + const writes: string[] = []; + let ended = false; + const reqHandlers: Record void> = {}; + const resHandlers: Record void> = {}; + + const req = { + on: (event: string, handler: () => void) => { + reqHandlers[event] = handler; + return req; + }, + } as unknown as Request; + + const res = { + set: () => res, + flushHeaders: () => undefined, + write: (chunk: string) => { + writes.push(chunk); + return true; + }, + end: () => { + ended = true; + }, + on: (event: string, handler: (err?: unknown) => void) => { + resHandlers[event] = handler; + return res; + }, + } as unknown as Response; + + return { + req, + res, + writes, + isEnded: () => ended, + disconnect: () => reqHandlers.close?.(), + fail: () => resHandlers.error?.(new Error('broken pipe')), + }; +} + +const FRAME = `event: ${ASSEMBLY_CHANGED}\ndata: {}\n\n`; + +describe('SseBroadcaster', () => { + test('broadcasts a named, payload-free frame to every connected client', () => { + const broadcaster = new SseBroadcaster(); + const a = fakeClient(); + const b = fakeClient(); + broadcaster.handle(a.req, a.res); + broadcaster.handle(b.req, b.res); + + broadcaster.broadcast(ASSEMBLY_CHANGED); + + expect(a.writes).toEqual([FRAME]); + expect(b.writes).toEqual([FRAME]); + }); + + test('stops writing to a client after it disconnects', () => { + const broadcaster = new SseBroadcaster(); + const gone = fakeClient(); + const live = fakeClient(); + broadcaster.handle(gone.req, gone.res); + broadcaster.handle(live.req, live.res); + + gone.disconnect(); + broadcaster.broadcast(ASSEMBLY_CHANGED); + + expect(gone.writes).toEqual([]); + expect(live.writes).toEqual([FRAME]); + }); + + test('drops a client whose socket errors so a broken pipe is not written again', () => { + const broadcaster = new SseBroadcaster(); + const a = fakeClient(); + broadcaster.handle(a.req, a.res); + + a.fail(); + broadcaster.broadcast(ASSEMBLY_CHANGED); + + expect(a.writes).toEqual([]); + }); + + test('close ends every open stream and reaches no one afterwards', () => { + const broadcaster = new SseBroadcaster(); + const a = fakeClient(); + const b = fakeClient(); + broadcaster.handle(a.req, a.res); + broadcaster.handle(b.req, b.res); + + broadcaster.close(); + + expect(a.isEnded()).toBe(true); + expect(b.isEnded()).toBe(true); + + broadcaster.broadcast(ASSEMBLY_CHANGED); + expect(a.writes).toEqual([]); + expect(b.writes).toEqual([]); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts index c35d00a94..944126812 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts @@ -117,7 +117,7 @@ describe('symlink containment', () => { describe('GET /api/tree', () => { /** Build an app whose /api router uses an injected assembly reader. */ function appWith( - readAssembly: (dir: string) => AssemblyReadResult, + readAssembly: (dir: string) => Promise, opts: { assemblyDir?: string } = {}, ): express.Express { const a = express(); @@ -127,7 +127,7 @@ describe('GET /api/tree', () => { test('maps a successful read into an ok TreeResponse with relativized paths', async () => { const realAppDir = fs.realpathSync(appDir); - const reader = (dir: string): AssemblyReadResult => { + const reader = async (dir: string): Promise => { const node: ConstructNode = { path: 'MyStack/Bucket', id: 'Bucket', @@ -155,20 +155,20 @@ describe('GET /api/tree', () => { }); test('returns not-synthesized (200) when no assembly is found', async () => { - const res = await request(appWith(() => ({ status: 'not-found' }))).get('/api/tree'); + const res = await request(appWith(async () => ({ status: 'not-found' }))).get('/api/tree'); expect(res.status).toBe(200); expect(res.body).toEqual({ status: 'not-synthesized' }); }); test('returns 500 when the read errors', async () => { - const res = await request(appWith(() => ({ status: 'error', message: 'bad manifest' }))).get('/api/tree'); + const res = await request(appWith(async () => ({ status: 'error', message: 'bad manifest' }))).get('/api/tree'); expect(res.status).toBe(500); expect(res.body).toEqual({ error: 'bad manifest' }); }); test('defaults the assembly dir to /cdk.out', async () => { let seen: string | undefined; - const reader = (dir: string): AssemblyReadResult => { + const reader = async (dir: string): Promise => { seen = dir; return { status: 'not-found' }; }; @@ -178,7 +178,7 @@ describe('GET /api/tree', () => { test('honors an explicit assemblyDir override', async () => { let seen: string | undefined; - const reader = (dir: string): AssemblyReadResult => { + const reader = async (dir: string): Promise => { seen = dir; return { status: 'not-found' }; }; @@ -188,7 +188,7 @@ describe('GET /api/tree', () => { }); describe('GET /api/policy-validation', () => { - function appWith(readAssembly: (dir: string) => AssemblyReadResult): express.Express { + function appWith(readAssembly: (dir: string) => Promise): express.Express { const a = express(); a.use('/api', createApiRouter({ appDir, readAssembly })); return a; @@ -196,7 +196,7 @@ describe('GET /api/policy-validation', () => { test('normalizes violations joined to the construct tree', async () => { const realAppDir = fs.realpathSync(appDir); - const reader = (dir: string): AssemblyReadResult => { + const reader = async (dir: string): Promise => { const node: ConstructNode = { path: 'MyStack/Bucket', id: 'Bucket', @@ -244,13 +244,13 @@ describe('GET /api/policy-validation', () => { }); test('returns not-synthesized (200) when no assembly is found', async () => { - const res = await request(appWith(() => ({ status: 'not-found' }))).get('/api/policy-validation'); + const res = await request(appWith(async () => ({ status: 'not-found' }))).get('/api/policy-validation'); expect(res.status).toBe(200); expect(res.body).toEqual({ status: 'not-synthesized' }); }); test('returns 500 when the read errors', async () => { - const res = await request(appWith(() => ({ status: 'error', message: 'boom' }))).get('/api/policy-validation'); + const res = await request(appWith(async () => ({ status: 'error', message: 'boom' }))).get('/api/policy-validation'); expect(res.status).toBe(500); expect(res.body).toEqual({ error: 'boom' }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts index 0dc76d138..ff0a46095 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts @@ -1,3 +1,4 @@ +import { ASSEMBLY_CHANGED } from '../../lib/web/protocol'; import { startWebServer, DEFAULT_PORT, type WebServer } from '../../lib/web/server'; describe('Web Server', () => { @@ -19,17 +20,17 @@ describe('Web Server', () => { expect(body).toEqual({ status: 'ok' }); }); - test('binds to 127.0.0.1 by default', async () => { + test('binds to localhost by default', async () => { server = await startWebServer(); - expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); + expect(server.url).toMatch(/^http:\/\/localhost:\d+$/); }); test('auto-increments port by 1 when default is taken', async () => { const first = await startWebServer({ port: DEFAULT_PORT }); server = await startWebServer(); - expect(first.url).toBe(`http://127.0.0.1:${DEFAULT_PORT}`); - expect(server.url).toBe(`http://127.0.0.1:${DEFAULT_PORT + 1}`); + expect(first.url).toBe(`http://localhost:${DEFAULT_PORT}`); + expect(server.url).toBe(`http://localhost:${DEFAULT_PORT + 1}`); await first.stop(); }); @@ -71,4 +72,47 @@ describe('Web Server', () => { expect(res.status).toBe(200); expect(res.headers.get('cache-control')).toBe('no-store'); }); + + test('watches the resolved assembly dir and closes the watcher on stop', async () => { + let seenDir: string | undefined; + let closed = false; + server = await startWebServer({ + assemblyDir: '/tmp/explorer-test/cdk.out', + startAssemblyWatcher: (opts) => { + seenDir = opts.assemblyDir; + return { + close: async () => { + closed = true; + }, + }; + }, + }); + + expect(seenDir).toBe('/tmp/explorer-test/cdk.out'); + + await server.stop(); + expect(closed).toBe(true); + }); + + test('broadcasts an assembly-changed event to a connected client when the watcher fires', async () => { + let fireChange = (): void => undefined; + server = await startWebServer({ + startAssemblyWatcher: (opts) => { + fireChange = opts.onChange; + return { close: async () => undefined }; + }, + }); + + const res = await fetch(`${server.url}/api/events`); + expect(res.headers.get('content-type')).toMatch(/text\/event-stream/); + const body = res.body; + if (!body) throw new Error('SSE response had no body'); + const reader = body.getReader(); + + fireChange(); + const { value } = await reader.read(); + expect(new TextDecoder().decode(value)).toContain(`event: ${ASSEMBLY_CHANGED}`); + + await reader.cancel(); + }); }); diff --git a/packages/aws-cdk/lib/commands/explore.ts b/packages/aws-cdk/lib/commands/explore.ts index 5dc63f616..50e1e7154 100644 --- a/packages/aws-cdk/lib/commands/explore.ts +++ b/packages/aws-cdk/lib/commands/explore.ts @@ -7,7 +7,12 @@ export interface ExploreOptions { } export async function explore(options: ExploreOptions): Promise { - const server = await startWebServer({ port: options.port }); + const server = await startWebServer({ + port: options.port, + onWatcherError: (err) => void options.ioHelper.defaults.error( + `CDK Explorer live refresh stopped: ${err instanceof Error ? err.message : String(err)}`, + ), + }); await options.ioHelper.defaults.info(`CDK Explorer running at ${server.url}`); await new Promise((resolve) => { diff --git a/packages/aws-cdk/test/commands/explore.test.ts b/packages/aws-cdk/test/commands/explore.test.ts index 37eb43f80..5e3eb714d 100644 --- a/packages/aws-cdk/test/commands/explore.test.ts +++ b/packages/aws-cdk/test/commands/explore.test.ts @@ -22,6 +22,6 @@ describe('explore command', () => { expect(exitCode).toBe(0); expect(messages).toHaveLength(1); - expect(messages[0]).toMatch(/CDK Explorer running at http:\/\/127\.0\.0\.1:\d+/); + expect(messages[0]).toMatch(/CDK Explorer running at http:\/\/localhost:\d+/); }); }); From a0b691bfeb0dbe223dcd2c10fa340fa40714a53f Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:21:19 -0400 Subject: [PATCH 22/28] chore: extract assembly read-lock factory into core (#1715) Matching LSP side of #1706. Moves the assembly read-lock acquirer out of lib/lsp into lib/core/assembly-lock.ts so the LSP and web server build the read lock from one shared factory. Pure refactor, LSP behavior is unchanged, and server.ts re-exports AssemblyLock so existing importers keep resolving it. Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .../cdk-explorer/lib/core/assembly-lock.ts | 25 +++++++++++++++++++ .../@aws-cdk/cdk-explorer/lib/lsp/main.ts | 7 ++---- .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 9 +------ .../cdk-explorer/test/lsp/server.test.ts | 3 ++- 4 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/core/assembly-lock.ts diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-lock.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-lock.ts new file mode 100644 index 000000000..c04467136 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-lock.ts @@ -0,0 +1,25 @@ +import type { Toolkit } from '@aws-cdk/toolkit-lib'; + +/** + * A held read lock on the cloud assembly directory; `release()` unlocks it. + * Wraps the Toolkit's `fromAssemblyDirectory().produce()` readable. + */ +export interface AssemblyLock { + release(): Promise; +} + +/** Acquire a read lock on the assembly dir; throws a `LockError` on writer contention. */ +export type AcquireAssemblyLock = (assemblyDir: string) => Promise; + +/** + * Builds an assembly read-lock acquirer from a Toolkit. The read lock is the + * Toolkit's own `fromAssemblyDirectory().produce()` lease, so callers never + * touch `RWLock` directly; `release()` disposes the lease. + */ +export function toolkitAssemblyLock(toolkit: Toolkit): AcquireAssemblyLock { + return async (assemblyDir) => { + const cx = await toolkit.fromAssemblyDirectory(assemblyDir, { failOnMissingContext: false }); + const readable = await cx.produce(); + return { release: () => readable.dispose() }; + }; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts index 262030761..4bd38ddaa 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/main.ts @@ -1,6 +1,7 @@ import { Toolkit } from '@aws-cdk/toolkit-lib'; import { LspIoHost } from './io-host'; import { startServer } from './server'; +import { toolkitAssemblyLock } from '../core/assembly-lock'; import { runSynth } from '../core/synth-runner'; /** @@ -19,11 +20,7 @@ export function startLspServer(): void { const toolkit = new Toolkit({ ioHost: new LspIoHost(console) }); return { synthRunner: (projectDir) => runSynth({ toolkit, projectDir }), - acquireAssemblyLock: async (assemblyDir) => { - const cx = await toolkit.fromAssemblyDirectory(assemblyDir, { failOnMissingContext: false }); - const readable = await cx.produce(); - return { release: () => readable.dispose() }; - }, + acquireAssemblyLock: toolkitAssemblyLock(toolkit), }; }, }); diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 7b2f7f8be..96888a196 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -35,6 +35,7 @@ import { synthFailureDiagnostics } from './synth-diagnostics'; import { sourceTargetAtTemplateOffset } from './template-locator'; import { WATCH_EXCLUDE_DEFAULTS } from '../../../toolkit-lib/lib/actions/watch/private/helpers'; import { createIgnoreMatcher } from '../../../toolkit-lib/lib/util/glob-matcher'; +import type { AssemblyLock } from '../core/assembly-lock'; import { readAssembly as defaultReadAssembly, type AssemblyReadResult, @@ -57,14 +58,6 @@ import type { SynthRunResult } from '../core/synth-runner'; const REFRESH_LOCK_RETRIES = 10; const REFRESH_LOCK_RETRY_MS = 50; -/** - * A held read lock on the cloud assembly directory; `release()` unlocks it. - * In production this wraps the Toolkit's `fromAssemblyDirectory().produce()` readable. - */ -export interface AssemblyLock { - release(): Promise; -} - export interface LspHandlerOptions { /** Override readAssembly for tests. Defaults to reading /cdk.out. */ readonly readAssembly?: (assemblyDir: string) => Promise; diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index 1ab055343..1d67b8b05 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -6,9 +6,10 @@ import { LockError } from '@aws-cdk/toolkit-lib'; import type { Diagnostic, InitializeParams } from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; import type { AssemblyReadResult } from '../../lib'; +import type { AssemblyLock } from '../../lib/core/assembly-lock'; import type { SynthRunResult } from '../../lib/core/synth-runner'; import { COMMAND_SYNTH_NOW, type NotifySink } from '../../lib/lsp/commands'; -import { createLspHandlers, type AssemblyLock, type LspHandlerOptions, type LspHandlers } from '../../lib/lsp/server'; +import { createLspHandlers, type LspHandlerOptions, type LspHandlers } from '../../lib/lsp/server'; function makeNotifySink(): NotifySink & { infoMessages: string[]; errorMessages: string[] } { const infoMessages: string[] = []; From 52a3635f945ef49a7edc6132b7330e58e8090cfe Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:05:36 -0400 Subject: [PATCH 23/28] fix: validate template paths and document the LSP server (#1716) - onDefinition now bounds the client-supplied *.template.json path to the project's cdk.out (realpath on both sides) before reading, matching the file-read discipline used elsewhere in the package. Prevents reading arbitrary files via a crafted textDocument/definition request. - Replace the placeholder README with real content: purpose, how to launch (cdk lsp / startLspServer), features, a security note that auto-synth and 'Synth now' run the cdk.json app command with your credentials on save (enable only on trusted projects), and editor-integration notes. - Remove the unused express / @types/express dependencies; the web interface is not part of this branch. Fixes # ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 2 - .../@aws-cdk/cdk-explorer/.projen/deps.json | 10 - packages/@aws-cdk/cdk-explorer/README.md | 83 +++++++- .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 5 + packages/@aws-cdk/cdk-explorer/package.json | 2 - .../cdk-explorer/test/lsp/server.test.ts | 21 +- .../@aws-cdk/cdk-explorer/tsconfig.dev.json | 1 - packages/@aws-cdk/cdk-explorer/tsconfig.json | 1 - yarn.lock | 187 +----------------- 9 files changed, 108 insertions(+), 204 deletions(-) diff --git a/.projenrc.ts b/.projenrc.ts index ac077f72c..d06bd1646 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1717,14 +1717,12 @@ const cdkExplorer = configureProject( 'vscode-languageserver@^9', 'vscode-languageserver-textdocument@^1', 'vscode-jsonrpc@^8', - 'express@^4', 'chokidar@^4', '@jridgewell/trace-mapping@^0.3', 'convert-source-map@^2', ], devDeps: [ 'vscode-languageserver-protocol@^3', - '@types/express@^4', '@types/convert-source-map@^2', ], tsconfig: { diff --git a/packages/@aws-cdk/cdk-explorer/.projen/deps.json b/packages/@aws-cdk/cdk-explorer/.projen/deps.json index 0bed08be3..c2152ce92 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/deps.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/deps.json @@ -14,11 +14,6 @@ "version": "^2", "type": "build" }, - { - "name": "@types/express", - "version": "^4", - "type": "build" - }, { "name": "@types/jest", "type": "build" @@ -135,11 +130,6 @@ "version": "^2", "type": "runtime" }, - { - "name": "express", - "version": "^4", - "type": "runtime" - }, { "name": "vscode-jsonrpc", "version": "^8", diff --git a/packages/@aws-cdk/cdk-explorer/README.md b/packages/@aws-cdk/cdk-explorer/README.md index b3fa7ddcd..208158edd 100644 --- a/packages/@aws-cdk/cdk-explorer/README.md +++ b/packages/@aws-cdk/cdk-explorer/README.md @@ -1 +1,82 @@ -# replace this \ No newline at end of file +# @aws-cdk/cdk-explorer + +A Language Server (LSP) for AWS CDK apps. It runs your CDK app, reads the +synthesized cloud assembly from your project's `cdk.out` directory, and +surfaces that back in your editor: code lenses on the constructs you author, +hover details from the generated CloudFormation, go-to-definition from a +template to the construct that created it, and diagnostics from policy +validation. + +Used by the `cdk lsp` command in the AWS CDK CLI. It is for CDK developers who +want in-editor feedback without leaving their source files. + +## Installing and running + +The server ships inside the AWS CDK CLI. Start it over stdio: + + cdk lsp + +Your editor's LSP client launches that command and talks to it over +stdin/stdout. You can also start it programmatically: + + import { startLspServer } from '@aws-cdk/cdk-explorer'; + startLspServer(); + +## Features + +- Code lenses on source lines that create resources: `Creates ` opens that + resource in the synthesized template (a picker when a line maps to several), + plus `Synth now` / `Enable auto-synth` / `Disable auto-synth` in the file header. +- Hover: a construct's resolved CloudFormation properties and a link to the + generated template. +- Go to definition: from a position in a synthesized `*.template.json` back to the + construct source that produced it. +- Diagnostics: findings from the policy validation report appear as squiggles in + your source. The rule description and any suggested fix ride in the diagnostic + message, so your editor shows them when you hover the squiggle and in its + problems list. The report identifies a violating construct rather than a source + line, so a squiggle sits on the line that creates the construct, not on the + specific property at fault. + +Source-linked features (code lenses, hover, go to definition) currently work for +TypeScript and Python. + +## Security + +`Enable auto-synth` runs your app on every save. When it is on, saving a file +runs the `app` command from your `cdk.json` (the same command `cdk synth` runs) +in a subprocess, with your shell environment and AWS credentials. The +`Synth now` code lens does the same once, on demand. + +This mirrors how `cdk synth` and `cdk watch` already work, but the code lens +makes it a single click. Enable auto-synth only for projects you trust. Do not +enable it in a workspace whose `cdk.json` or source you have not reviewed, as +opening it and saving would run that project's command with your credentials. + +## Editor integration + +`cdk lsp` is a standard stdio language server. No editor plugin is published +yet; any LSP client can integrate: + +- Launch `cdk lsp` and connect over stdin/stdout. +- On `initialize`, set `initializationOptions.applicationDir` to the CDK project + root (the directory containing `cdk.json`). It falls back to the server's + working directory if omitted. +- The server advertises hover, go-to-definition, code lens, and + `workspace/executeCommand` for `cdk.explorer.synthNow`, + `cdk.explorer.enableAutoSynth`, and `cdk.explorer.disableAutoSynth`. +- Register a client command named `cdkExplorer.openResource` so the resource + lenses navigate. It receives a list of `{ label, description, target }` items; + open the target's template location, showing a picker when there is more than + one. +- To refresh lenses after a synth, advertise `workspace.codeLens.refreshSupport` + in your client capabilities. + +`cdk lsp` is intended to be consumed by an editor extension. Because enabling +auto-synth runs project code with your credentials (see Security above), the +integrating extension should gate the first `Enable auto-synth` in a workspace +behind the editor's workspace-trust prompt. + +## Supported clients + +LSP clients speaking Language Server Protocol 3.x. diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 96888a196..2df0f27f3 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -46,6 +46,7 @@ import { type AssemblyWatcher, type AssemblyWatcherOptions, } from '../core/assembly-watcher'; +import { isWithinRoot } from '../core/source-resolver'; import type { SynthRunResult } from '../core/synth-runner'; /** @@ -396,6 +397,10 @@ export function createLspHandlers(options: LspHandlerOptions): LspHandlers { return undefined; } const filePath = fileURLToPath(uri); + // Bound the client-supplied path to this project's cdk.out before reading. + if (!(await isWithinRoot(path.join(currentProjectDir(), 'cdk.out'), filePath))) { + return undefined; + } let templateText: string; try { templateText = await fs.promises.readFile(filePath, 'utf-8'); diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index b206944fc..63dc5374b 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -33,7 +33,6 @@ "@cdklabs/eslint-plugin": "^2.0.9", "@stylistic/eslint-plugin": "^3", "@types/convert-source-map": "^2", - "@types/express": "^4", "@types/jest": "^29.5.14", "@types/node": "^20", "@typescript-eslint/eslint-plugin": "^8", @@ -62,7 +61,6 @@ "@jridgewell/trace-mapping": "^0.3", "chokidar": "^4", "convert-source-map": "^2", - "express": "^4", "vscode-jsonrpc": "^8", "vscode-languageserver": "^9", "vscode-languageserver-textdocument": "^1" diff --git a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts index 1d67b8b05..4bafb925c 100644 --- a/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/lsp/server.test.ts @@ -449,7 +449,9 @@ describe('LSP Server', () => { test('onDefinition resolves a template position back to construct source', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'server-def-')); - const templateFile = path.join(dir, 'Stack1.template.json'); + const outDir = path.join(dir, 'cdk.out'); + fs.mkdirSync(outDir, { recursive: true }); + const templateFile = path.join(outDir, 'Stack1.template.json'); const text = JSON.stringify({ Resources: { MyBucket: { Type: 'AWS::S3::Bucket' } } }, undefined, 1); fs.writeFileSync(templateFile, text); try { @@ -499,6 +501,23 @@ describe('LSP Server', () => { position: { line: 0, character: 0 }, })).toBeUndefined(); }); + + test('onDefinition returns undefined for a template outside the project cdk.out', async () => { + const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'server-def-outside-')); + const templateFile = path.join(outside, 'Evil.template.json'); + fs.writeFileSync(templateFile, JSON.stringify({ Resources: {} })); + try { + const client = createTestClient(); + await initializeClient(client, { applicationDir: '/p' }); + const target = await client.handlers.onDefinition({ + textDocument: { uri: pathToFileURL(templateFile).toString() }, + position: { line: 0, character: 0 }, + }); + expect(target).toBeUndefined(); + } finally { + fs.rmSync(outside, { recursive: true, force: true }); + } + }); }); describe('LSP Server -- executeCommand', () => { diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json index ac3e8501d..56dc8cf9b 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json @@ -26,7 +26,6 @@ "target": "ES2020", "types": [ "convert-source-map", - "express", "jest", "node" ], diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.json b/packages/@aws-cdk/cdk-explorer/tsconfig.json index 7324429b1..dfa26c5c1 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.json @@ -28,7 +28,6 @@ "target": "ES2020", "types": [ "convert-source-map", - "express", "jest", "node" ], diff --git a/yarn.lock b/yarn.lock index a7ffad63b..8ab91cd13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -170,7 +170,6 @@ __metadata: "@jridgewell/trace-mapping": "npm:^0.3" "@stylistic/eslint-plugin": "npm:^3" "@types/convert-source-map": "npm:^2" - "@types/express": "npm:^4" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^20" "@typescript-eslint/eslint-plugin": "npm:^8" @@ -185,7 +184,6 @@ __metadata: eslint-plugin-jest: "npm:^29.15.2" eslint-plugin-jsdoc: "npm:^62.9.0" eslint-plugin-prettier: "npm:^4.2.5" - express: "npm:^4" jest: "npm:^29.7.0" jest-junit: "npm:^16" nx: "npm:^22.7.5" @@ -6680,16 +6678,6 @@ __metadata: languageName: node linkType: hard -"@types/body-parser@npm:*": - version: 1.19.6 - resolution: "@types/body-parser@npm:1.19.6" - dependencies: - "@types/connect": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/542da05c924dce58ee23f50a8b981fee36921850c82222e384931fda3e106f750f7880c47be665217d72dbe445129049db6eb1f44e7a06b09d62af8f3cca8ea7 - languageName: node - linkType: hard - "@types/chai@npm:^5.2.2": version: 5.2.3 resolution: "@types/chai@npm:5.2.3" @@ -6700,15 +6688,6 @@ __metadata: languageName: node linkType: hard -"@types/connect@npm:*": - version: 3.4.38 - resolution: "@types/connect@npm:3.4.38" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c - languageName: node - linkType: hard - "@types/convert-source-map@npm:^2": version: 2.0.3 resolution: "@types/convert-source-map@npm:2.0.3" @@ -6739,30 +6718,6 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^4.17.33": - version: 4.19.8 - resolution: "@types/express-serve-static-core@npm:4.19.8" - dependencies: - "@types/node": "npm:*" - "@types/qs": "npm:*" - "@types/range-parser": "npm:*" - "@types/send": "npm:*" - checksum: 10c0/6fb58a85b209e0e421b29c52e0a51dbf7c039b711c604cf45d46470937a5c7c16b30aa5ce9bf7da0bd8a2e9361c95b5055599c0500a96bf4414d26c81f02d7fe - languageName: node - linkType: hard - -"@types/express@npm:^4": - version: 4.17.25 - resolution: "@types/express@npm:4.17.25" - dependencies: - "@types/body-parser": "npm:*" - "@types/express-serve-static-core": "npm:^4.17.33" - "@types/qs": "npm:*" - "@types/serve-static": "npm:^1" - checksum: 10c0/f42b616d2c9dbc50352c820db7de182f64ebbfa8dba6fb6c98e5f8f0e2ef3edde0131719d9dc6874803d25ad9ca2d53471d0fec2fbc60a6003a43d015bab72c4 - languageName: node - linkType: hard - "@types/fs-extra@npm:^11": version: 11.0.4 resolution: "@types/fs-extra@npm:11.0.4" @@ -6782,13 +6737,6 @@ __metadata: languageName: node linkType: hard -"@types/http-errors@npm:*": - version: 2.0.5 - resolution: "@types/http-errors@npm:2.0.5" - checksum: 10c0/00f8140fbc504f47356512bd88e1910c2f07e04233d99c88c854b3600ce0523c8cd0ba7d1897667243282eb44c59abb9245959e2428b9de004f93937f52f7c15 - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -6889,13 +6837,6 @@ __metadata: languageName: node linkType: hard -"@types/mime@npm:^1": - version: 1.3.5 - resolution: "@types/mime@npm:1.3.5" - checksum: 10c0/c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc - languageName: node - linkType: hard - "@types/mime@npm:^2": version: 2.0.3 resolution: "@types/mime@npm:2.0.3" @@ -6983,20 +6924,6 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:*": - version: 6.15.1 - resolution: "@types/qs@npm:6.15.1" - checksum: 10c0/1dfdbcb4cf2a8f66d57f0b9a9fe6b1c7091cb816687b6698c1351eaf31f62e412cea9b7453a9637b570cd5fad8dced527e5a9e69b4fcc6e318daacd8b749f094 - languageName: node - linkType: hard - -"@types/range-parser@npm:*": - version: 1.2.7 - resolution: "@types/range-parser@npm:1.2.7" - checksum: 10c0/361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c - languageName: node - linkType: hard - "@types/semver@npm:^7, @types/semver@npm:^7.7.1": version: 7.7.1 resolution: "@types/semver@npm:7.7.1" @@ -7004,36 +6931,6 @@ __metadata: languageName: node linkType: hard -"@types/send@npm:*": - version: 1.2.1 - resolution: "@types/send@npm:1.2.1" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/7673747f8c2d8e67f3b1b3b57e9d4d681801a4f7b526ecf09987bb9a84a61cf94aa411c736183884dc762c1c402a61681eb1ef200d8d45d7e5ec0ab67ea5f6c1 - languageName: node - linkType: hard - -"@types/send@npm:<1": - version: 0.17.6 - resolution: "@types/send@npm:0.17.6" - dependencies: - "@types/mime": "npm:^1" - "@types/node": "npm:*" - checksum: 10c0/a9d76797f0637738062f1b974e0fcf3d396a28c5dc18c3f95ecec5dabda82e223afbc2d56a0bca46b6326fd7bb229979916cea40de2270a98128fd94441b87c2 - languageName: node - linkType: hard - -"@types/serve-static@npm:^1": - version: 1.15.10 - resolution: "@types/serve-static@npm:1.15.10" - dependencies: - "@types/http-errors": "npm:*" - "@types/node": "npm:*" - "@types/send": "npm:<1" - checksum: 10c0/842fca14c9e80468f89b6cea361773f2dcd685d4616a9f59013b55e1e83f536e4c93d6d8e3ba5072d40c4e7e64085210edd6646b15d538ded94512940a23021f - languageName: node - linkType: hard - "@types/sinon@npm:^17.0.3, @types/sinon@npm:^17.0.4": version: 17.0.4 resolution: "@types/sinon@npm:17.0.4" @@ -8522,26 +8419,6 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:~1.20.5": - version: 1.20.5 - resolution: "body-parser@npm:1.20.5" - dependencies: - bytes: "npm:~3.1.2" - content-type: "npm:~1.0.5" - debug: "npm:2.6.9" - depd: "npm:2.0.0" - destroy: "npm:~1.2.0" - http-errors: "npm:~2.0.1" - iconv-lite: "npm:~0.4.24" - on-finished: "npm:~2.4.1" - qs: "npm:~6.15.1" - raw-body: "npm:~2.5.3" - type-is: "npm:~1.6.18" - unpipe: "npm:~1.0.0" - checksum: 10c0/ad777ca5e4711eae253c93f50fdc4608c60b76a9710d79e5e5b84581c76691e6ad21ecc9158986d9ea2b365df73e403ca33c27a8bccc1a7cfc2ccc248548118d - languageName: node - linkType: hard - "bowser@npm:^2.11.0": version: 2.14.1 resolution: "bowser@npm:2.14.1" @@ -10899,45 +10776,6 @@ __metadata: languageName: node linkType: hard -"express@npm:^4": - version: 4.22.2 - resolution: "express@npm:4.22.2" - dependencies: - accepts: "npm:~1.3.8" - array-flatten: "npm:1.1.1" - body-parser: "npm:~1.20.5" - content-disposition: "npm:~0.5.4" - content-type: "npm:~1.0.4" - cookie: "npm:~0.7.1" - cookie-signature: "npm:~1.0.6" - debug: "npm:2.6.9" - depd: "npm:2.0.0" - encodeurl: "npm:~2.0.0" - escape-html: "npm:~1.0.3" - etag: "npm:~1.8.1" - finalhandler: "npm:~1.3.1" - fresh: "npm:~0.5.2" - http-errors: "npm:~2.0.0" - merge-descriptors: "npm:1.0.3" - methods: "npm:~1.1.2" - on-finished: "npm:~2.4.1" - parseurl: "npm:~1.3.3" - path-to-regexp: "npm:~0.1.12" - proxy-addr: "npm:~2.0.7" - qs: "npm:~6.15.1" - range-parser: "npm:~1.2.1" - safe-buffer: "npm:5.2.1" - send: "npm:~0.19.0" - serve-static: "npm:~1.16.2" - setprototypeof: "npm:1.2.0" - statuses: "npm:~2.0.1" - type-is: "npm:~1.6.18" - utils-merge: "npm:1.0.1" - vary: "npm:~1.1.2" - checksum: 10c0/d06dd4379fd217440b30f8abbe45f0e74931114c1395034f03e7d635196ecdab530d4835a1962a6aa34838d61967dc6f1f77846999bba3032373e9e714222c44 - languageName: node - linkType: hard - "express@npm:^4.14.0": version: 4.22.1 resolution: "express@npm:4.22.1" @@ -16129,16 +15967,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:~6.15.1": - version: 6.15.3 - resolution: "qs@npm:6.15.3" - dependencies: - es-define-property: "npm:^1.0.1" - side-channel: "npm:^1.1.1" - checksum: 10c0/8f3f6e45ece255347d57696628401cde29e9ec649fff698b53bd3150dea7cefdf33036e1bc1826b9f110bfa7cb0ec4ab9f5297eca628ce216c55af82c304e08e - languageName: node - linkType: hard - "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -16914,7 +16742,7 @@ __metadata: languageName: node linkType: hard -"side-channel-list@npm:^1.0.0, side-channel-list@npm:^1.0.1": +"side-channel-list@npm:^1.0.0": version: 1.0.1 resolution: "side-channel-list@npm:1.0.1" dependencies: @@ -16962,19 +16790,6 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.1.1": - version: 1.1.1 - resolution: "side-channel@npm:1.1.1" - dependencies: - es-errors: "npm:^1.3.0" - object-inspect: "npm:^1.13.4" - side-channel-list: "npm:^1.0.1" - side-channel-map: "npm:^1.0.1" - side-channel-weakmap: "npm:^1.0.2" - checksum: 10c0/dc0ab81d67f61bda9247d053ce93f41c3fd8ad2bdcb9cf9d8d2f8540d488f26d87a5e99ebfc07eea49ec025867b2452b705442d974b1478f0395e69f6bfb3270 - languageName: node - linkType: hard - "signal-exit@npm:3.0.7, signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" From b2ce9499903c0d608069a87fc0185050d46c814f Mon Sep 17 00:00:00 2001 From: megha-narayanan Date: Thu, 9 Jul 2026 12:14:05 -0400 Subject: [PATCH 24/28] chore(cdk-explorer): use default fs import for esModuleInterop (follows #1685) --- .../@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts index a7659322e..3c60cf049 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/source-resolver.test.ts @@ -1,4 +1,4 @@ -import * as fs from 'fs'; +import fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { SourceMapResolver, isWithinRoot } from '../../lib/core/source-resolver'; From bd5fa615d024c371231b6ffa5a80aa23d4ad55aa Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:16:58 -0400 Subject: [PATCH 25/28] feat: web explorer web three way nav (#1706) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A full cdk explore UI over the synthesized assembly. Builds on #1624. Features - Three panes: construct tree, source, template, with syntax highlighting for all CDK source languages (TypeScript, JavaScript, Python, Java, C#, Go) - Template pane: JSON/YAML toggle - Linked navigation by double-click: tree → source + template, template → source, and source → template - Violations panel grouped by rule (click an occurrence to jump to its construct), inline diagnostic squiggles in the source pane, and severity coloring in the tree (inherited up to ancestors) - Resizable split panes and an "Open" file picker for either pane Design decisions - Custom line renderer over PrismJS tokens, not Prism's HTML. syntax.ts calls Prism.tokenize and flattens the token tree into per-line token arrays (Prism tokens can straddle newlines). CodeViewer then renders each line itself, which is what lets one component compose four things per line: syntax colors, the nav-highlight band, scroll-to-line, and column-accurate diagnostic squiggles. Prism's string output can't be sliced per line or overlaid with diagnostics. - PrismJS grammars, not a full editor/highlighter. This bundles into the shipped CLI via esbuild, so Monaco-scale dependencies are probably too big. Prism core plus JSON, YAML, and the CDK source languages keeps it small, and one CodeViewer serves both panes. - Navigate by logical ID, not line numbers. JSON and YAML render at different lines, so navigation carries only the logical ID and the template viewer resolves the highlight line in whichever format is on screen. - Source → template is nearest-preceding, ties to the top-most construct. Synthesized children share their parent's single creation line (every subnet/NAT under a new ec2.Vpc(...)), so a click there resolves to the authored parent, not a child. Screenshot 2026-07-06 at 12 08
28 PM ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 3 + .../@aws-cdk/cdk-explorer/.projen/deps.json | 15 + .../@aws-cdk/cdk-explorer/frontend/App.tsx | 344 ++++++++++++++++-- .../@aws-cdk/cdk-explorer/frontend/api.ts | 9 + .../frontend/components/CodeViewer.tsx | 237 ++++++++++++ .../frontend/components/ConstructTree.tsx | 47 ++- .../frontend/components/FilePane.tsx | 115 ------ .../frontend/components/TemplateViewer.tsx | 169 +++++++++ .../frontend/components/ViolationsPanel.tsx | 49 ++- .../cdk-explorer/frontend/highlight.css | 17 + .../@aws-cdk/cdk-explorer/frontend/index.tsx | 1 + .../cdk-explorer/frontend/nav-types.ts | 9 + .../cdk-explorer/frontend/prismjs.d.ts | 9 + .../@aws-cdk/cdk-explorer/frontend/syntax.ts | 93 +++++ packages/@aws-cdk/cdk-explorer/lib/index.ts | 2 + .../@aws-cdk/cdk-explorer/lib/web/protocol.ts | 20 + .../@aws-cdk/cdk-explorer/lib/web/routes.ts | 201 +++++++++- .../@aws-cdk/cdk-explorer/lib/web/server.ts | 12 +- .../cdk-explorer/lib/web/source-nav.ts | 87 +++++ packages/@aws-cdk/cdk-explorer/package.json | 5 +- .../cdk-explorer/test/web/routes.test.ts | 185 +++++++++- .../cdk-explorer/test/web/source-nav.test.ts | 113 ++++++ .../@aws-cdk/cdk-explorer/tsconfig.dev.json | 1 + packages/@aws-cdk/cdk-explorer/tsconfig.json | 1 + .../cloud-assembly-api/lib/template-ranges.ts | 42 +++ yarn.lock | 19 +- 26 files changed, 1629 insertions(+), 176 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx delete mode 100644 packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/components/TemplateViewer.tsx create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/highlight.css create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/prismjs.d.ts create mode 100644 packages/@aws-cdk/cdk-explorer/frontend/syntax.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/source-nav.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/source-nav.test.ts diff --git a/.projenrc.ts b/.projenrc.ts index f1703ed24..80853235a 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1736,6 +1736,9 @@ const cdkExplorer = configureProject( 'supertest@^6', '@types/supertest@^6', '@types/convert-source-map@^2', + 'prismjs@^1', + '@types/prismjs@^1', + 'yaml@^2', ], tsconfig: { compilerOptions: { diff --git a/packages/@aws-cdk/cdk-explorer/.projen/deps.json b/packages/@aws-cdk/cdk-explorer/.projen/deps.json index 7bb433d89..fac5ba5a3 100644 --- a/packages/@aws-cdk/cdk-explorer/.projen/deps.json +++ b/packages/@aws-cdk/cdk-explorer/.projen/deps.json @@ -38,6 +38,11 @@ "version": "^20", "type": "build" }, + { + "name": "@types/prismjs", + "version": "^1", + "type": "build" + }, { "name": "@types/react-dom", "version": "^18", @@ -119,6 +124,11 @@ "version": "^2.8", "type": "build" }, + { + "name": "prismjs", + "version": "^1", + "type": "build" + }, { "name": "projen", "type": "build" @@ -156,6 +166,11 @@ "version": "^3", "type": "build" }, + { + "name": "yaml", + "version": "^2", + "type": "build" + }, { "name": "@aws-cdk/cloud-assembly-api", "type": "runtime" diff --git a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx index e2618675b..f7f72e744 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx @@ -1,21 +1,57 @@ import Box from '@cloudscape-design/components/box'; +import Button from '@cloudscape-design/components/button'; import Container from '@cloudscape-design/components/container'; -import Grid from '@cloudscape-design/components/grid'; import Header from '@cloudscape-design/components/header'; +import SpaceBetween from '@cloudscape-design/components/space-between'; import Spinner from '@cloudscape-design/components/spinner'; import * as React from 'react'; -import { api, type TreeResponse, type ViolationsResponse } from './api'; +import { buildSourceAnchorIndex, findConstructAtLine } from '../lib/web/source-nav'; +import { api, type DirEntry, type TemplateResponse, type TreeResponse, type ViolationsResponse } from './api'; +import { CodeViewer, type Diagnostic } from './components/CodeViewer'; import { ConstructTree } from './components/ConstructTree'; -import { FilePane } from './components/FilePane'; +import type { Language } from './syntax'; +import { TemplateViewer } from './components/TemplateViewer'; import { ViolationsPanel } from './components/ViolationsPanel'; +import type { NavigateHandler } from './nav-types'; +export type { NavigateHandler } from './nav-types'; -/** Web explorer shell. Two splits: horizontal between the construct tree and the file panes, vertical between the top row and the violations panel. Each split has a draggable resizer with an arrow toggle that collapses one side. */ +/** Navigation target describing what both panes should show. */ +export interface NavTarget { + readonly source?: { file: string; startLine: number; endLine: number }; + readonly template?: { file: string; logicalId: string }; + readonly color: string; + readonly navCounter: number; +} + +const NEUTRAL_COLOR = '#5f6b7a'; + +/** Web explorer shell. */ export function App(): JSX.Element { const [tree, setTree] = React.useState(); const [violations, setViolations] = React.useState(); const [error, setError] = React.useState(); const [appDir, setAppDir] = React.useState(); + // Source pane state. + const [sourceFile, setSourceFile] = React.useState(); + const [sourceContent, setSourceContent] = React.useState(''); + + // Template pane state. + const [templateFile, setTemplateFile] = React.useState(); + const [templateData, setTemplateData] = React.useState(); + + // Refs for current values in async callbacks (avoids stale closures). + const sourceFileRef = React.useRef(sourceFile); + sourceFileRef.current = sourceFile; + const templateFileRef = React.useRef(templateFile); + templateFileRef.current = templateFile; + const templateDataRef = React.useRef(templateData); + templateDataRef.current = templateData; + + // Navigation state shared across panes. + const [nav, setNav] = React.useState(); + const navCounterRef = React.useRef(0); + const reload = React.useCallback((): void => { Promise.all([api.getTree(), api.getViolations(), api.getAppInfo()]) .then(([t, v, info]) => { @@ -34,6 +70,124 @@ export function App(): JSX.Element { return api.subscribe(reload); }, [reload]); + /** Navigate to a construct (from tree double-click or violation double-click). */ + const navigate: NavigateHandler = React.useCallback(async (opts) => { + const counter = ++navCounterRef.current; + const color = opts.color ?? NEUTRAL_COLOR; + + let sourceTarget: NavTarget['source']; + if (opts.sourceLocation) { + sourceTarget = { file: opts.sourceLocation.file, startLine: opts.sourceLocation.line, endLine: opts.sourceLocation.line }; + if (sourceFileRef.current !== opts.sourceLocation.file) { + try { + const res = await api.readFile(opts.sourceLocation.file); + setSourceFile(res.path); + setSourceContent(res.content); + } catch { + setSourceFile(opts.sourceLocation.file); + setSourceContent(`// Could not load ${opts.sourceLocation.file}`); + } + } + } + + let templateTarget: NavTarget['template']; + if (opts.templateFile && opts.logicalId) { + if (templateFileRef.current !== opts.templateFile) { + try { + const data = await api.getTemplate(opts.templateFile); + setTemplateFile(opts.templateFile); + setTemplateData(data); + } catch { + setTemplateFile(opts.templateFile); + setTemplateData(undefined); + } + } + // Carry identity only; TemplateViewer resolves the highlight line from the + // rendered format (JSON or YAML), which is the sole coordinate authority. + templateTarget = { file: opts.templateFile, logicalId: opts.logicalId }; + } + + setNav({ source: sourceTarget, template: templateTarget, color, navCounter: counter }); + }, []); + + /** Jump from template pane to source (the "Open in source" button). */ + const jumpToSource = React.useCallback(async (logicalId: string) => { + const data = templateDataRef.current; + if (!data) return; + const resource = data.resources[logicalId]; + if (!resource?.source) return; + const counter = ++navCounterRef.current; + if (sourceFileRef.current !== resource.source.file) { + try { + const res = await api.readFile(resource.source.file); + setSourceFile(res.path); + setSourceContent(res.content); + } catch { return; } + } + setNav({ + source: { file: resource.source.file, startLine: resource.source.line, endLine: resource.source.line }, + template: undefined, + color: NEUTRAL_COLOR, + navCounter: counter, + }); + }, []); + + // Per-file index of constructs navigable from source to template, built from + // the construct tree the app already loads. Rebuilt only when the tree changes. + const sourceAnchors = React.useMemo( + () => (tree?.status === 'ok' ? buildSourceAnchorIndex(tree.tree) : undefined), + [tree], + ); + + /** Double-click a line in the source pane to jump to its resource in the template. */ + const handleSourceDoubleClick = React.useCallback((line: number) => { + const file = sourceFileRef.current; + if (!file) return; + const node = findConstructAtLine(sourceAnchors?.get(file), line); + if (!node) return; + void navigate({ + sourceLocation: node.sourceLocation, + templateFile: node.templateFile, + logicalId: node.logicalId, + }); + }, [sourceAnchors, navigate]); + + // File picker state. + const [showFilePicker, setShowFilePicker] = React.useState(false); + const [pickerDir, setPickerDir] = React.useState(''); + const [pickerEntries, setPickerEntries] = React.useState([]); + + const openFilePicker = React.useCallback(async (pane: 'source' | 'template') => { + setShowFilePicker(pane); + try { + const res = await api.listFiles(''); + setPickerDir(res.dir); + setPickerEntries(res.entries); + } catch { /* ignore */ } + }, []); + + const browseDir = React.useCallback(async (dir: string) => { + try { + const res = await api.listFiles(dir); + setPickerDir(res.dir); + setPickerEntries(res.entries); + } catch { /* ignore */ } + }, []); + + const pickFile = React.useCallback(async (filePath: string, pane: 'source' | 'template') => { + try { + const res = await api.readFile(filePath); + if (pane === 'source') { + setSourceFile(res.path); + setSourceContent(res.content); + } else { + setTemplateFile(res.path); + setTemplateData({ content: res.content, resources: {} }); + } + setShowFilePicker(false); + } catch { /* ignore */ } + }, []); + // Vertical split: violations row's share of the page height (default 33%). const vSplit = useSplit({ orientation: 'vertical', defaultFraction: 0.33, min: 0.15, max: 0.85 }); // Horizontal split inside the top row: file panes' share of that row's width (default 75%; tree gets the remaining 25%). @@ -50,17 +204,70 @@ export function App(): JSX.Element { {!hSplit.collapsed && (
    Construct Tree}> - +
    )}
    - - - - +
    +
    + + + {sourceFile ?? 'Source'} + + + + }> + {showFilePicker === 'source' ? ( + void pickFile(p, 'source')} /> + ) : sourceContent ? ( + + ) : ( + Double-click a construct to view its source. + )} + +
    +
    + + + {templateFile ?? 'Template'} + + + + }> + {showFilePicker === 'template' ? ( + void pickFile(p, 'template')} /> + ) : templateData ? ( + + ) : templateFile ? ( + Could not load {templateFile} + ) : ( + Double-click a construct to view its template. + )} + +
    +
    @@ -68,7 +275,7 @@ export function App(): JSX.Element { {!vSplit.collapsed && (
    Violations}> - +
    )} @@ -77,24 +284,48 @@ export function App(): JSX.Element { ); } -function ConstructTreeContent({ tree }: { readonly tree?: TreeResponse }): JSX.Element { + +function FilePicker({ dir, entries, onBrowse, onPick }: { + readonly dir: string; + readonly entries: readonly DirEntry[]; + readonly onBrowse: (dir: string) => void; + readonly onPick: (path: string) => void; +}): JSX.Element { + return ( + + /{dir} + {dir !== '' && } + {entries.map((entry) => ( + + ))} + + ); +} + +function ConstructTreeContent({ tree, onNavigate }: { readonly tree?: TreeResponse; readonly onNavigate: NavigateHandler }): JSX.Element { if (!tree) return ; if (tree.status === 'not-synthesized') { return No cloud assembly found. Run cdk synth first.; } - return ; + return ; } -function ViolationsContent({ violations }: { readonly violations?: ViolationsResponse }): JSX.Element { +function ViolationsContent({ violations, onNavigate }: { readonly violations?: ViolationsResponse; readonly onNavigate: NavigateHandler }): JSX.Element { if (!violations) return ; if (violations.status === 'not-synthesized') { return No cloud assembly found.; } - return ; + return ; } interface SplitOptions { - /** 'horizontal' = the resizer moves left/right (separating columns). 'vertical' = the resizer moves up/down (separating rows). */ readonly orientation: 'horizontal' | 'vertical'; readonly defaultFraction: number; readonly min: number; @@ -102,7 +333,6 @@ interface SplitOptions { } interface Split extends SplitOptions { - /** Fraction the *trailing* pane (right column / bottom row) takes of its container; 0..1. */ readonly fraction: number; readonly collapsed: boolean; readonly toggleCollapsed: () => void; @@ -110,14 +340,13 @@ interface Split extends SplitOptions { readonly containerRef: React.RefObject; } -/** Drives a resizable / collapsible split. Drag updates the fraction live; the arrow toggles full collapse of the trailing pane. */ function useSplit(opts: SplitOptions): Split { const containerRef = React.useRef(null); const [fraction, setFraction] = React.useState(opts.defaultFraction); const [collapsed, setCollapsed] = React.useState(false); const startDrag = React.useCallback((e: React.MouseEvent) => { - if (collapsed) return; // Drag while collapsed makes no sense; user must expand first via the arrow. + if (collapsed) return; e.preventDefault(); const container = containerRef.current; if (!container) return; @@ -125,7 +354,6 @@ function useSplit(opts: SplitOptions): Split { document.body.style.cursor = opts.orientation === 'vertical' ? 'row-resize' : 'col-resize'; const onMove = (ev: MouseEvent) => { const rect = container.getBoundingClientRect(); - // Trailing pane share grows as the pointer moves toward the leading edge. const frac = opts.orientation === 'vertical' ? (rect.bottom - ev.clientY) / rect.height : (rect.right - ev.clientX) / rect.width; @@ -146,10 +374,8 @@ function useSplit(opts: SplitOptions): Split { return { ...opts, fraction, collapsed, toggleCollapsed, startDrag, containerRef }; } -/** Thin strip pinned to the edge of the trailing pane. The whole strip is the drag handle; a centered arrow toggles collapse. */ function Resizer({ split }: { readonly split: Split }): JSX.Element { const isVertical = split.orientation === 'vertical'; - // Arrow points the way the boundary travels on collapse, and flips once collapsed. const arrow = isVertical ? (split.collapsed ? '▲' : '▼') : (split.collapsed ? '▶' : '◀'); @@ -235,7 +461,6 @@ function resizerStyle(split: Split): React.CSSProperties { justifyContent: 'center', cursor: split.collapsed ? 'pointer' : 'col-resize', position: 'relative', - // Slide 4px left so the pill straddles the tree pane's border. marginLeft: split.collapsed ? 0 : '-4px', }; } @@ -249,9 +474,34 @@ const PAGE_STYLE: React.CSSProperties = { overflow: 'hidden', }; const TITLE_BLOCK_STYLE: React.CSSProperties = { flexShrink: 0, marginBottom: '12px' }; -/** Wraps a Cloudscape Container so it stretches to fill its flex parent (Container is intrinsically sized). */ const GROW_STYLE: React.CSSProperties = { flex: '1 1 0', minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }; +const HEADER_WITH_ACTION_STYLE: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: '6px' }; +const OPEN_FILE_BUTTON_STYLE: React.CSSProperties = { + border: '1px solid #d1d5db', + borderRadius: '4px', + background: '#fafafa', + cursor: 'pointer', + fontSize: '11px', + padding: '1px 6px', + color: '#5f6b7a', + lineHeight: '16px', +}; + +const CODE_PANES_STYLE: React.CSSProperties = { + display: 'flex', + gap: '8px', + height: '100%', +}; + +const CODE_PANE_STYLE: React.CSSProperties = { + flex: '1 1 50%', + minWidth: 0, + minHeight: 0, + display: 'flex', + flexDirection: 'column', +}; + const RESIZER_BUTTON_BASE: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', @@ -277,3 +527,51 @@ const RESIZER_BUTTON_VERTICAL: React.CSSProperties = { height: '40px', borderRadius: '7px', }; + +function detectLanguage(file: string | undefined): Language { + if (!file) return 'typescript'; + if (file.endsWith('.json')) return 'json'; + if (file.endsWith('.yaml') || file.endsWith('.yml')) return 'yaml'; + if (file.endsWith('.py')) return 'python'; + if (file.endsWith('.java')) return 'java'; + if (file.endsWith('.cs')) return 'csharp'; + if (file.endsWith('.go')) return 'go'; + if (file.endsWith('.js') || file.endsWith('.jsx') || file.endsWith('.mjs')) return 'javascript'; + return 'typescript'; +} + +function buildDiagnostics( + sourceFile: string | undefined, + violations: ViolationsResponse | undefined, +): Diagnostic[] | undefined { + if (!sourceFile || !violations || violations.status !== 'ok' || violations.violations.length === 0) { + return undefined; + } + const diagnostics: Diagnostic[] = []; + for (const violation of violations.violations) { + const severity = violationSeverityToDiagnostic(violation.severity); + for (const occ of violation.occurrences) { + if (occ.sourceLocation?.file === sourceFile && occ.sourceLocation.line >= 1) { + diagnostics.push({ + startLine: occ.sourceLocation.line, + startCol: occ.sourceLocation.column >= 1 ? occ.sourceLocation.column : 1, + severity, + message: violation.description, + }); + } + } + } + return diagnostics.length > 0 ? diagnostics : undefined; +} + +function violationSeverityToDiagnostic(severity: string | undefined): 'error' | 'warning' | 'info' { + switch (severity) { + case 'fatal': + case 'error': + return 'error'; + case 'warning': + return 'warning'; + default: + return 'info'; + } +} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/api.ts b/packages/@aws-cdk/cdk-explorer/frontend/api.ts index 145274a03..e28a07588 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/api.ts +++ b/packages/@aws-cdk/cdk-explorer/frontend/api.ts @@ -3,9 +3,13 @@ import { type DirEntry, type FilesResponse, type FileResponse, + type LineRange, + type TemplateResource, + type TemplateResponse, type TreeResponse, type ViolationsResponse, type WebConstructNode, + type WebSourceLocation, type WebViolation, type WebViolationOccurrence, } from '../lib/web/protocol'; @@ -14,9 +18,13 @@ export type { DirEntry, FilesResponse, FileResponse, + LineRange, + TemplateResource, + TemplateResponse, TreeResponse, ViolationsResponse, WebConstructNode, + WebSourceLocation, WebViolation, WebViolationOccurrence, }; @@ -50,4 +58,5 @@ export const api = { source.addEventListener(ASSEMBLY_CHANGED, () => onChange()); return () => source.close(); }, + getTemplate: (file: string): Promise => getJson(`/api/template?file=${encodeURIComponent(file)}`), }; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx new file mode 100644 index 000000000..47f2c8f83 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx @@ -0,0 +1,237 @@ +import * as React from 'react'; +import { tokenizeLines, type Language, type Token } from '../syntax'; + +export interface Diagnostic { + readonly startLine: number; + readonly startCol: number; + readonly endCol?: number; + readonly severity: 'error' | 'warning' | 'info'; + readonly message?: string; +} + +export interface CodeViewerProps { + readonly content: string; + readonly language: Language; + readonly highlightStart?: number; + readonly highlightEnd?: number; + readonly highlightColor?: string; + readonly navCounter?: number; + readonly scrollToLine?: number; + readonly onLineDoubleClick?: (line: number) => void; + readonly diagnostics?: readonly Diagnostic[]; +} + +const SQUIGGLE_COLORS: Record = { + error: '#d91515', + warning: '#ff9900', + info: '#0972d3', +}; + +const TOKEN_COLORS: Record = { + keyword: '#0000ff', + string: '#a31515', + number: '#098658', + boolean: '#0000ff', + 'attr-name': '#0451a5', + property: '#0451a5', + operator: '#393a34', + punctuation: '#393a34', + comment: '#008000', + 'class-name': '#267f99', + builtin: '#267f99', + function: '#795e26', + tag: '#0451a5', + selector: '#0451a5', + key: '#0451a5', + 'atrule': '#0000ff', + // YAML-specific + important: '#098658', + // null/undefined + 'null': '#0000ff', +}; + +export function CodeViewer({ + content, + language, + highlightStart, + highlightEnd, + highlightColor, + navCounter, + scrollToLine, + onLineDoubleClick, + diagnostics, +}: CodeViewerProps): JSX.Element { + const scrollTargetRef = React.useRef(null); + + const tokenizedLines = React.useMemo( + () => tokenizeLines(content, language), + [content, language], + ); + + const gutterWidth = React.useMemo(() => { + const digits = String(tokenizedLines.length).length; + return `${digits * 8 + 8}px`; + }, [tokenizedLines.length]); + + const diagnosticsByLine = React.useMemo(() => { + if (!diagnostics?.length) return undefined; + const map = new Map(); + for (const d of diagnostics) { + const arr = map.get(d.startLine) ?? []; + arr.push(d); + map.set(d.startLine, arr); + } + return map; + }, [diagnostics]); + + React.useEffect(() => { + if (scrollToLine && scrollTargetRef.current) { + scrollTargetRef.current.scrollIntoView({ block: 'start', behavior: 'smooth' }); + } + }, [scrollToLine, navCounter]); + + return ( +
    + {tokenizedLines.map((lineTokens, i) => { + const lineNum = i + 1; + const isHighlighted = highlightStart !== undefined + && highlightEnd !== undefined + && lineNum >= highlightStart + && lineNum <= highlightEnd; + const isScrollTarget = lineNum === scrollToLine; + const lineDiagnostics = diagnosticsByLine?.get(lineNum); + + return ( +
    onLineDoubleClick(lineNum) : undefined} + > + {lineNum} + + {lineDiagnostics + ? renderWithDiagnostics(lineTokens, lineDiagnostics) + : renderTokens(lineTokens) + } + +
    + ); + })} +
    + ); +} + +function renderTokens(tokens: Token[]): React.ReactNode { + return tokens.map((token, j) => { + const color = token.type ? TOKEN_COLORS[token.type] : undefined; + return color + ? {token.content} + : {token.content}; + }); +} + +function renderWithDiagnostics(tokens: Token[], diagnostics: Diagnostic[]): React.ReactNode { + let col = 1; + const elements: React.ReactNode[] = []; + let key = 0; + + for (const token of tokens) { + const tokenStart = col; + const tokenEnd = col + token.content.length; + const color = token.type ? TOKEN_COLORS[token.type] : undefined; + + let hasOverlap = false; + for (const d of diagnostics) { + const dEnd = d.endCol ?? 999; + if (d.startCol < tokenEnd && dEnd > tokenStart) { + hasOverlap = true; + break; + } + } + + if (!hasOverlap) { + elements.push( + color + ? {token.content} + : {token.content}, + ); + } else { + const chars = token.content; + let segStart = 0; + let currentSeverity = ''; + + for (let c = 0; c <= chars.length; c++) { + const charCol = tokenStart + c; + let severity = ''; + for (const d of diagnostics) { + const dEnd = d.endCol ?? 999; + if (charCol >= d.startCol && charCol < dEnd) { + severity = d.severity; + break; + } + } + + if (c === chars.length || severity !== currentSeverity) { + if (segStart < c) { + const text = chars.slice(segStart, c); + if (currentSeverity) { + elements.push( + + {text} + , + ); + } else { + elements.push( + color + ? {text} + : {text}, + ); + } + } + segStart = c; + currentSeverity = severity; + } + } + } + + col = tokenEnd; + } + + return elements; +} + +const CONTAINER_STYLE: React.CSSProperties = { + margin: 0, + maxHeight: '100%', + overflowX: 'auto', + overflowY: 'auto', + fontFamily: 'Monaco, Menlo, "Courier New", monospace', + fontSize: '12px', + lineHeight: '18px', + whiteSpace: 'pre', +}; + +const LINE_STYLE: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + minHeight: '18px', + position: 'relative', +}; + +const LINE_NUM_STYLE: React.CSSProperties = { + flexShrink: 0, + textAlign: 'right', + paddingRight: '8px', + color: '#9ba7b6', + userSelect: 'none', +}; + +const LINE_CONTENT_STYLE: React.CSSProperties = { + flex: '1 1 auto', + minWidth: 0, +}; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx index fb7df3a50..63aa3653f 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/ConstructTree.tsx @@ -3,28 +3,25 @@ import Icon from '@cloudscape-design/components/icon'; import * as React from 'react'; import { severityHexColor } from '../../lib/web/severity'; import type { WebConstructNode } from '../api'; +import type { NavigateHandler } from '../nav-types'; interface ConstructTreeProps { readonly nodes: readonly WebConstructNode[]; readonly depth?: number; + readonly onNavigate: NavigateHandler; } /** Auto-expand the tree through this depth on load (0 = root), so stacks and their top-level constructs are visible without manual clicks. */ const AUTO_EXPAND_DEPTH = 2; -/** - * Renders the construct hierarchy as a nested, indented list. Rows never spill - * past the panel: long labels truncate with an ellipsis and the tree clips - * horizontally. Nodes collapse via a caret (whole-row click). Display-only. - */ -export function ConstructTree({ nodes, depth = 0 }: ConstructTreeProps): JSX.Element { +export function ConstructTree({ nodes, depth = 0, onNavigate }: ConstructTreeProps): JSX.Element { if (nodes.length === 0) { return No constructs.; } const list = (
      {nodes.map((node) => ( - + ))}
    ); @@ -32,7 +29,7 @@ export function ConstructTree({ nodes, depth = 0 }: ConstructTreeProps): JSX.Ele return depth === 0 ?
    {list}
    : list; } -function TreeNode({ node, depth }: { readonly node: WebConstructNode; readonly depth: number }): JSX.Element { +function TreeNode({ node, depth, onNavigate }: { readonly node: WebConstructNode; readonly depth: number; readonly onNavigate: NavigateHandler }): JSX.Element { const hasChildren = node.children.length > 0; const [expanded, setExpanded] = React.useState(depth < AUTO_EXPAND_DEPTH); @@ -42,15 +39,43 @@ function TreeNode({ node, depth }: { readonly node: WebConstructNode; readonly d const inherited = !severity ? node.inheritedSeverity : undefined; const inheritedColor = inherited ? severityHexColor(inherited) : undefined; + const clickTimer = React.useRef | null>(null); + + const handleClick = React.useCallback(() => { + if (!hasChildren) return; + if (clickTimer.current) return; + clickTimer.current = setTimeout(() => { + clickTimer.current = null; + setExpanded((e) => !e); + }, 200); + }, [hasChildren]); + + const handleDoubleClick = React.useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + if (clickTimer.current) { + clearTimeout(clickTimer.current); + clickTimer.current = null; + } + if (!node.sourceLocation && !node.templateFile) return; + const color = severityColor ?? '#5f6b7a'; + onNavigate({ + sourceLocation: node.sourceLocation, + templateFile: node.templateFile, + logicalId: node.logicalId, + color, + }); + }, [node, severityColor, onNavigate]); + return (
  • setExpanded((e) => !e) : undefined} + onClick={handleClick} + onDoubleClick={handleDoubleClick} role={hasChildren ? 'button' : undefined} aria-expanded={hasChildren ? expanded : undefined} > - {hasChildren ? {expanded ? '\u25be' : '\u25b8'} : } + {hasChildren ? {expanded ? '▾' : '▸'} : } {severityColor && ( )}
    - {hasChildren && expanded && } + {hasChildren && expanded && }
  • ); } diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx deleted file mode 100644 index b0a97ac49..000000000 --- a/packages/@aws-cdk/cdk-explorer/frontend/components/FilePane.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import Box from '@cloudscape-design/components/box'; -import Button from '@cloudscape-design/components/button'; -import Container from '@cloudscape-design/components/container'; -import Header from '@cloudscape-design/components/header'; -import SpaceBetween from '@cloudscape-design/components/space-between'; -import * as React from 'react'; -import { api, type DirEntry } from '../api'; - -interface FilePaneProps { - /** Heading shown in the pane header (e.g. "file 1"). */ - readonly title: string; -} - -/** - * A self-contained code pane with a server-backed file picker. Browses - * directories under the app root via /api/files and shows file contents via - * /api/file. Rendered once per code pane (center and right). - */ -export function FilePane({ title }: FilePaneProps): JSX.Element { - const [picking, setPicking] = React.useState(false); - const [dir, setDir] = React.useState(''); - const [entries, setEntries] = React.useState([]); - const [filePath, setFilePath] = React.useState(); - const [content, setContent] = React.useState(''); - const [error, setError] = React.useState(); - - const browse = React.useCallback(async (nextDir: string) => { - try { - const res = await api.listFiles(nextDir); - setDir(res.dir); - setEntries(res.entries); - setError(undefined); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } - }, []); - - const openPicker = React.useCallback(() => { - setPicking(true); - void browse(''); - }, [browse]); - - const choose = React.useCallback(async (entry: DirEntry) => { - if (entry.type === 'dir') { - void browse(entry.path); - return; - } - try { - const res = await api.readFile(entry.path); - setFilePath(res.path); - setContent(res.content); - setPicking(false); - setError(undefined); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } - }, [browse]); - - return ( - Open file…}> - {filePath ?? title} - - } - > - - {error && {error}} - {picking ? ( - browse(parentOf(dir))} /> - ) : ( -
    {content || 'No file selected.'}
    - )} -
    -
    - ); -} - -const CODE_STYLE: React.CSSProperties = { - margin: 0, - maxHeight: '60vh', - overflow: 'auto', - fontFamily: 'Monaco, Menlo, "Courier New", monospace', - fontSize: '12px', - whiteSpace: 'pre', -}; - -function FileBrowser(props: { - readonly dir: string; - readonly entries: readonly DirEntry[]; - readonly onChoose: (entry: DirEntry) => void; - readonly onUp: () => void; -}): JSX.Element { - return ( - - /{props.dir} - {props.dir !== '' && } - {props.entries.map((entry) => ( - - ))} - - ); -} - -function parentOf(dir: string): string { - const idx = dir.lastIndexOf('/'); - return idx === -1 ? '' : dir.slice(0, idx); -} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/TemplateViewer.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/TemplateViewer.tsx new file mode 100644 index 000000000..1c2360192 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/TemplateViewer.tsx @@ -0,0 +1,169 @@ +import { isNode, isScalar, isMap, LineCounter, parseDocument, stringify } from 'yaml'; +import * as React from 'react'; +import type { TemplateResource } from '../api'; +import { CodeViewer } from './CodeViewer'; + +export interface TemplateViewerProps { + readonly jsonContent: string; + readonly resources: Record; + readonly highlightLogicalId?: string; + readonly highlightColor?: string; + readonly navCounter?: number; + readonly onResourceDoubleClick?: (logicalId: string) => void; +} + +type Format = 'yaml' | 'json'; + +interface ResourceSection { + readonly logicalId: string; + readonly startLine: number; + readonly endLine: number; +} + +export function TemplateViewer({ + jsonContent, + resources, + highlightLogicalId, + highlightColor, + navCounter, + onResourceDoubleClick, +}: TemplateViewerProps): JSX.Element { + const [format, setFormat] = React.useState('yaml'); + + const { displayContent, displayResources, sections } = React.useMemo(() => { + if (format === 'json') { + return { + displayContent: jsonContent, + displayResources: resources, + sections: buildSections(resources), + }; + } + return jsonToYaml(jsonContent); + }, [jsonContent, resources, format]); + + // Resolve the highlight from the rendered format's own ranges (JSON block in + // JSON view, YAML block in YAML view), so it is always in the coordinate + // system on screen. Derived, so toggling format re-resolves. + const highlight = React.useMemo(() => { + const block = highlightLogicalId ? displayResources[highlightLogicalId]?.block : undefined; + return block ? { start: block.startLine, end: block.endLine } : undefined; + }, [highlightLogicalId, displayResources]); + + const handleDoubleClick = React.useCallback((line: number) => { + if (!onResourceDoubleClick) return; + // Map the clicked line back to the resource that owns it, so the reverse + // jump is identity-based like the forward one. + const section = sections.find((s) => line >= s.startLine && line <= s.endLine); + if (section) { + onResourceDoubleClick(section.logicalId); + } + }, [sections, onResourceDoubleClick]); + + return ( +
    +
    + + +
    + +
    + ); +} + +function buildSections(resources: Record): ResourceSection[] { + return Object.entries(resources) + .map(([logicalId, r]) => ({ logicalId, startLine: r.block.startLine, endLine: r.block.endLine })) + .sort((a, b) => a.startLine - b.startLine); +} + +interface YamlResult { + displayContent: string; + displayResources: Record; + sections: ResourceSection[]; +} + +function jsonToYaml(jsonContent: string): YamlResult { + let parsed: unknown; + try { + parsed = JSON.parse(jsonContent); + } catch { + return { displayContent: jsonContent, displayResources: {}, sections: [] }; + } + + // Render and measure with the same parser so the text and the ranges always + // agree. lineWidth:0 disables wrapping (keeps one CFN value per line, like the + // old serializer); aliasDuplicateObjects:false keeps CloudFormation's repeated + // objects inline instead of emitting &anchor/*alias. + const displayContent = stringify(parsed, { indent: 2, lineWidth: 0, aliasDuplicateObjects: false }); + const lineCounter = new LineCounter(); + const doc = parseDocument(displayContent, { lineCounter }); + + const displayResources: Record = {}; + const resources = doc.get('Resources'); + if (isMap(resources)) { + for (const pair of resources.items) { + const key = pair.key; + const value = pair.value; + if (!isScalar(key) || !isNode(value) || key.range == null || value.range == null) { + continue; + } + // range is [valueStart, valueEnd, nodeEnd]; a resource block spans from its + // logical-id key line through the end of its value. + const startLine = lineCounter.linePos(key.range[0]).line; + const endLine = lineCounter.linePos(value.range[1]).line; + displayResources[String(key.value)] = { block: { startLine, endLine } }; + } + } + + return { displayContent, displayResources, sections: buildSections(displayResources) }; +} + +const WRAPPER_STYLE: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + height: '100%', + minHeight: 0, +}; + +const TOOLBAR_STYLE: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: '4px', + padding: '4px 0', + flexShrink: 0, +}; + +const TOGGLE_STYLE: React.CSSProperties = { + border: '1px solid #d1d5db', + borderRadius: '4px', + background: '#fafafa', + cursor: 'pointer', + fontSize: '11px', + padding: '2px 8px', + color: '#5f6b7a', + lineHeight: '16px', +}; + +const TOGGLE_ACTIVE_STYLE: React.CSSProperties = { + ...TOGGLE_STYLE, + background: '#0972d3', + color: '#ffffff', + borderColor: '#0972d3', +}; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx index b77c6e87c..06ed14ec5 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx @@ -4,18 +4,15 @@ import SpaceBetween from '@cloudscape-design/components/space-between'; import StatusIndicator from '@cloudscape-design/components/status-indicator'; import * as React from 'react'; import { displaySeverity, severityHexColor, severityRank } from '../../lib/web/severity'; -import type { WebViolation } from '../api'; +import type { WebViolation, WebViolationOccurrence } from '../api'; +import type { NavigateHandler } from '../nav-types'; interface ViolationsPanelProps { readonly violations: readonly WebViolation[]; + readonly onNavigate: NavigateHandler; } -/** - * Renders policy-validation violations like a Problems panel: sorted by - * severity, each a collapsible row led by a colored severity indicator, with - * the rule, affected-construct count, and plugin source. - */ -export function ViolationsPanel({ violations }: ViolationsPanelProps): JSX.Element { +export function ViolationsPanel({ violations, onNavigate }: ViolationsPanelProps): JSX.Element { if (violations.length === 0) { return No policy violations.; } @@ -24,14 +21,14 @@ export function ViolationsPanel({ violations }: ViolationsPanelProps): JSX.Eleme
    {sorted.map((violation, i) => ( - + ))}
    ); } -function ViolationItem({ violation }: { readonly violation: WebViolation }): JSX.Element { +function ViolationItem({ violation, onNavigate }: { readonly violation: WebViolation; readonly onNavigate: NavigateHandler }): JSX.Element { const severity = displaySeverity(violation); const count = violation.occurrences.length; return ( @@ -51,17 +48,40 @@ function ViolationItem({ violation }: { readonly violation: WebViolation }): JSX {violation.description} {violation.suggestedFix && Suggested fix: {violation.suggestedFix}} {violation.occurrences.map((occ, i) => ( - - {occ.constructPath} - {occ.logicalId ? ` → ${occ.logicalId}` : ''} - {occ.templateFile ? ` (${occ.templateFile})` : ''} - + ))} ); } +function OccurrenceRow({ occurrence, severity, onNavigate }: { + readonly occurrence: WebViolationOccurrence; + readonly severity: string; + readonly onNavigate: NavigateHandler; +}): JSX.Element { + const handleDoubleClick = React.useCallback(() => { + if (!occurrence.sourceLocation && !occurrence.templateFile) return; + onNavigate({ + sourceLocation: occurrence.sourceLocation, + templateFile: occurrence.templateFile, + logicalId: occurrence.logicalId, + propertyPaths: occurrence.propertyPaths, + color: severityHexColor(severity), + }); + }, [occurrence, severity, onNavigate]); + + return ( + + + {occurrence.constructPath} + {occurrence.logicalId ? ` → ${occurrence.logicalId}` : ''} + {occurrence.templateFile ? ` (${occurrence.templateFile})` : ''} + + + ); +} + function severityStyle(severity: string): React.CSSProperties { return { color: severityHexColor(severity), fontWeight: 700 }; } @@ -70,3 +90,4 @@ const SCROLL_STYLE: React.CSSProperties = { maxHeight: '100%', overflowY: 'auto' const HEADER_STYLE: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: '8px' }; const RULE_STYLE: React.CSSProperties = { fontWeight: 700 }; const META_STYLE: React.CSSProperties = { color: '#5f6b7a', fontWeight: 400, fontSize: '12px' }; +const OCCURRENCE_STYLE: React.CSSProperties = { cursor: 'default' }; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/highlight.css b/packages/@aws-cdk/cdk-explorer/frontend/highlight.css new file mode 100644 index 000000000..8f184542a --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/highlight.css @@ -0,0 +1,17 @@ +@keyframes nav-highlight-fade { + from { opacity: 0.35; } + to { opacity: 0; } +} + +.nav-highlight { + position: relative; +} + +.nav-highlight::before { + content: ''; + position: absolute; + inset: 0; + background-color: var(--nav-highlight-color, #0972d3); + animation: nav-highlight-fade 3s ease-out forwards; + pointer-events: none; +} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/index.tsx b/packages/@aws-cdk/cdk-explorer/frontend/index.tsx index 8f261abc4..83d6a6e82 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/index.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/index.tsx @@ -1,4 +1,5 @@ import '@cloudscape-design/global-styles/index.css'; +import './highlight.css'; import { applyMode, Mode } from '@cloudscape-design/global-styles'; import * as React from 'react'; import { createRoot } from 'react-dom/client'; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts b/packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts new file mode 100644 index 000000000..5f60095e9 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts @@ -0,0 +1,9 @@ +import type { WebSourceLocation } from './api'; + +export type NavigateHandler = (opts: { + sourceLocation?: WebSourceLocation; + templateFile?: string; + logicalId?: string; + propertyPaths?: readonly string[]; + color?: string; +}) => void; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/prismjs.d.ts b/packages/@aws-cdk/cdk-explorer/frontend/prismjs.d.ts new file mode 100644 index 000000000..18aac4244 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/prismjs.d.ts @@ -0,0 +1,9 @@ +declare module 'prismjs/components/prism-core' { + import Prism from 'prismjs'; + export default Prism; +} +declare module 'prismjs/components/prism-json' {} +declare module 'prismjs/components/prism-yaml' {} +declare module 'prismjs/components/prism-clike' {} +declare module 'prismjs/components/prism-javascript' {} +declare module 'prismjs/components/prism-typescript' {} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/syntax.ts b/packages/@aws-cdk/cdk-explorer/frontend/syntax.ts new file mode 100644 index 000000000..46b90c15a --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/frontend/syntax.ts @@ -0,0 +1,93 @@ +/** + * PrismJS-based syntax tokenizer covering the template formats (JSON, YAML) and + * every CDK source language (TypeScript, JavaScript, Python, Java, C#, Go). + * Returns structured tokens per line for our custom line renderer (which handles + * highlighting, scroll, diagnostics). + */ +import Prism from 'prismjs/components/prism-core'; +import 'prismjs/components/prism-json'; +import 'prismjs/components/prism-yaml'; +import 'prismjs/components/prism-clike'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-typescript'; +import 'prismjs/components/prism-python'; +import 'prismjs/components/prism-java'; +import 'prismjs/components/prism-csharp'; +import 'prismjs/components/prism-go'; + +/** Languages the viewer can highlight: template formats plus CDK source languages. */ +export type Language = 'json' | 'yaml' | 'typescript' | 'javascript' | 'python' | 'java' | 'csharp' | 'go'; + +export interface Token { + readonly type: string | undefined; + readonly content: string; +} + +const GRAMMAR_MAP: Record = { + json: Prism.languages.json, + yaml: Prism.languages.yaml, + typescript: Prism.languages.typescript, + javascript: Prism.languages.javascript, + python: Prism.languages.python, + java: Prism.languages.java, + csharp: Prism.languages.csharp, + go: Prism.languages.go, +}; + +export function tokenizeLines(code: string, language: Language): Token[][] { + const grammar = GRAMMAR_MAP[language]; + if (!grammar) { + return code.split('\n').map((line) => [{ type: undefined, content: line }]); + } + + const tokens = Prism.tokenize(code, grammar); + return splitIntoLines(tokens); +} + +function splitIntoLines(tokens: Array): Token[][] { + const lines: Token[][] = [[]]; + + for (const token of tokens) { + if (typeof token === 'string') { + const parts = token.split('\n'); + for (let i = 0; i < parts.length; i++) { + if (i > 0) lines.push([]); + if (parts[i]) { + lines[lines.length - 1].push({ type: undefined, content: parts[i] }); + } + } + } else { + flattenToken(token, lines); + } + } + + return lines; +} + +function flattenToken(token: Prism.Token, lines: Token[][]): void { + const type = token.type; + + if (typeof token.content === 'string') { + const parts = token.content.split('\n'); + for (let i = 0; i < parts.length; i++) { + if (i > 0) lines.push([]); + if (parts[i]) { + lines[lines.length - 1].push({ type, content: parts[i] }); + } + } + } else if (Array.isArray(token.content)) { + for (const inner of token.content) { + if (typeof inner === 'string') { + const parts = inner.split('\n'); + for (let i = 0; i < parts.length; i++) { + if (i > 0) lines.push([]); + if (parts[i]) { + lines[lines.length - 1].push({ type, content: parts[i] }); + } + } + } else { + flattenToken(inner, lines); + } + } + } +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/index.ts b/packages/@aws-cdk/cdk-explorer/lib/index.ts index 2226aca3c..1e2d38cf4 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/index.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/index.ts @@ -3,6 +3,8 @@ export const VERSION = '0.0.0'; export { readAssembly } from './core/assembly-reader'; export type { AssemblyData, AssemblyReadResult, ConstructNode } from './core/assembly-reader'; export type { SourceLocation } from './core/source-resolver'; +export { toolkitAssemblyLock } from './core/assembly-lock'; +export type { AssemblyLock, AcquireAssemblyLock } from './core/assembly-lock'; export { startLspServer } from './lsp/main'; export { startServer, createLspHandlers } from './lsp/server'; diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts index 81be6a4fa..954fce2e3 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts @@ -78,6 +78,26 @@ export interface WebConstructNode { readonly children: readonly WebConstructNode[]; } +/** Line range within a template (1-based, inclusive on both ends). */ +export interface LineRange { + readonly startLine: number; + readonly endLine: number; +} + +/** Resolved resource metadata returned by `GET /api/template`. */ +export interface TemplateResource { + /** Line range of the resource's value block `{ ... }`. */ + readonly block: LineRange; + /** User source location for the construct that owns this resource. */ + readonly source?: WebSourceLocation; +} + +/** Response for `GET /api/template?file=`. */ +export interface TemplateResponse { + readonly content: string; + readonly resources: Record; +} + /** * Response for `GET /api/tree`. `not-synthesized` means no cloud assembly was * found (the user has not run `cdk synth`) diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts index 8f6d54a13..b7706f1e4 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts @@ -1,19 +1,26 @@ import * as fs from 'fs'; import * as path from 'path'; -import { ConstructIndex } from '@aws-cdk/cloud-assembly-api'; +import { ConstructIndex, MANIFEST_FILE, resolveAllResourceRanges } from '@aws-cdk/cloud-assembly-api'; import type { PolicyValidationReportJson, ViolatingConstructJson } from '@aws-cdk/cloud-assembly-schema'; +import { ToolkitError } from '@aws-cdk/toolkit-lib'; import { type Router, type Express, type Response } from 'express'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); -import type { DirEntry, TreeResponse, ViolationsResponse, WebConstructNode, WebSourceLocation, WebViolation, WebViolationOccurrence } from './protocol'; +import type { DirEntry, LineRange, TemplateResource, TemplateResponse, TreeResponse, ViolationsResponse, WebConstructNode, WebSourceLocation, WebViolation, WebViolationOccurrence } from './protocol'; import { resolveWithinRoot } from './safe-path'; import { classifyReportSeverity, displaySeverity, severityRank } from './severity'; +import type { AcquireAssemblyLock, AssemblyLock } from '../core/assembly-lock'; import { readAssembly as defaultReadAssembly, type AssemblyData, type AssemblyReadResult, type ConstructNode } from '../core/assembly-reader'; import type { SourceLocation } from '../core/source-resolver'; /** Largest file the viewer returns inline, to avoid buffering huge artifacts into memory and the response. */ const MAX_FILE_BYTES = 2 * 1024 * 1024; +/** The read lock is fail-fast; retry this many times, this far apart, before replying 503. */ +const LOCK_RETRIES = 10; +const LOCK_RETRY_MS = 50; +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + export interface ApiOptions { /** Root of the CDK app; all file listing/reading is confined to this directory. */ readonly appDir: string; @@ -27,12 +34,67 @@ export interface ApiOptions { * `readAssembly` against {@link assemblyDir}. */ readonly readAssembly?: (assemblyDir: string) => Promise; + /** + * Acquire a read lock around each assembly read so no request observes a + * mid-synth assembly. Built from the Toolkit in {@link registerApi}'s caller. + */ + readonly acquireAssemblyLock: AcquireAssemblyLock; } export function createApiRouter(options: ApiOptions): Router { const appDir = canonicalDir(options.appDir); const assemblyDir = options.assemblyDir ?? path.join(options.appDir, 'cdk.out'); const readAssembly = options.readAssembly ?? defaultReadAssembly; + const acquireAssemblyLock = options.acquireAssemblyLock; + let cachedAssembly: { result: AssemblyReadResult; mtimeMs: number } | undefined; + + /** mtime of the assembly's manifest, or undefined when no assembly exists yet. */ + function manifestMtimeMs(): number | undefined { + try { + return fs.statSync(path.join(assemblyDir, MANIFEST_FILE)).mtimeMs; + } catch { + return undefined; + } + } + + /** + * Read the cloud assembly, memoized on the manifest's mtime. A request whose + * manifest is unchanged since the last successful read is served from cache + * without touching the lock (and, mid-synth, keeps serving the last complete + * generation rather than contending). A changed or first-seen manifest + * triggers a re-read under the assembly read lock, so a concurrent synth is + * never observed mid-write. The lock is fail-fast, so retry on writer + * contention; if it never clears, report `locked` (served as a 503). + */ + async function getCachedAssembly(): Promise { + const mtimeMs = manifestMtimeMs(); + if (mtimeMs === undefined) return { status: 'not-found' }; + if (cachedAssembly && cachedAssembly.mtimeMs === mtimeMs) return cachedAssembly.result; + + let lock: AssemblyLock | undefined; + for (let attempt = 0; lock === undefined; attempt++) { + try { + lock = await acquireAssemblyLock(assemblyDir); + } catch (err) { + if (!ToolkitError.isLockError(err)) return { status: 'error', message: (err as Error).message }; + if (attempt === LOCK_RETRIES) return { status: 'locked' }; + await delay(LOCK_RETRY_MS); + } + } + try { + // Under the read lock no synth can write, so the manifest mtime is stable + // across the read; cache the successful result against it. + const lockedMtimeMs = manifestMtimeMs(); + const result = await readAssembly(assemblyDir); + if (result.status === 'success' && lockedMtimeMs !== undefined) { + cachedAssembly = { result, mtimeMs: lockedMtimeMs }; + } + return result; + } finally { + await lock.release(); + } + } + const router = express.Router(); router.get('/health', (_req, res) => { @@ -90,7 +152,7 @@ export function createApiRouter(options: ApiOptions): Router { }); router.get('/tree', async (_req, res) => { - withAssembly(await readAssembly(assemblyDir), res, (data) => { + withAssembly(await getCachedAssembly(), res, (data) => { const severityByPath = highestSeverityByPath(data.violations); const tree = data.tree.map((node) => toWebNode(node, severityByPath, assemblyDir, appDir)); const body: TreeResponse = { status: 'ok', tree, warnings: data.warnings }; @@ -99,7 +161,7 @@ export function createApiRouter(options: ApiOptions): Router { }); router.get('/policy-validation', async (_req, res) => { - withAssembly(await readAssembly(assemblyDir), res, (data) => { + withAssembly(await getCachedAssembly(), res, (data) => { const index = ConstructIndex.fromTree(data.tree); const violations = normalizeViolations(data.violations, index, assemblyDir, appDir); const body: ViolationsResponse = { status: 'ok', violations }; @@ -107,6 +169,42 @@ export function createApiRouter(options: ApiOptions): Router { }); }); + router.get('/template', async (req, res) => { + const file = typeof req.query.file === 'string' ? req.query.file : ''; + if (!file) { + return res.status(400).json({ error: 'file query parameter is required' }); + } + const resolved = resolveWithinRoot(assemblyDir, file); + if (!resolved) { + return res.status(403).json({ error: 'path escapes assembly directory' }); + } + let content: string; + try { + const stat = fs.statSync(resolved); + if (!stat.isFile()) { + return res.status(400).json({ error: 'not a file' }); + } + if (stat.size > MAX_FILE_BYTES) { + return res.status(413).json({ error: `file exceeds ${MAX_FILE_BYTES} byte limit` }); + } + content = fs.readFileSync(resolved, 'utf-8'); + } catch { + return res.status(404).json({ error: 'template not found' }); + } + + const result = await getCachedAssembly(); + if (result.status === 'locked') { + return res.status(503).json({ error: 'synth in progress, please retry' }); + } + // On a cache hit the manifest is unchanged, so the on-disk template read + // above is the same generation as this index; on a miss the index was just + // re-read past the synth that changed it. + const index = result.status === 'success' ? ConstructIndex.fromTree(result.data.tree) : undefined; + const resources = buildTemplateResources(content, file, index, assemblyDir, appDir); + const body: TemplateResponse = { content, resources }; + return res.json(body); + }); + return router; } @@ -119,10 +217,14 @@ export function registerApi(app: Express, options: ApiOptions): void { * invoking `onReady` with the assembly data only on success. */ function withAssembly( - result: AssemblyReadResult, + result: AssemblyReadResult | { status: 'locked' }, res: Response, onReady: (data: AssemblyData) => void, ): void { + if (result.status === 'locked') { + res.status(503).json({ error: 'synth in progress, please retry' }); + return; + } if (result.status === 'not-found') { res.json({ status: 'not-synthesized' }); return; @@ -281,6 +383,95 @@ function toOccurrence( }; } +/** + * Build the resource metadata map for a template file. Each resource gets its + * block line range and the source location of the construct that owns it. + */ +function buildTemplateResources( + content: string, + templateFile: string, + index: ConstructIndex | undefined, + assemblyDir: string, + appDir: string, +): Record { + const allRanges = resolveAllResourceRanges(content); + if (!allRanges) return {}; + + const lineOffsets = computeLineOffsets(content); + const resources: Record = {}; + + for (const [logicalId, ranges] of Object.entries(allRanges)) { + const block = offsetRangeToLineRange(ranges.block, lineOffsets); + + let source: WebSourceLocation | undefined; + if (index) { + const owner = findOwnerByLogicalId(index, logicalId, templateFile, assemblyDir); + if (owner?.sourceLocation) { + source = toWebSourceLocation(owner.sourceLocation, appDir); + } + } + + resources[logicalId] = { block, ...(source && { source }) }; + } + return resources; +} + +/** + * Find the construct node that owns a given logicalId within a specific + * template file. Logical IDs are only unique per template, so we match on both. + */ +function findOwnerByLogicalId( + index: ConstructIndex, + logicalId: string, + templateFile: string, + assemblyDir: string, +): ConstructNode | undefined { + for (const node of index) { + if (node.logicalId === logicalId && node.templateFile) { + const relTemplate = toPosix(path.relative(assemblyDir, node.templateFile)); + if (relTemplate === templateFile) return node; + } + } + return undefined; +} + +/** + * Compute 0-based byte offsets for the start of each line. Line 1 starts at + * offset 0. Used for fast offset-to-line conversion. + */ +function computeLineOffsets(text: string): number[] { + const offsets = [0]; + for (let i = 0; i < text.length; i++) { + if (text[i] === '\n') { + offsets.push(i + 1); + } + } + return offsets; +} + +/** Convert a character offset to a 1-based line number using precomputed line offsets. */ +function offsetToLine(offset: number, lineOffsets: number[]): number { + let lo = 0; + let hi = lineOffsets.length - 1; + while (lo < hi) { + const mid = Math.floor((lo + hi + 1) / 2); + if (lineOffsets[mid] <= offset) { + lo = mid; + } else { + hi = mid - 1; + } + } + return lo + 1; +} + +/** Convert an OffsetRange to a 1-based inclusive LineRange. */ +function offsetRangeToLineRange(range: { start: number; end: number }, lineOffsets: number[]): LineRange { + return { + startLine: offsetToLine(range.start, lineOffsets), + endLine: offsetToLine(Math.max(range.start, range.end - 1), lineOffsets), + }; +} + function listDir(appDir: string, dir: string): DirEntry[] { return fs.readdirSync(dir, { withFileTypes: true }) .map((entry): DirEntry => ({ diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts index 3627224e8..294b0b7bd 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts @@ -1,11 +1,13 @@ import * as http from 'http'; import * as path from 'path'; +import { Toolkit, NonInteractiveIoHost } from '@aws-cdk/toolkit-lib'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); import { SseBroadcaster } from './events'; import { ASSEMBLY_CHANGED } from './protocol'; import { registerApi } from './routes'; import { indexHtml, webAsset } from './web-assets'; +import { toolkitAssemblyLock } from '../core/assembly-lock'; import { startAssemblyWatcher as defaultStartAssemblyWatcher, type AssemblyWatcher, @@ -61,7 +63,15 @@ export async function startWebServer(options: WebServerOptions = {}): Promise { + const byFile = new Map(); + + const walk = (nodes: readonly WebConstructNode[]): void => { + for (const node of nodes) { + const loc = node.sourceLocation; + if (loc && node.templateFile && node.logicalId) { + const anchors = byFile.get(loc.file); + if (anchors) { + anchors.push({ line: loc.line, node }); + } else { + byFile.set(loc.file, [{ line: loc.line, node }]); + } + } + walk(node.children); + } + }; + walk(tree); + + for (const anchors of byFile.values()) { + anchors.sort((a, b) => a.line - b.line); + } + return byFile; +} + +/** + * Resolve which construct a source line belongs to: the one whose definition is + * nearest at or above `line`. Source anchors are single lines (a construct's + * creation line), not ranges, so a construct is treated as owning every line + * from its definition down to the next construct's. Returns `undefined` when + * `line` sits above the first construct in the file, or when there are no + * anchors. + * + * When several constructs share the nearest line (a parent and its synthesized + * children are all anchored to the single `new Xyz(...)` line), the top-most one + * wins, since that is the construct the user actually authored on that line. The + * depth-first pre-order of {@link buildSourceAnchorIndex} places it first, so on + * a tie we keep the earliest match rather than overwriting it. + * + * `anchors` must be sorted by line ascending, as produced by + * {@link buildSourceAnchorIndex}. + */ +export function findConstructAtLine( + anchors: readonly SourceAnchor[] | undefined, + line: number, +): WebConstructNode | undefined { + if (!anchors) { + return undefined; + } + let match: WebConstructNode | undefined; + let matchLine = -Infinity; + for (const anchor of anchors) { + if (anchor.line > line) { + break; + } + // Anchors are line-ascending, so a strictly greater line is a nearer owner; + // an equal line is a same-line child, which must not displace its parent. + if (anchor.line > matchLine) { + match = anchor.node; + matchLine = anchor.line; + } + } + return match; +} diff --git a/packages/@aws-cdk/cdk-explorer/package.json b/packages/@aws-cdk/cdk-explorer/package.json index f12ba7d6b..6dde7e3f3 100644 --- a/packages/@aws-cdk/cdk-explorer/package.json +++ b/packages/@aws-cdk/cdk-explorer/package.json @@ -38,6 +38,7 @@ "@types/express": "^4", "@types/jest": "^29.5.14", "@types/node": "^20", + "@types/prismjs": "^1", "@types/react": "^18", "@types/react-dom": "^18", "@types/supertest": "^6", @@ -56,6 +57,7 @@ "jest-junit": "^16", "nx": "^22.7.5", "prettier": "^2.8", + "prismjs": "^1", "projen": "^0.99.73", "react": "^18", "react-dom": "^18", @@ -63,7 +65,8 @@ "ts-jest": "^29.4.11", "tsx": "^4.22.4", "typescript": "5.9", - "vscode-languageserver-protocol": "^3" + "vscode-languageserver-protocol": "^3", + "yaml": "^2" }, "dependencies": { "@aws-cdk/cloud-assembly-api": "^0.0.0", diff --git a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts index 944126812..97fc3379c 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import type { PolicyValidationReportJson } from '@aws-cdk/cloud-assembly-schema'; +import { LockError } from '@aws-cdk/toolkit-lib'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -12,6 +13,15 @@ import { createApiRouter } from '../../lib/web/routes'; let appDir: string; let app: express.Express; +/** No-op assembly lock; these tests inject readAssembly directly, so no real lock is exercised. */ +const noopAssemblyLock = async () => ({ release: () => Promise.resolve() }); + +/** Writes a minimal manifest so the router's mtime check reaches the injected readAssembly. */ +function writeManifest(dir: string): void { + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, 'manifest.json'), '{"version":"36.0.0"}'); +} + beforeEach(() => { appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-explorer-routes-')); fs.writeFileSync(path.join(appDir, 'app.ts'), 'export const x = 1;\n'); @@ -19,7 +29,7 @@ beforeEach(() => { fs.writeFileSync(path.join(appDir, 'lib', 'stack.ts'), 'class Stack {}\n'); app = express(); - app.use('/api', createApiRouter({ appDir })); + app.use('/api', createApiRouter({ appDir, acquireAssemblyLock: noopAssemblyLock })); }); afterEach(() => { @@ -120,8 +130,10 @@ describe('GET /api/tree', () => { readAssembly: (dir: string) => Promise, opts: { assemblyDir?: string } = {}, ): express.Express { + const assemblyDir = opts.assemblyDir ?? path.join(appDir, 'cdk.out'); + writeManifest(assemblyDir); const a = express(); - a.use('/api', createApiRouter({ appDir, assemblyDir: opts.assemblyDir, readAssembly })); + a.use('/api', createApiRouter({ appDir, assemblyDir, readAssembly, acquireAssemblyLock: noopAssemblyLock })); return a; } @@ -182,15 +194,97 @@ describe('GET /api/tree', () => { seen = dir; return { status: 'not-found' }; }; - await request(appWith(reader, { assemblyDir: '/custom/out' })).get('/api/tree'); - expect(seen).toBe('/custom/out'); + const override = path.join(appDir, 'custom-out'); + await request(appWith(reader, { assemblyDir: override })).get('/api/tree'); + expect(seen).toBe(override); + }); +}); + +describe('GET /api/template', () => { + const TEMPLATE_OBJ = { + Resources: { + Bucket123: { + Type: 'AWS::S3::Bucket', + Properties: { BucketName: 'my-bucket', VersioningConfiguration: { Status: 'Enabled' } }, + }, + Topic456: { Type: 'AWS::SNS::Topic' }, + }, + }; + const TEMPLATE_TEXT = JSON.stringify(TEMPLATE_OBJ, undefined, 1); + + function appWith( + readAssembly: (dir: string) => Promise, + opts: { assemblyDir?: string } = {}, + ): express.Express { + const assemblyDir = opts.assemblyDir ?? path.join(appDir, 'cdk.out'); + writeManifest(assemblyDir); + fs.writeFileSync(path.join(assemblyDir, 'MyStack.template.json'), TEMPLATE_TEXT); + const a = express(); + a.use('/api', createApiRouter({ appDir, assemblyDir, readAssembly, acquireAssemblyLock: noopAssemblyLock })); + return a; + } + + test('returns template content and resource line ranges', async () => { + const realAppDir = fs.realpathSync(appDir); + const assemblyDir = path.join(appDir, 'cdk.out'); + const reader = async (dir: string): Promise => { + const node: ConstructNode = { + path: 'MyStack/Bucket', + id: 'Bucket', + type: 'AWS::S3::Bucket', + logicalId: 'Bucket123', + templateFile: path.join(dir, 'MyStack.template.json'), + sourceLocation: { file: path.join(realAppDir, 'lib', 'stack.ts'), line: 10, column: 5 }, + children: [], + }; + return { status: 'success', data: { tree: [node], warnings: [] } }; + }; + + const res = await request(appWith(reader)).get('/api/template').query({ file: 'MyStack.template.json' }); + expect(res.status).toBe(200); + expect(res.body.content).toBe(TEMPLATE_TEXT); + expect(res.body.resources.Bucket123).toBeDefined(); + expect(res.body.resources.Bucket123.block.startLine).toBeGreaterThan(0); + expect(res.body.resources.Bucket123.block.endLine).toBeGreaterThanOrEqual(res.body.resources.Bucket123.block.startLine); + expect(res.body.resources.Bucket123.source).toEqual({ file: 'lib/stack.ts', line: 10, column: 5 }); + expect(res.body.resources.Topic456).toBeDefined(); + expect(res.body.resources.Topic456.source).toBeUndefined(); + }); + + test('requires a file parameter', async () => { + const reader = async (): Promise => ({ status: 'not-found' }); + const res = await request(appWith(reader)).get('/api/template'); + expect(res.status).toBe(400); + }); + + test('rejects path traversal with 403', async () => { + const reader = async (): Promise => ({ status: 'not-found' }); + const res = await request(appWith(reader)).get('/api/template').query({ file: '../../etc/passwd' }); + expect(res.status).toBe(403); + }); + + test('returns 404 for a missing template', async () => { + const reader = async (): Promise => ({ status: 'not-found' }); + const res = await request(appWith(reader)).get('/api/template').query({ file: 'nope.template.json' }); + expect(res.status).toBe(404); + }); + + test('works without a construct tree (assembly not found)', async () => { + const reader = async (): Promise => ({ status: 'not-found' }); + const res = await request(appWith(reader)).get('/api/template').query({ file: 'MyStack.template.json' }); + expect(res.status).toBe(200); + expect(res.body.content).toBe(TEMPLATE_TEXT); + expect(res.body.resources.Bucket123).toBeDefined(); + expect(res.body.resources.Bucket123.source).toBeUndefined(); }); }); describe('GET /api/policy-validation', () => { function appWith(readAssembly: (dir: string) => Promise): express.Express { + const assemblyDir = path.join(appDir, 'cdk.out'); + writeManifest(assemblyDir); const a = express(); - a.use('/api', createApiRouter({ appDir, readAssembly })); + a.use('/api', createApiRouter({ appDir, assemblyDir, readAssembly, acquireAssemblyLock: noopAssemblyLock })); return a; } @@ -255,3 +349,84 @@ describe('GET /api/policy-validation', () => { expect(res.body).toEqual({ error: 'boom' }); }); }); + +describe('assembly read lock', () => { + function appWithLock(acquireAssemblyLock: () => Promise<{ release: () => Promise }>): express.Express { + const assemblyDir = path.join(appDir, 'cdk.out'); + writeManifest(assemblyDir); + const a = express(); + a.use('/api', createApiRouter({ + appDir, + assemblyDir, + readAssembly: () => ({ status: 'not-found' }), + acquireAssemblyLock, + })); + return a; + } + + test('acquires the lock and releases it after reading', async () => { + const release = jest.fn(() => Promise.resolve()); + const acquireAssemblyLock = jest.fn(async () => ({ release })); + const res = await request(appWithLock(acquireAssemblyLock)).get('/api/tree'); + expect(res.status).toBe(200); + expect(acquireAssemblyLock).toHaveBeenCalledWith(path.join(appDir, 'cdk.out')); + expect(release).toHaveBeenCalledTimes(1); + }); + + test('retries while a synth holds the write lock, then reads once it clears', async () => { + let calls = 0; + const acquireAssemblyLock = jest.fn(async () => { + calls += 1; + if (calls <= 2) throw new LockError('ConcurrentWriteLock', 'a synth is writing'); + return { release: () => Promise.resolve() }; + }); + const res = await request(appWithLock(acquireAssemblyLock)).get('/api/tree'); + expect(res.status).toBe(200); + expect(acquireAssemblyLock).toHaveBeenCalledTimes(3); // 2 contended + 1 success + }); + + test('returns 503 when the write lock never clears', async () => { + const acquireAssemblyLock = jest.fn(async () => { + throw new LockError('ConcurrentWriteLock', 'a synth is writing'); + }); + const res = await request(appWithLock(acquireAssemblyLock)).get('/api/tree'); + expect(res.status).toBe(503); + expect(res.body).toEqual({ error: 'synth in progress, please retry' }); + }); + + test('returns 500 when the lock fails for a non-contention reason', async () => { + const acquireAssemblyLock = jest.fn(async () => { + throw new Error('corrupt manifest'); + }); + const res = await request(appWithLock(acquireAssemblyLock)).get('/api/tree'); + expect(res.status).toBe(500); + expect(res.body).toEqual({ error: 'corrupt manifest' }); + }); + + test('serves a cached read without re-locking while the manifest mtime is unchanged', async () => { + const assemblyDir = path.join(appDir, 'cdk.out'); + writeManifest(assemblyDir); + const acquireAssemblyLock = jest.fn(async () => ({ release: () => Promise.resolve() })); + const readAssembly = jest.fn((): AssemblyReadResult => ({ status: 'success', data: { tree: [], warnings: [] } })); + const a = express(); + a.use('/api', createApiRouter({ appDir, assemblyDir, readAssembly, acquireAssemblyLock })); + await request(a).get('/api/tree'); + await request(a).get('/api/tree'); + expect(readAssembly).toHaveBeenCalledTimes(1); // second request served from cache + expect(acquireAssemblyLock).toHaveBeenCalledTimes(1); // no lock taken on a cache hit + }); + + test('re-reads under the lock when the manifest mtime changes', async () => { + const assemblyDir = path.join(appDir, 'cdk.out'); + writeManifest(assemblyDir); + const acquireAssemblyLock = jest.fn(async () => ({ release: () => Promise.resolve() })); + const readAssembly = jest.fn((): AssemblyReadResult => ({ status: 'success', data: { tree: [], warnings: [] } })); + const a = express(); + a.use('/api', createApiRouter({ appDir, assemblyDir, readAssembly, acquireAssemblyLock })); + await request(a).get('/api/tree'); + const future = new Date(Date.now() + 5000); + fs.utimesSync(path.join(assemblyDir, 'manifest.json'), future, future); + await request(a).get('/api/tree'); + expect(readAssembly).toHaveBeenCalledTimes(2); // mtime changed, so re-read + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/source-nav.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/source-nav.test.ts new file mode 100644 index 000000000..b3162c8c6 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/source-nav.test.ts @@ -0,0 +1,113 @@ +import type { WebConstructNode } from '../../lib/web/protocol'; +import { buildSourceAnchorIndex, findConstructAtLine } from '../../lib/web/source-nav'; + +function node(props: { + path: string; + logicalId?: string; + templateFile?: string; + file?: string; + line?: number; + children?: WebConstructNode[]; +}): WebConstructNode { + return { + path: props.path, + id: props.path.split('/').pop()!, + logicalId: props.logicalId, + templateFile: props.templateFile, + sourceLocation: props.file !== undefined ? { file: props.file, line: props.line ?? 0, column: 0 } : undefined, + children: props.children ?? [], + }; +} + +/** A fully-navigable construct: has source location + template coordinates. */ +function navigable(path: string, file: string, line: number, children?: WebConstructNode[]): WebConstructNode { + return node({ path, logicalId: path.replace(/\W/g, ''), templateFile: 'App.template.json', file, line, children }); +} + +describe('buildSourceAnchorIndex', () => { + test('returns an empty map for an empty tree', () => { + expect(buildSourceAnchorIndex([]).size).toBe(0); + }); + + test('indexes navigable constructs grouped by file', () => { + const index = buildSourceAnchorIndex([ + navigable('App/Bucket', 'app.ts', 10), + navigable('App/Queue', 'app.ts', 20), + navigable('App/Nested/Table', 'db.ts', 5), + ]); + + expect([...index.keys()].sort()).toEqual(['app.ts', 'db.ts']); + expect(index.get('app.ts')!.map((a) => a.node.path)).toEqual(['App/Bucket', 'App/Queue']); + expect(index.get('db.ts')!.map((a) => a.node.path)).toEqual(['App/Nested/Table']); + }); + + test('walks children depth-first and sorts each file by line ascending', () => { + const index = buildSourceAnchorIndex([ + navigable('App/Outer', 'app.ts', 30, [ + navigable('App/Outer/Inner', 'app.ts', 12), + ]), + navigable('App/First', 'app.ts', 4), + ]); + + expect(index.get('app.ts')!.map((a) => a.line)).toEqual([4, 12, 30]); + }); + + test('excludes constructs missing a source location, templateFile, or logicalId', () => { + const index = buildSourceAnchorIndex([ + node({ path: 'App/NoSource', logicalId: 'X', templateFile: 'App.template.json' }), + node({ path: 'App/NoTemplate', logicalId: 'Y', file: 'app.ts', line: 1 }), + node({ path: 'App/NoLogicalId', templateFile: 'App.template.json', file: 'app.ts', line: 2 }), + navigable('App/Ok', 'app.ts', 3), + ]); + + expect([...index.keys()]).toEqual(['app.ts']); + expect(index.get('app.ts')!.map((a) => a.node.path)).toEqual(['App/Ok']); + }); +}); + +describe('findConstructAtLine', () => { + const anchors = buildSourceAnchorIndex([ + navigable('App/Bucket', 'app.ts', 10), + navigable('App/Queue', 'app.ts', 20), + ]).get('app.ts'); + + test('returns undefined when anchors are undefined', () => { + expect(findConstructAtLine(undefined, 10)).toBeUndefined(); + }); + + test('returns undefined for a line above the first construct', () => { + expect(findConstructAtLine(anchors, 3)).toBeUndefined(); + }); + + test('matches the construct on its exact definition line', () => { + expect(findConstructAtLine(anchors, 10)!.path).toBe('App/Bucket'); + expect(findConstructAtLine(anchors, 20)!.path).toBe('App/Queue'); + }); + + test('matches the nearest preceding construct for a line inside its block', () => { + expect(findConstructAtLine(anchors, 15)!.path).toBe('App/Bucket'); + }); + + test('matches the last construct for a line below all definitions', () => { + expect(findConstructAtLine(anchors, 999)!.path).toBe('App/Queue'); + }); + + test('returns undefined for an empty anchor list', () => { + expect(findConstructAtLine([], 10)).toBeUndefined(); + }); + + test('prefers the top-most construct when several share the nearest line', () => { + // Mirrors the real VPC case: a parent and its synthesized children are all + // anchored to the single `new Vpc(...)` line. Double-clicking that line + // should land on the authored parent, not a deep synthetic child. + const shared = buildSourceAnchorIndex([ + navigable('Net/Vpc', 'net.ts', 11, [ + navigable('Net/Vpc/PublicSubnet1', 'net.ts', 11), + navigable('Net/Vpc/PublicSubnet1/NatGateway', 'net.ts', 11), + ]), + ]).get('net.ts'); + + expect(findConstructAtLine(shared, 11)!.path).toBe('Net/Vpc'); + expect(findConstructAtLine(shared, 40)!.path).toBe('Net/Vpc'); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json index ed785310b..94a4cd46a 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.dev.json @@ -29,6 +29,7 @@ "express", "jest", "node", + "prismjs", "react-dom", "react", "supertest" diff --git a/packages/@aws-cdk/cdk-explorer/tsconfig.json b/packages/@aws-cdk/cdk-explorer/tsconfig.json index 54247ff97..ac6b34f5a 100644 --- a/packages/@aws-cdk/cdk-explorer/tsconfig.json +++ b/packages/@aws-cdk/cdk-explorer/tsconfig.json @@ -31,6 +31,7 @@ "express", "jest", "node", + "prismjs", "react-dom", "react", "supertest" diff --git a/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts b/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts index 730c9c162..dbd9e117f 100644 --- a/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts +++ b/packages/@aws-cdk/cloud-assembly-api/lib/template-ranges.ts @@ -99,6 +99,48 @@ function propertyRanges(pointers: Pointers, escapedId: string): Record | undefined { + const pointers = parsePointers(templateText); + if (pointers === undefined) { + return undefined; + } + + const result: Record = {}; + const resourceProperties = new Map>(); + + for (const [pointer, mapping] of Object.entries(pointers)) { + const resourceMatch = /^\/Resources\/([^/]+)$/.exec(pointer); + if (resourceMatch) { + const id = unescapePointerSegment(resourceMatch[1]); + result[id] = { block: { start: mapping.value.pos, end: mapping.valueEnd.pos }, properties: {} }; + continue; + } + const propMatch = /^\/Resources\/([^/]+)\/Properties\/([^/]+)$/.exec(pointer); + if (propMatch && mapping.key !== undefined) { + const id = unescapePointerSegment(propMatch[1]); + let props = resourceProperties.get(id); + if (!props) { + props = {}; + resourceProperties.set(id, props); + } + props[unescapePointerSegment(propMatch[2])] = { start: mapping.key.pos, end: mapping.valueEnd.pos }; + } + } + + for (const [id, props] of resourceProperties) { + if (result[id]) { + result[id] = { block: result[id].block, properties: props }; + } + } + + return result; +} + /** * The inverse of `resolveResourceRange`: given a character offset into a * template's text, returns the logical id of the resource whose block contains diff --git a/yarn.lock b/yarn.lock index 843776f13..33b19f979 100644 --- a/yarn.lock +++ b/yarn.lock @@ -175,6 +175,7 @@ __metadata: "@types/express": "npm:^4" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^20" + "@types/prismjs": "npm:^1" "@types/react": "npm:^18" "@types/react-dom": "npm:^18" "@types/supertest": "npm:^6" @@ -196,6 +197,7 @@ __metadata: jest-junit: "npm:^16" nx: "npm:^22.7.5" prettier: "npm:^2.8" + prismjs: "npm:^1" projen: "npm:^0.99.73" react: "npm:^18" react-dom: "npm:^18" @@ -207,6 +209,7 @@ __metadata: vscode-languageserver: "npm:^9" vscode-languageserver-protocol: "npm:^3" vscode-languageserver-textdocument: "npm:^1" + yaml: "npm:^2" languageName: unknown linkType: soft @@ -7205,6 +7208,13 @@ __metadata: languageName: node linkType: hard +"@types/prismjs@npm:^1": + version: 1.26.6 + resolution: "@types/prismjs@npm:1.26.6" + checksum: 10c0/152a27500cb32b114edfb77f9d0dccd03bebc84828d1e92abacaf212b22d3ccdde041ce421dd58b6ec8461bbec7cd76ed5ee773cae4be7ca36a6dd4ddcf0f9e7 + languageName: node + linkType: hard + "@types/promptly@npm:^3.0.5": version: 3.0.5 resolution: "@types/promptly@npm:3.0.5" @@ -16380,6 +16390,13 @@ __metadata: languageName: node linkType: hard +"prismjs@npm:^1": + version: 1.30.0 + resolution: "prismjs@npm:1.30.0" + checksum: 10c0/f56205bfd58ef71ccfcbcb691fd0eb84adc96c6ff21b0b69fc6fdcf02be42d6ef972ba4aed60466310de3d67733f6a746f89f2fb79c00bf217406d465b3e8f23 + languageName: node + linkType: hard + "proc-log@npm:^6.0.0, proc-log@npm:^6.1.0": version: 6.1.0 resolution: "proc-log@npm:6.1.0" @@ -19418,7 +19435,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:2.9.0, yaml@npm:^2.9.0": +"yaml@npm:2.9.0, yaml@npm:^2, yaml@npm:^2.9.0": version: 2.9.0 resolution: "yaml@npm:2.9.0" bin: From 9df5ca1a139fc584c8279ef79aa0e084e90d4035 Mon Sep 17 00:00:00 2001 From: megha-narayanan Date: Thu, 16 Jul 2026 13:23:27 -0400 Subject: [PATCH 26/28] chore(aws-cdk): regenerate THIRD_PARTY_LICENSES The bundled dependency licenses were out of date after cdk-explorer dependencies were added. --- packages/aws-cdk/THIRD_PARTY_LICENSES | 2862 ++++++++++++++++++++----- 1 file changed, 2291 insertions(+), 571 deletions(-) diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index be879de73..dd9318dc1 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -10437,6 +10437,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** accepts@1.3.8 - https://www.npmjs.com/package/accepts/v/1.3.8 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** agent-base@7.1.4 - https://www.npmjs.com/package/agent-base/v/7.1.4 | MIT @@ -10544,6 +10572,32 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** array-flatten@1.1.1 - https://www.npmjs.com/package/array-flatten/v/1.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** ast-types@0.13.4 - https://www.npmjs.com/package/ast-types/v/0.13.4 | MIT @@ -10606,6 +10660,34 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** body-parser@1.20.4 - https://www.npmjs.com/package/body-parser/v/1.20.4 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** bowser@2.14.1 - https://www.npmjs.com/package/bowser/v/2.14.1 | MIT @@ -10700,6 +10782,86 @@ FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TOR ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** bytes@3.1.2 - https://www.npmjs.com/package/bytes/v/3.1.2 | MIT +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** call-bind-apply-helpers@1.0.2 - https://www.npmjs.com/package/call-bind-apply-helpers/v/1.0.2 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** call-bound@1.0.4 - https://www.npmjs.com/package/call-bound/v/1.0.4 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** camelcase@5.3.1 - https://www.npmjs.com/package/camelcase/v/5.3.1 | MIT @@ -10803,6 +10965,60 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** content-disposition@0.5.4 - https://www.npmjs.com/package/content-disposition/v/0.5.4 | MIT +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** content-type@1.0.5 - https://www.npmjs.com/package/content-type/v/1.0.5 | MIT +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** convert-source-map@2.0.0 - https://www.npmjs.com/package/convert-source-map/v/2.0.0 | MIT @@ -10831,6 +11047,39 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** cookie-signature@1.0.7 - https://www.npmjs.com/package/cookie-signature/v/1.0.7 | MIT + +---------------- + +** cookie@0.7.2 - https://www.npmjs.com/package/cookie/v/0.7.2 | MIT +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + ---------------- ** data-uri-to-buffer@6.0.2 - https://www.npmjs.com/package/data-uri-to-buffer/v/6.0.2 | MIT @@ -10857,6 +11106,30 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** debug@2.6.9 - https://www.npmjs.com/package/debug/v/2.6.9 | MIT +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + ---------------- ** debug@4.4.3 - https://www.npmjs.com/package/debug/v/4.4.3 | MIT @@ -10928,12 +11201,14 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT -Copyright Mathias Bynens +** depd@2.0.0 - https://www.npmjs.com/package/depd/v/2.0.0 | MIT +(The MIT License) + +Copyright (c) 2014-2018 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -10942,21 +11217,23 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** destroy@1.2.0 - https://www.npmjs.com/package/destroy/v/1.2.0 | MIT -** enquirer@2.4.1 - https://www.npmjs.com/package/enquirer/v/2.4.1 | MIT The MIT License (MIT) -Copyright (c) 2016-present, Jon Schlinkert. +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -10979,110 +11256,115 @@ THE SOFTWARE. ---------------- -** escodegen@2.1.0 - https://www.npmjs.com/package/escodegen/v/2.1.0 | BSD-2-Clause -Copyright (C) 2012 Yusuke Suzuki (twitter: @Constellation) and other contributors. +** dunder-proto@1.0.1 - https://www.npmjs.com/package/dunder-proto/v/1.0.1 | MIT +MIT License -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Copyright (c) 2024 ECMAScript Shims - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** esprima@4.0.1 - https://www.npmjs.com/package/esprima/v/4.0.1 | BSD-2-Clause -Copyright JS Foundation and other contributors, https://js.foundation/ +** ee-first@1.1.1 - https://www.npmjs.com/package/ee-first/v/1.1.1 | MIT -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +The MIT License (MIT) - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +Copyright (c) 2014 Jonathan Ong me@jongleberry.com -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** estraverse@5.3.0 - https://www.npmjs.com/package/estraverse/v/5.3.0 | BSD-2-Clause -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +** emoji-regex@8.0.0 - https://www.npmjs.com/package/emoji-regex/v/8.0.0 | MIT +Copyright Mathias Bynens - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** esutils@2.0.3 - https://www.npmjs.com/package/esutils/v/2.0.3 | BSD-2-Clause -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +** encodeurl@2.0.0 - https://www.npmjs.com/package/encodeurl/v/2.0.0 | MIT +(The MIT License) - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. +Copyright (c) 2016 Douglas Christopher Wilson -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** eventemitter3@4.0.7 - https://www.npmjs.com/package/eventemitter3/v/4.0.7 | MIT +** enquirer@2.4.1 - https://www.npmjs.com/package/enquirer/v/2.4.1 | MIT The MIT License (MIT) -Copyright (c) 2014 Arnout Kazemier +Copyright (c) 2016-present, Jon Schlinkert. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11091,24 +11373,24 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT +** es-define-property@1.0.1 - https://www.npmjs.com/package/es-define-property/v/1.0.1 | MIT MIT License -Copyright (c) 2017 Evgeny Poberezkin +Copyright (c) 2024 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11131,10 +11413,10 @@ SOFTWARE. ---------------- -** fast-glob@3.3.3 - https://www.npmjs.com/package/fast-glob/v/3.3.3 | MIT -The MIT License (MIT) +** es-errors@1.3.0 - https://www.npmjs.com/package/es-errors/v/1.3.0 | MIT +MIT License -Copyright (c) Denis Malinochkin +Copyright (c) 2024 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11157,28 +11439,10 @@ SOFTWARE. ---------------- -** fastq@1.20.1 - https://www.npmjs.com/package/fastq/v/1.20.1 | ISC -Copyright (c) 2015-2020, Matteo Collina - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ----------------- - -** fill-range@7.1.1 - https://www.npmjs.com/package/fill-range/v/7.1.1 | MIT -The MIT License (MIT) +** es-object-atoms@1.1.1 - https://www.npmjs.com/package/es-object-atoms/v/1.1.1 | MIT +MIT License -Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) 2024 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11187,62 +11451,26 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - ----------------- - -** find-up@4.1.0 - https://www.npmjs.com/package/find-up/v/4.1.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** fs-extra@11.3.5 - https://www.npmjs.com/package/fs-extra/v/11.3.5 | MIT -(The MIT License) - -Copyright (c) 2011-2024 JP Richardson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. -** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC ---------------- -** get-uri@6.0.5 - https://www.npmjs.com/package/get-uri/v/6.0.5 | MIT +** escape-html@1.0.3 - https://www.npmjs.com/package/escape-html/v/1.0.3 | MIT (The MIT License) -Copyright (c) 2014 Nathan Rajlich +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -11263,66 +11491,113 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ---------------- -** glob-parent@5.1.2 - https://www.npmjs.com/package/glob-parent/v/5.1.2 | ISC -The ISC License +** escodegen@2.1.0 - https://www.npmjs.com/package/escodegen/v/2.1.0 | BSD-2-Clause +Copyright (C) 2012 Yusuke Suzuki (twitter: @Constellation) and other contributors. -Copyright (c) 2015, 2019 Elan Shanker +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------- -** graceful-fs@4.2.11 - https://www.npmjs.com/package/graceful-fs/v/4.2.11 | ISC -The ISC License +** esprima@4.0.1 - https://www.npmjs.com/package/esprima/v/4.0.1 | BSD-2-Clause +Copyright JS Foundation and other contributors, https://js.foundation/ -Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------- -** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT -MIT License +** estraverse@5.3.0 - https://www.npmjs.com/package/estraverse/v/5.3.0 | BSD-2-Clause +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -Copyright (c) Sindre Sorhus (sindresorhus.com) + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** esutils@2.0.3 - https://www.npmjs.com/package/esutils/v/2.0.3 | BSD-2-Clause +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------- -** http-proxy-agent@7.0.2 - https://www.npmjs.com/package/http-proxy-agent/v/7.0.2 | MIT +** etag@1.8.1 - https://www.npmjs.com/package/etag/v/1.8.1 | MIT (The MIT License) -Copyright (c) 2013 Nathan Rajlich +Copyright (c) 2014-2016 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -11346,10 +11621,38 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** https-proxy-agent@7.0.6 - https://www.npmjs.com/package/https-proxy-agent/v/7.0.6 | MIT +** eventemitter3@4.0.7 - https://www.npmjs.com/package/eventemitter3/v/4.0.7 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** express@4.22.1 - https://www.npmjs.com/package/express/v/4.22.1 | MIT (The MIT License) -Copyright (c) 2013 Nathan Rajlich +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -11370,10 +11673,13 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ---------------- -** ip-address@10.2.0 - https://www.npmjs.com/package/ip-address/v/10.2.0 | MIT -Copyright (C) 2011 by Beau Gunderson +** fast-deep-equal@3.1.3 - https://www.npmjs.com/package/fast-deep-equal/v/3.1.3 | MIT +MIT License + +Copyright (c) 2017 Evgeny Poberezkin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11382,24 +11688,24 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** is-extglob@2.1.1 - https://www.npmjs.com/package/is-extglob/v/2.1.1 | MIT +** fast-glob@3.3.3 - https://www.npmjs.com/package/fast-glob/v/3.3.3 | MIT The MIT License (MIT) -Copyright (c) 2014-2016, Jon Schlinkert +Copyright (c) Denis Malinochkin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11408,38 +11714,42 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** is-fullwidth-code-point@3.0.0 - https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +** fastq@1.20.1 - https://www.npmjs.com/package/fastq/v/1.20.1 | ISC +Copyright (c) 2015-2020, Matteo Collina -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** is-glob@4.0.3 - https://www.npmjs.com/package/is-glob/v/4.0.3 | MIT +** fill-range@7.1.1 - https://www.npmjs.com/package/fill-range/v/7.1.1 | MIT The MIT License (MIT) -Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2014-present, Jon Schlinkert. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11462,136 +11772,83 @@ THE SOFTWARE. ---------------- -** is-number@7.0.0 - https://www.npmjs.com/package/is-number/v/7.0.0 | MIT -The MIT License (MIT) +** finalhandler@1.3.2 - https://www.npmjs.com/package/finalhandler/v/1.3.2 | MIT +(The MIT License) -Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) 2014-2022 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** json-source-map@0.6.1 - https://www.npmjs.com/package/json-source-map/v/0.6.1 | MIT +** find-up@4.1.0 - https://www.npmjs.com/package/find-up/v/4.1.0 | MIT MIT License -Copyright (c) 2017 Evgeny Poberezkin +Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** jsonfile@6.2.0 - https://www.npmjs.com/package/jsonfile/v/6.2.0 | MIT +** forwarded@0.2.0 - https://www.npmjs.com/package/forwarded/v/0.2.0 | MIT (The MIT License) -Copyright (c) 2012-2015, JP Richardson +Copyright (c) 2014-2017 Douglas Christopher Wilson -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, - merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ----------------- - -** jsonschema@1.5.0 - https://www.npmjs.com/package/jsonschema/v/1.5.0 | MIT -jsonschema is licensed under MIT license. - -Copyright (C) 2012-2015 Tom de Grunt - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ----------------- - -** locate-path@5.0.0 - https://www.npmjs.com/package/locate-path/v/5.0.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** lodash.truncate@4.4.2 - https://www.npmjs.com/package/lodash.truncate/v/4.4.2 | MIT -Copyright jQuery Foundation and other contributors - -Based on Underscore.js, copyright Jeremy Ashkenas, -DocumentCloud and Investigative Reporters & Editors - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/lodash/lodash - -The following license applies to all parts of this software except as -documented below: +** fresh@0.5.2 - https://www.npmjs.com/package/fresh/v/0.5.2 | MIT +(The MIT License) -==== +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -11600,56 +11857,39 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -==== - -Copyright and related rights for sample code are waived via CC0. Sample -code is defined as all source code displayed within the prose of the -documentation. - -CC0: http://creativecommons.org/publicdomain/zero/1.0/ - -==== - -Files located in the node_modules and vendor directories are externally -maintained libraries used by this software which have their own -licenses; we recommend you read them, as their terms may differ from the -terms above. +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** lru-cache@7.18.3 - https://www.npmjs.com/package/lru-cache/v/7.18.3 | ISC -The ISC License +** fs-extra@11.3.5 - https://www.npmjs.com/package/fs-extra/v/11.3.5 | MIT +(The MIT License) -Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors +Copyright (c) 2011-2024 JP Richardson -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- -** merge2@1.4.1 - https://www.npmjs.com/package/merge2/v/1.4.1 | MIT -The MIT License (MIT) +---------------- -Copyright (c) 2014-2020 Teambition +** function-bind@1.1.2 - https://www.npmjs.com/package/function-bind/v/1.1.2 | MIT +Copyright (c) 2013 Raynos. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11658,24 +11898,29 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ---------------- -** micromatch@4.0.8 - https://www.npmjs.com/package/micromatch/v/4.0.8 | MIT -The MIT License (MIT) +** get-caller-file@2.0.5 - https://www.npmjs.com/package/get-caller-file/v/2.0.5 | ISC -Copyright (c) 2014-present, Jon Schlinkert. +---------------- + +** get-intrinsic@1.3.0 - https://www.npmjs.com/package/get-intrinsic/v/1.3.0 | MIT +MIT License + +Copyright (c) 2020 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11684,24 +11929,24 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** mime@2.6.0 - https://www.npmjs.com/package/mime/v/2.6.0 | MIT -The MIT License (MIT) +** get-proto@1.0.1 - https://www.npmjs.com/package/get-proto/v/1.0.1 | MIT +MIT License -Copyright (c) 2010 Benjamin Thomas, Robert Kieffer +Copyright (c) 2025 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11710,28 +11955,50 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** ms@2.1.3 - https://www.npmjs.com/package/ms/v/2.1.3 | MIT +** get-uri@6.0.5 - https://www.npmjs.com/package/get-uri/v/6.0.5 | MIT +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** mute-stream@0.0.8 - https://www.npmjs.com/package/mute-stream/v/0.0.8 | ISC +** glob-parent@5.1.2 - https://www.npmjs.com/package/glob-parent/v/5.1.2 | ISC The ISC License -Copyright (c) Isaac Z. Schlueter and Contributors +Copyright (c) 2015, 2019 Elan Shanker Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -11748,14 +12015,10 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** netmask@2.1.1 - https://www.npmjs.com/package/netmask/v/2.1.1 | MIT - ----------------- - -** p-finally@1.0.0 - https://www.npmjs.com/package/p-finally/v/1.0.0 | MIT -The MIT License (MIT) +** gopd@1.2.0 - https://www.npmjs.com/package/gopd/v/1.2.0 | MIT +MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2022 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11764,38 +12027,44 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** p-limit@2.3.0 - https://www.npmjs.com/package/p-limit/v/2.3.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) +** graceful-fs@4.2.11 - https://www.npmjs.com/package/graceful-fs/v/4.2.11 | ISC +The ISC License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** p-limit@3.1.0 - https://www.npmjs.com/package/p-limit/v/3.1.0 | MIT +** has-flag@4.0.0 - https://www.npmjs.com/package/has-flag/v/4.0.0 | MIT MIT License -Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -11806,66 +12075,90 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** p-locate@4.1.0 - https://www.npmjs.com/package/p-locate/v/4.1.0 | MIT +** has-symbols@1.1.0 - https://www.npmjs.com/package/has-symbols/v/1.1.0 | MIT MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) 2016 Jordan Harband -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** p-queue@6.6.2 - https://www.npmjs.com/package/p-queue/v/6.6.2 | MIT +** hasown@2.0.4 - https://www.npmjs.com/package/hasown/v/2.0.4 | MIT MIT License -Copyright (c) Sindre Sorhus (https://sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Copyright (c) Jordan Harband and contributors +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: ----------------- - -** p-timeout@3.2.0 - https://www.npmjs.com/package/p-timeout/v/3.2.0 | MIT -MIT License - -Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** p-try@2.2.0 - https://www.npmjs.com/package/p-try/v/2.2.0 | MIT -MIT License +** http-errors@2.0.1 - https://www.npmjs.com/package/http-errors/v/2.0.1 | MIT -Copyright (c) Sindre Sorhus (sindresorhus.com) +The MIT License (MIT) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** pac-proxy-agent@7.2.0 - https://www.npmjs.com/package/pac-proxy-agent/v/7.2.0 | MIT +** http-proxy-agent@7.0.2 - https://www.npmjs.com/package/http-proxy-agent/v/7.0.2 | MIT (The MIT License) -Copyright (c) 2014 Nathan Rajlich +Copyright (c) 2013 Nathan Rajlich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -11889,7 +12182,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** pac-resolver@7.0.1 - https://www.npmjs.com/package/pac-resolver/v/7.0.1 | MIT +** https-proxy-agent@7.0.6 - https://www.npmjs.com/package/https-proxy-agent/v/7.0.6 | MIT (The MIT License) Copyright (c) 2013 Nathan Rajlich @@ -11915,24 +12208,55 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** path-exists@4.0.0 - https://www.npmjs.com/package/path-exists/v/4.0.0 | MIT -MIT License +** iconv-lite@0.4.24 - https://www.npmjs.com/package/iconv-lite/v/0.4.24 | MIT +Copyright (c) 2011 Alexander Shtuchkin -Copyright (c) Sindre Sorhus (sindresorhus.com) +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** picomatch@2.3.2 - https://www.npmjs.com/package/picomatch/v/2.3.2 | MIT -The MIT License (MIT) +** inherits@2.0.4 - https://www.npmjs.com/package/inherits/v/2.0.4 | ISC +The ISC License -Copyright (c) 2017-present, Jon Schlinkert. +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +---------------- + +** ip-address@10.2.0 - https://www.npmjs.com/package/ip-address/v/10.2.0 | MIT +Copyright (C) 2011 by Beau Gunderson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11955,10 +12279,8 @@ THE SOFTWARE. ---------------- -** picomatch@4.0.4 - https://www.npmjs.com/package/picomatch/v/4.0.4 | MIT -The MIT License (MIT) - -Copyright (c) 2017-present, Jon Schlinkert. +** ipaddr.js@1.9.1 - https://www.npmjs.com/package/ipaddr.js/v/1.9.1 | MIT +Copyright (C) 2011-2017 whitequark Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -11981,10 +12303,10 @@ THE SOFTWARE. ---------------- -** promptly@3.2.0 - https://www.npmjs.com/package/promptly/v/3.2.0 | MIT +** is-extglob@2.1.1 - https://www.npmjs.com/package/is-extglob/v/2.1.1 | MIT The MIT License (MIT) -Copyright (c) 2018 Made With MOXY Lda +Copyright (c) 2014-2016, Jon Schlinkert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12007,120 +12329,1326 @@ THE SOFTWARE. ---------------- -** proxy-agent@6.5.0 - https://www.npmjs.com/package/proxy-agent/v/6.5.0 | MIT -(The MIT License) +** is-fullwidth-code-point@3.0.0 - https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 | MIT +MIT License -Copyright (c) 2013 Nathan Rajlich +Copyright (c) Sindre Sorhus (sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** proxy-from-env@1.1.0 - https://www.npmjs.com/package/proxy-from-env/v/1.1.0 | MIT -The MIT License +** is-glob@4.0.3 - https://www.npmjs.com/package/is-glob/v/4.0.3 | MIT +The MIT License (MIT) -Copyright (C) 2016-2018 Rob Wu +Copyright (c) 2014-2017, Jon Schlinkert. -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** queue-microtask@1.2.3 - https://www.npmjs.com/package/queue-microtask/v/1.2.3 | MIT +** is-number@7.0.0 - https://www.npmjs.com/package/is-number/v/7.0.0 | MIT The MIT License (MIT) -Copyright (c) Feross Aboukhadijeh +Copyright (c) 2014-present, Jon Schlinkert. -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. ---------------- -** read@1.0.7 - https://www.npmjs.com/package/read/v/1.0.7 | ISC -The ISC License +** json-source-map@0.6.1 - https://www.npmjs.com/package/json-source-map/v/0.6.1 | MIT +MIT License -Copyright (c) Isaac Z. Schlueter and Contributors +Copyright (c) 2017 Evgeny Poberezkin -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** require-directory@2.1.1 - https://www.npmjs.com/package/require-directory/v/2.1.1 | MIT -The MIT License (MIT) +** jsonfile@6.2.0 - https://www.npmjs.com/package/jsonfile/v/6.2.0 | MIT +(The MIT License) -Copyright (c) 2011 Troy Goode +Copyright (c) 2012-2015, JP Richardson -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** jsonschema@1.5.0 - https://www.npmjs.com/package/jsonschema/v/1.5.0 | MIT +jsonschema is licensed under MIT license. + +Copyright (C) 2012-2015 Tom de Grunt + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** locate-path@5.0.0 - https://www.npmjs.com/package/locate-path/v/5.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** lodash.truncate@4.4.2 - https://www.npmjs.com/package/lodash.truncate/v/4.4.2 | MIT +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +---------------- + +** lru-cache@7.18.3 - https://www.npmjs.com/package/lru-cache/v/7.18.3 | ISC +The ISC License + +Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** math-intrinsics@1.1.0 - https://www.npmjs.com/package/math-intrinsics/v/1.1.0 | MIT +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** media-typer@0.3.0 - https://www.npmjs.com/package/media-typer/v/0.3.0 | MIT +(The MIT License) + +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** merge-descriptors@1.0.3 - https://www.npmjs.com/package/merge-descriptors/v/1.0.3 | MIT +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** merge2@1.4.1 - https://www.npmjs.com/package/merge2/v/1.4.1 | MIT +The MIT License (MIT) + +Copyright (c) 2014-2020 Teambition + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** methods@1.1.2 - https://www.npmjs.com/package/methods/v/1.1.2 | MIT +(The MIT License) + +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +---------------- + +** micromatch@4.0.8 - https://www.npmjs.com/package/micromatch/v/4.0.8 | MIT +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** mime-db@1.52.0 - https://www.npmjs.com/package/mime-db/v/1.52.0 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** mime-types@2.1.35 - https://www.npmjs.com/package/mime-types/v/2.1.35 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** mime@1.6.0 - https://www.npmjs.com/package/mime/v/1.6.0 | MIT +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** mime@2.6.0 - https://www.npmjs.com/package/mime/v/2.6.0 | MIT +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** ms@2.0.0 - https://www.npmjs.com/package/ms/v/2.0.0 | MIT + +---------------- + +** ms@2.1.3 - https://www.npmjs.com/package/ms/v/2.1.3 | MIT + +---------------- + +** mute-stream@0.0.8 - https://www.npmjs.com/package/mute-stream/v/0.0.8 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** negotiator@0.6.3 - https://www.npmjs.com/package/negotiator/v/0.6.3 | MIT +(The MIT License) + +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** netmask@2.1.1 - https://www.npmjs.com/package/netmask/v/2.1.1 | MIT + +---------------- + +** object-inspect@1.13.4 - https://www.npmjs.com/package/object-inspect/v/1.13.4 | MIT +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** on-finished@2.4.1 - https://www.npmjs.com/package/on-finished/v/2.4.1 | MIT +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** p-finally@1.0.0 - https://www.npmjs.com/package/p-finally/v/1.0.0 | MIT +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** p-limit@2.3.0 - https://www.npmjs.com/package/p-limit/v/2.3.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** p-limit@3.1.0 - https://www.npmjs.com/package/p-limit/v/3.1.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** p-locate@4.1.0 - https://www.npmjs.com/package/p-locate/v/4.1.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** p-queue@6.6.2 - https://www.npmjs.com/package/p-queue/v/6.6.2 | MIT +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** p-timeout@3.2.0 - https://www.npmjs.com/package/p-timeout/v/3.2.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** p-try@2.2.0 - https://www.npmjs.com/package/p-try/v/2.2.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** pac-proxy-agent@7.2.0 - https://www.npmjs.com/package/pac-proxy-agent/v/7.2.0 | MIT +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** pac-resolver@7.0.1 - https://www.npmjs.com/package/pac-resolver/v/7.0.1 | MIT +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** parseurl@1.3.3 - https://www.npmjs.com/package/parseurl/v/1.3.3 | MIT + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** path-exists@4.0.0 - https://www.npmjs.com/package/path-exists/v/4.0.0 | MIT +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** path-to-regexp@0.1.13 - https://www.npmjs.com/package/path-to-regexp/v/0.1.13 | MIT +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** picomatch@2.3.2 - https://www.npmjs.com/package/picomatch/v/2.3.2 | MIT +The MIT License (MIT) + +Copyright (c) 2017-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** picomatch@4.0.4 - https://www.npmjs.com/package/picomatch/v/4.0.4 | MIT +The MIT License (MIT) + +Copyright (c) 2017-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** promptly@3.2.0 - https://www.npmjs.com/package/promptly/v/3.2.0 | MIT +The MIT License (MIT) + +Copyright (c) 2018 Made With MOXY Lda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** proxy-addr@2.0.7 - https://www.npmjs.com/package/proxy-addr/v/2.0.7 | MIT +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** proxy-agent@6.5.0 - https://www.npmjs.com/package/proxy-agent/v/6.5.0 | MIT +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** proxy-from-env@1.1.0 - https://www.npmjs.com/package/proxy-from-env/v/1.1.0 | MIT +The MIT License + +Copyright (C) 2016-2018 Rob Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** qs@6.14.2 - https://www.npmjs.com/package/qs/v/6.14.2 | BSD-3-Clause + +---------------- + +** queue-microtask@1.2.3 - https://www.npmjs.com/package/queue-microtask/v/1.2.3 | MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** range-parser@1.2.1 - https://www.npmjs.com/package/range-parser/v/1.2.1 | MIT +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** read@1.0.7 - https://www.npmjs.com/package/read/v/1.0.7 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** require-directory@2.1.1 - https://www.npmjs.com/package/require-directory/v/2.1.1 | MIT +The MIT License (MIT) + +Copyright (c) 2011 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** require-main-filename@2.0.0 - https://www.npmjs.com/package/require-main-filename/v/2.0.0 | ISC +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** reusify@1.1.0 - https://www.npmjs.com/package/reusify/v/1.1.0 | MIT +The MIT License (MIT) + +Copyright (c) 2015-2024 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +---------------- + +** run-parallel@1.2.0 - https://www.npmjs.com/package/run-parallel/v/1.2.0 | MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** safe-buffer@5.2.1 - https://www.npmjs.com/package/safe-buffer/v/5.2.1 | MIT +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +---------------- + +** safer-buffer@2.1.2 - https://www.npmjs.com/package/safer-buffer/v/2.1.2 | MIT +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +---------------- + +** semver@7.8.5 - https://www.npmjs.com/package/semver/v/7.8.5 | ISC +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** send@0.19.2 - https://www.npmjs.com/package/send/v/0.19.2 | MIT +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** serve-static@1.16.3 - https://www.npmjs.com/package/serve-static/v/1.16.3 | MIT +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, @@ -12130,7 +13658,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** require-main-filename@2.0.0 - https://www.npmjs.com/package/require-main-filename/v/2.0.0 | ISC +** set-blocking@2.0.0 - https://www.npmjs.com/package/set-blocking/v/2.0.0 | ISC Copyright (c) 2016, Contributors Permission to use, copy, modify, and/or distribute this software @@ -12149,10 +13677,28 @@ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---------------- -** reusify@1.1.0 - https://www.npmjs.com/package/reusify/v/1.1.0 | MIT -The MIT License (MIT) +** setprototypeof@1.2.0 - https://www.npmjs.com/package/setprototypeof/v/1.2.0 | ISC +Copyright (c) 2015, Wes Todd -Copyright (c) 2015-2024 Matteo Collina +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +---------------- + +** side-channel-list@1.0.1 - https://www.npmjs.com/package/side-channel-list/v/1.0.1 | MIT +MIT License + +Copyright (c) 2024 Jordan Harband Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12173,69 +13719,82 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------- -** run-parallel@1.2.0 - https://www.npmjs.com/package/run-parallel/v/1.2.0 | MIT -The MIT License (MIT) +** side-channel-map@1.0.1 - https://www.npmjs.com/package/side-channel-map/v/1.0.1 | MIT +MIT License -Copyright (c) Feross Aboukhadijeh +Copyright (c) 2024 Jordan Harband -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** semver@7.8.5 - https://www.npmjs.com/package/semver/v/7.8.5 | ISC -The ISC License +** side-channel-weakmap@1.0.2 - https://www.npmjs.com/package/side-channel-weakmap/v/1.0.2 | MIT +MIT License -Copyright (c) Isaac Z. Schlueter and Contributors +Copyright (c) 2019 Jordan Harband -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR -IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- -** set-blocking@2.0.0 - https://www.npmjs.com/package/set-blocking/v/2.0.0 | ISC -Copyright (c) 2016, Contributors +** side-channel@1.1.0 - https://www.npmjs.com/package/side-channel/v/1.1.0 | MIT +MIT License -Permission to use, copy, modify, and/or distribute this software -for any purpose with or without fee is hereby granted, provided -that the above copyright notice and this permission notice -appear in all copies. +Copyright (c) 2019 Jordan Harband -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE -LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. ---------------- @@ -12380,6 +13939,34 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** statuses@2.0.2 - https://www.npmjs.com/package/statuses/v/2.0.2 | MIT + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ---------------- ** string-width@4.2.3 - https://www.npmjs.com/package/string-width/v/4.2.3 | MIT @@ -12477,6 +14064,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** toidentifier@1.0.1 - https://www.npmjs.com/package/toidentifier/v/1.0.1 | MIT +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ---------------- ** tslib@2.8.1 - https://www.npmjs.com/package/tslib/v/2.8.1 | 0BSD @@ -12493,6 +14106,34 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---------------- + +** type-is@1.6.18 - https://www.npmjs.com/package/type-is/v/1.6.18 | MIT +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** universalify@2.0.1 - https://www.npmjs.com/package/universalify/v/2.0.1 | MIT @@ -12518,6 +14159,85 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------- + +** unpipe@1.0.0 - https://www.npmjs.com/package/unpipe/v/1.0.0 | MIT +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** utils-merge@1.0.1 - https://www.npmjs.com/package/utils-merge/v/1.0.1 | MIT +The MIT License (MIT) + +Copyright (c) 2013-2017 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +---------------- + +** vary@1.1.2 - https://www.npmjs.com/package/vary/v/1.1.2 | MIT +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ---------------- ** vscode-jsonrpc@8.2.0 - https://www.npmjs.com/package/vscode-jsonrpc/v/8.2.0 | MIT From 1f242862419b19a34dcda0ab458325daad8f7def Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:00:30 -0400 Subject: [PATCH 27/28] feat: explorer UI improvements (#1732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI improvements to the CDK Web Explorer's violations panel, construct tree, file picker, and template pane diagnostics. **Violations Panel** - Human-readable violation titles: Violation headers show the description (e.g. "Require point-in-time recovery for a DynamoDB table") as the primary title instead of the raw rule name, with the rule name shown right-aligned in monospace as a secondary identifier - Search bar: Added a search input (top-right in header) that filters violations across all text fields (description, rule name, construct path, suggested fix) - Resource filter on double-click: Double-clicking a construct in the tree or a squiggle in the source pane filters the violations panel to show only violations for that resource (and its children) - Dismissable filter pill: When filtered, the header shows "Violations for [ResourceName ×]" with a clear button pill/chip - Right-aligned rule names: Monospace rule name pushed to far-right of each violation row (matching construct tree type alignment) - Two-row header layout: Severity + description on row 1, count/source subtitle on row 2 - Tighter expanded content: Reduced gap between violation header and expanded body, indented expanded content under the header - Clickable construct paths: Occurrence paths styled as blue underlined links with single-click navigation (was double-click) Screenshot 2026-07-16 at 12 23
46 PM **Template Pane** - Squiggle diagnostics: Resources with violations now show wavy underline squiggles in the template pane, which skip leading whitespace: Underlines start at the first non-space character - YAML/JSON format toggle moved to header: Toggle is now in the upper-right of the template pane header (always visible regardless of scroll), only appears when a template is loaded. On switch, takes you to the same PLACE in the other format, not the same line number Screenshot 2026-07-16 at 1 01 35 PM **Construct Tree** - Removed opacity on inherited colors: Tree labels with inherited severity colors now display at full opacity - Default cursor on non-clickable rows: Non-expandable rows show default cursor instead of pointer Screenshot 2026-07-16 at 12 24 38 PM **File Picker** - Folder icon button: Replaced the grey "Open" button with a folder icon - Dropdown overlay: File picker overlays the code pane instead of replacing it (code stays mounted underneath) - No animation replay: Opening/closing the file picker no longer re-triggers the navigation highlight animation - Click-outside to close: Clicking anywhere outside the picker dismisses it Screenshot 2026-07-16 at 1 03 54 PM ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../@aws-cdk/cdk-explorer/frontend/App.tsx | 306 +++++++++++++----- .../@aws-cdk/cdk-explorer/frontend/api.ts | 5 - .../frontend/components/CodeViewer.tsx | 10 +- .../frontend/components/ConstructTree.tsx | 5 +- .../frontend/components/TemplateViewer.tsx | 112 +++---- .../frontend/components/ViolationsPanel.tsx | 88 +++-- .../cdk-explorer/frontend/highlight.css | 17 + .../cdk-explorer/frontend/nav-types.ts | 1 + .../@aws-cdk/cdk-explorer/lib/web/protocol.ts | 12 - .../@aws-cdk/cdk-explorer/lib/web/routes.ts | 35 +- .../cdk-explorer/test/web/routes.test.ts | 28 -- 11 files changed, 369 insertions(+), 250 deletions(-) diff --git a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx index f7f72e744..48c9d1f7c 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx @@ -6,11 +6,11 @@ import SpaceBetween from '@cloudscape-design/components/space-between'; import Spinner from '@cloudscape-design/components/spinner'; import * as React from 'react'; import { buildSourceAnchorIndex, findConstructAtLine } from '../lib/web/source-nav'; -import { api, type DirEntry, type TemplateResponse, type TreeResponse, type ViolationsResponse } from './api'; +import { api, type TemplateResponse, type TreeResponse, type ViolationsResponse } from './api'; import { CodeViewer, type Diagnostic } from './components/CodeViewer'; import { ConstructTree } from './components/ConstructTree'; import type { Language } from './syntax'; -import { TemplateViewer } from './components/TemplateViewer'; +import { TemplateViewer, type Format } from './components/TemplateViewer'; import { ViolationsPanel } from './components/ViolationsPanel'; import type { NavigateHandler } from './nav-types'; export type { NavigateHandler } from './nav-types'; @@ -39,6 +39,7 @@ export function App(): JSX.Element { // Template pane state. const [templateFile, setTemplateFile] = React.useState(); const [templateData, setTemplateData] = React.useState(); + const [templateFormat, setTemplateFormat] = React.useState('yaml'); // Refs for current values in async callbacks (avoids stale closures). const sourceFileRef = React.useRef(sourceFile); @@ -52,6 +53,10 @@ export function App(): JSX.Element { const [nav, setNav] = React.useState(); const navCounterRef = React.useRef(0); + // Violations filter: when set, only violations affecting this construct path are shown. + const [violationFilter, setViolationFilter] = React.useState(); + const [violationSearch, setViolationSearch] = React.useState(''); + const reload = React.useCallback((): void => { Promise.all([api.getTree(), api.getViolations(), api.getAppInfo()]) .then(([t, v, info]) => { @@ -75,6 +80,10 @@ export function App(): JSX.Element { const counter = ++navCounterRef.current; const color = opts.color ?? NEUTRAL_COLOR; + if (opts.constructPath) { + setViolationFilter(opts.constructPath); + } + let sourceTarget: NavTarget['source']; if (opts.sourceLocation) { sourceTarget = { file: opts.sourceLocation.file, startLine: opts.sourceLocation.line, endLine: opts.sourceLocation.line }; @@ -149,40 +158,55 @@ export function App(): JSX.Element { sourceLocation: node.sourceLocation, templateFile: node.templateFile, logicalId: node.logicalId, + constructPath: node.path, }); }, [sourceAnchors, navigate]); // File picker state. const [showFilePicker, setShowFilePicker] = React.useState(false); - const [pickerDir, setPickerDir] = React.useState(''); - const [pickerEntries, setPickerEntries] = React.useState([]); + const pickerRef = React.useRef(null); - const openFilePicker = React.useCallback(async (pane: 'source' | 'template') => { - setShowFilePicker(pane); - try { - const res = await api.listFiles(''); - setPickerDir(res.dir); - setPickerEntries(res.entries); - } catch { /* ignore */ } - }, []); + React.useEffect(() => { + if (!showFilePicker) return; + const handleClick = (e: MouseEvent) => { + const target = e.target as Node; + if (pickerRef.current && !pickerRef.current.contains(target)) { + if ((target as HTMLElement).closest?.('[aria-label="Open file"], [aria-label="Open template file"]')) return; + setShowFilePicker(false); + } + }; + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [showFilePicker]); - const browseDir = React.useCallback(async (dir: string) => { - try { - const res = await api.listFiles(dir); - setPickerDir(res.dir); - setPickerEntries(res.entries); - } catch { /* ignore */ } - }, []); + const knownSourceFiles = React.useMemo( + () => sourceAnchors ? [...sourceAnchors.keys()].sort() : [], + [sourceAnchors], + ); + + const knownTemplateFiles = React.useMemo(() => { + if (!tree || tree.status !== 'ok') return []; + const files = new Set(); + const walk = (nodes: readonly import('./api').WebConstructNode[]) => { + for (const n of nodes) { + if (n.templateFile) files.add(n.templateFile); + walk(n.children); + } + }; + walk(tree.tree); + return [...files].sort(); + }, [tree]); const pickFile = React.useCallback(async (filePath: string, pane: 'source' | 'template') => { try { - const res = await api.readFile(filePath); if (pane === 'source') { + const res = await api.readFile(filePath); setSourceFile(res.path); setSourceContent(res.content); } else { - setTemplateFile(res.path); - setTemplateData({ content: res.content, resources: {} }); + const data = await api.getTemplate(filePath); + setTemplateFile(filePath); + setTemplateData(data); } setShowFilePicker(false); } catch { /* ignore */ } @@ -215,56 +239,77 @@ export function App(): JSX.Element {
    - - {sourceFile ?? 'Source'} - + + + {sourceFile ?? 'Source'} + + + {showFilePicker === 'source' && ( +
    + void pickFile(p, 'source')} /> +
    + )}
    }> - {showFilePicker === 'source' ? ( - void pickFile(p, 'source')} /> - ) : sourceContent ? ( - - ) : ( - Double-click a construct to view its source. - )} +
    + {sourceContent ? ( + + ) : ( + Double-click a construct to view its source. + )} +
    - - {templateFile ?? 'Template'} - +
    }> + + + {templateFile ?? 'Template'} + + + {showFilePicker === 'template' && ( +
    + void pickFile(p, 'template')} /> +
    + )}
    }> - {showFilePicker === 'template' ? ( - void pickFile(p, 'template')} /> - ) : templateData ? ( - - ) : templateFile ? ( - Could not load {templateFile} - ) : ( - Double-click a construct to view its template. - )} +
    + {templateData ? ( + + ) : templateFile ? ( + Could not load {templateFile} + ) : ( + Double-click a construct to view its template. + )} +
    @@ -274,8 +319,12 @@ export function App(): JSX.Element { {!vSplit.collapsed && (
    - Violations}> - + }> + setViolationFilter(undefined)} /> + + }> + setViolationFilter(undefined)} search={violationSearch} />
    )} @@ -285,24 +334,16 @@ export function App(): JSX.Element { } -function FilePicker({ dir, entries, onBrowse, onPick }: { - readonly dir: string; - readonly entries: readonly DirEntry[]; - readonly onBrowse: (dir: string) => void; +function KnownFileList({ files, onPick }: { + readonly files: readonly string[]; readonly onPick: (path: string) => void; }): JSX.Element { + if (files.length === 0) return No files found.; return ( - /{dir} - {dir !== '' && } - {entries.map((entry) => ( - ))} @@ -317,12 +358,47 @@ function ConstructTreeContent({ tree, onNavigate }: { readonly tree?: TreeRespon return ; } -function ViolationsContent({ violations, onNavigate }: { readonly violations?: ViolationsResponse; readonly onNavigate: NavigateHandler }): JSX.Element { +function ViolationsContent({ violations, onNavigate, filter, onClearFilter, search }: { + readonly violations?: ViolationsResponse; + readonly onNavigate: NavigateHandler; + readonly filter?: string; + readonly onClearFilter: () => void; + readonly search: string; +}): JSX.Element { if (!violations) return ; if (violations.status === 'not-synthesized') { return No cloud assembly found.; } - return ; + return ; +} + +function ViolationsTitle({ filter, onClearFilter }: { readonly filter?: string; readonly onClearFilter: () => void }): JSX.Element { + if (!filter) return <>Violations; + const name = filter.split('/').pop() || filter; + return ( + + Violations for + + {name} + + + + ); +} + +function ViolationsActions({ search, onSearchChange }: { + readonly search: string; + readonly onSearchChange: (value: string) => void; +}): JSX.Element { + return ( + onSearchChange(e.target.value)} + style={VIOLATIONS_SEARCH_STYLE} + /> + ); } interface SplitOptions { @@ -477,15 +553,31 @@ const TITLE_BLOCK_STYLE: React.CSSProperties = { flexShrink: 0, marginBottom: '1 const GROW_STYLE: React.CSSProperties = { flex: '1 1 0', minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }; const HEADER_WITH_ACTION_STYLE: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: '6px' }; -const OPEN_FILE_BUTTON_STYLE: React.CSSProperties = { - border: '1px solid #d1d5db', - borderRadius: '4px', - background: '#fafafa', +const FOLDER_BUTTON_STYLE: React.CSSProperties = { + border: 'none', + background: 'none', cursor: 'pointer', - fontSize: '11px', - padding: '1px 6px', + padding: '2px', color: '#5f6b7a', - lineHeight: '16px', + display: 'inline-flex', + alignItems: 'center', +}; +const CODE_PANE_INNER_STYLE: React.CSSProperties = { height: '100%' }; +const PICKER_ANCHOR_STYLE: React.CSSProperties = { position: 'relative', display: 'inline-flex', alignItems: 'center' }; +const PICKER_DROPDOWN_STYLE: React.CSSProperties = { + position: 'absolute', + top: '100%', + left: 0, + marginTop: '4px', + maxHeight: '50vh', + minWidth: '320px', + overflowY: 'auto', + background: '#ffffff', + border: '1px solid #d1d5db', + borderRadius: '4px', + boxShadow: '0 4px 12px rgba(0,0,0,0.1)', + padding: '8px', + zIndex: 100, }; const CODE_PANES_STYLE: React.CSSProperties = { @@ -528,6 +620,46 @@ const RESIZER_BUTTON_VERTICAL: React.CSSProperties = { borderRadius: '7px', }; +function FormatToggle({ format, onChange }: { readonly format: Format; readonly onChange: (f: Format) => void }): JSX.Element { + return ( + + + + + ); +} + +function FolderIcon(): JSX.Element { + return ( + + + + ); +} + +const FORMAT_TOGGLE_GROUP_STYLE: React.CSSProperties = { display: 'inline-flex', gap: '2px' }; +const FORMAT_TOGGLE_STYLE: React.CSSProperties = { + border: '1px solid #d1d5db', borderRadius: '4px', background: '#fafafa', + cursor: 'pointer', fontSize: '11px', padding: '2px 8px', color: '#5f6b7a', lineHeight: '16px', +}; +const FORMAT_TOGGLE_ACTIVE_STYLE: React.CSSProperties = { + ...FORMAT_TOGGLE_STYLE, background: '#0972d3', color: '#ffffff', borderColor: '#0972d3', +}; +const VIOLATIONS_TITLE_STYLE: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: '6px' }; +const FILTER_PILL_STYLE: React.CSSProperties = { + display: 'inline-flex', alignItems: 'center', gap: '4px', + background: '#f2f8fd', border: '1px solid #89bdee', borderRadius: '12px', + padding: '1px 8px 1px 10px', fontSize: '14px', color: '#0972d3', fontWeight: 600, +}; +const FILTER_CLEAR_STYLE: React.CSSProperties = { + background: 'none', border: 'none', cursor: 'pointer', + fontSize: '14px', lineHeight: 1, color: '#0972d3', padding: '0 2px', +}; +const VIOLATIONS_SEARCH_STYLE: React.CSSProperties = { + width: '160px', padding: '3px 8px', border: '1px solid #d1d5db', + borderRadius: '4px', fontSize: '13px', outline: 'none', +}; + function detectLanguage(file: string | undefined): Language { if (!file) return 'typescript'; if (file.endsWith('.json')) return 'json'; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/api.ts b/packages/@aws-cdk/cdk-explorer/frontend/api.ts index e28a07588..35d9e03c9 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/api.ts +++ b/packages/@aws-cdk/cdk-explorer/frontend/api.ts @@ -1,7 +1,5 @@ import { ASSEMBLY_CHANGED, - type DirEntry, - type FilesResponse, type FileResponse, type LineRange, type TemplateResource, @@ -15,8 +13,6 @@ import { } from '../lib/web/protocol'; export type { - DirEntry, - FilesResponse, FileResponse, LineRange, TemplateResource, @@ -43,7 +39,6 @@ export interface AppInfoResponse { } export const api = { - listFiles: (dir = ''): Promise => getJson(`/api/files?dir=${encodeURIComponent(dir)}`), readFile: (filePath: string): Promise => getJson(`/api/file?path=${encodeURIComponent(filePath)}`), getTree: (): Promise => getJson('/api/tree'), getViolations: (): Promise => getJson('/api/policy-validation'), diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx index 47f2c8f83..d5f2dd748 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/CodeViewer.tsx @@ -84,9 +84,13 @@ export function CodeViewer({ return map; }, [diagnostics]); + const lastNavRef = React.useRef(); + const animateNav = navCounter !== lastNavRef.current; + React.useEffect(() => { lastNavRef.current = navCounter; }, [navCounter]); + React.useEffect(() => { if (scrollToLine && scrollTargetRef.current) { - scrollTargetRef.current.scrollIntoView({ block: 'start', behavior: 'smooth' }); + scrollTargetRef.current.scrollIntoView({ block: 'start', behavior: animateNav ? 'smooth' : 'instant' }); } }, [scrollToLine, navCounter]); @@ -103,9 +107,9 @@ export function CodeViewer({ return (
    void; + readonly templateFile?: string; + readonly violations?: readonly WebViolation[]; + readonly format: Format; } -type Format = 'yaml' | 'json'; +export type Format = 'yaml' | 'json'; interface ResourceSection { readonly logicalId: string; @@ -27,8 +30,10 @@ export function TemplateViewer({ highlightColor, navCounter, onResourceDoubleClick, + templateFile, + violations, + format, }: TemplateViewerProps): JSX.Element { - const [format, setFormat] = React.useState('yaml'); const { displayContent, displayResources, sections } = React.useMemo(() => { if (format === 'json') { @@ -49,6 +54,26 @@ export function TemplateViewer({ return block ? { start: block.startLine, end: block.endLine } : undefined; }, [highlightLogicalId, displayResources]); + const diagnostics = React.useMemo(() => { + if (!templateFile || !violations?.length) return undefined; + const lines = displayContent.split('\n'); + const diags: Diagnostic[] = []; + for (const violation of violations) { + const severity = violationSeverity(violation.severity); + for (const occ of violation.occurrences) { + if (occ.templateFile === templateFile && occ.logicalId) { + const resource = displayResources[occ.logicalId]; + if (resource) { + const line = lines[resource.block.startLine - 1]; + const startCol = line ? line.search(/\S/) + 1 : 1; + diags.push({ startLine: resource.block.startLine, startCol: Math.max(1, startCol), severity, message: violation.description }); + } + } + } + } + return diags.length > 0 ? diags : undefined; + }, [templateFile, violations, displayResources, displayContent]); + const handleDoubleClick = React.useCallback((line: number) => { if (!onResourceDoubleClick) return; // Map the clicked line back to the resource that owns it, so the reverse @@ -60,30 +85,17 @@ export function TemplateViewer({ }, [sections, onResourceDoubleClick]); return ( -
    -
    - - -
    - -
    + ); } @@ -135,35 +147,15 @@ function jsonToYaml(jsonContent: string): YamlResult { return { displayContent, displayResources, sections: buildSections(displayResources) }; } -const WRAPPER_STYLE: React.CSSProperties = { - display: 'flex', - flexDirection: 'column', - height: '100%', - minHeight: 0, -}; - -const TOOLBAR_STYLE: React.CSSProperties = { - display: 'flex', - alignItems: 'center', - gap: '4px', - padding: '4px 0', - flexShrink: 0, -}; - -const TOGGLE_STYLE: React.CSSProperties = { - border: '1px solid #d1d5db', - borderRadius: '4px', - background: '#fafafa', - cursor: 'pointer', - fontSize: '11px', - padding: '2px 8px', - color: '#5f6b7a', - lineHeight: '16px', -}; - -const TOGGLE_ACTIVE_STYLE: React.CSSProperties = { - ...TOGGLE_STYLE, - background: '#0972d3', - color: '#ffffff', - borderColor: '#0972d3', -}; + +function violationSeverity(severity: string | undefined): 'error' | 'warning' | 'info' { + switch (severity) { + case 'fatal': + case 'error': + return 'error'; + case 'warning': + return 'warning'; + default: + return 'info'; + } +} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx b/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx index 06ed14ec5..e29faef2b 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/components/ViolationsPanel.tsx @@ -10,13 +10,23 @@ import type { NavigateHandler } from '../nav-types'; interface ViolationsPanelProps { readonly violations: readonly WebViolation[]; readonly onNavigate: NavigateHandler; + readonly filter?: string; + readonly onClearFilter: () => void; + readonly search: string; } -export function ViolationsPanel({ violations, onNavigate }: ViolationsPanelProps): JSX.Element { +export function ViolationsPanel({ violations, onNavigate, filter, search }: ViolationsPanelProps): JSX.Element { if (violations.length === 0) { return No policy violations.; } - const sorted = [...violations].sort((a, b) => severityRank(displaySeverity(a)) - severityRank(displaySeverity(b))); + + const filtered = filterViolations(violations, filter, search); + const sorted = [...filtered].sort((a, b) => severityRank(displaySeverity(a)) - severityRank(displaySeverity(b))); + + if (sorted.length === 0) { + return {filter ? 'No violations for this resource.' : 'No matching violations.'}; + } + return (
    @@ -28,29 +38,65 @@ export function ViolationsPanel({ violations, onNavigate }: ViolationsPanelProps ); } +function filterViolations(violations: readonly WebViolation[], filter: string | undefined, search: string): readonly WebViolation[] { + let result = violations; + + if (filter) { + const filtered: WebViolation[] = []; + for (const v of result) { + const matchingOccs = v.occurrences.filter( + (occ) => occ.constructPath === filter || occ.constructPath.startsWith(filter + '/'), + ); + if (matchingOccs.length > 0) { + filtered.push({ ...v, occurrences: matchingOccs }); + } + } + result = filtered; + } + + if (search.trim()) { + const q = search.trim().toLowerCase(); + result = result.filter((v) => + v.ruleName.toLowerCase().includes(q) || + (v.description ?? '').toLowerCase().includes(q) || + (v.suggestedFix ?? '').toLowerCase().includes(q) || + v.occurrences.some((occ) => occ.constructPath.toLowerCase().includes(q)), + ); + } + + return result; +} + + function ViolationItem({ violation, onNavigate }: { readonly violation: WebViolation; readonly onNavigate: NavigateHandler }): JSX.Element { const severity = displaySeverity(violation); const count = violation.occurrences.length; + const title = violation.description?.trim() || violation.ruleName; + const showRuleName = title !== violation.ruleName; return ( - [{severity.toUpperCase()}] - {violation.ruleName} - +
    +
    + + [{severity.toUpperCase()}] + {title} + + {showRuleName && {violation.ruleName}} +
    +
    {count} {count === 1 ? 'construct' : 'constructs'} {'·'} {violation.source} - - +
    +
    } > - - {violation.description} +
    {violation.suggestedFix && Suggested fix: {violation.suggestedFix}} {violation.occurrences.map((occ, i) => ( ))} - +
    ); } @@ -60,7 +106,7 @@ function OccurrenceRow({ occurrence, severity, onNavigate }: { readonly severity: string; readonly onNavigate: NavigateHandler; }): JSX.Element { - const handleDoubleClick = React.useCallback(() => { + const handleClick = React.useCallback(() => { if (!occurrence.sourceLocation && !occurrence.templateFile) return; onNavigate({ sourceLocation: occurrence.sourceLocation, @@ -73,7 +119,7 @@ function OccurrenceRow({ occurrence, severity, onNavigate }: { return ( - + {occurrence.constructPath} {occurrence.logicalId ? ` → ${occurrence.logicalId}` : ''} {occurrence.templateFile ? ` (${occurrence.templateFile})` : ''} @@ -83,11 +129,15 @@ function OccurrenceRow({ occurrence, severity, onNavigate }: { } function severityStyle(severity: string): React.CSSProperties { - return { color: severityHexColor(severity), fontWeight: 700 }; + return { color: severityHexColor(severity), fontWeight: 700, whiteSpace: 'nowrap', flexShrink: 0 }; } -const SCROLL_STYLE: React.CSSProperties = { maxHeight: '100%', overflowY: 'auto' }; -const HEADER_STYLE: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: '8px' }; -const RULE_STYLE: React.CSSProperties = { fontWeight: 700 }; -const META_STYLE: React.CSSProperties = { color: '#5f6b7a', fontWeight: 400, fontSize: '12px' }; -const OCCURRENCE_STYLE: React.CSSProperties = { cursor: 'default' }; +const SCROLL_STYLE: React.CSSProperties = { flex: '1 1 0', overflowY: 'auto', overflowX: 'hidden', minHeight: 0 }; +const HEADER_WRAPPER_STYLE: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: '2px', width: '100%', overflow: 'hidden' }; +const HEADER_ROW_STYLE: React.CSSProperties = { display: 'flex', alignItems: 'baseline', gap: '8px', width: '100%', justifyContent: 'space-between', overflow: 'hidden' }; +const TITLE_GROUP_STYLE: React.CSSProperties = { display: 'flex', alignItems: 'baseline', gap: '8px', minWidth: 0, flex: '1 1 0', overflow: 'hidden' }; +const RULE_STYLE: React.CSSProperties = { fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; +const RULE_NAME_STYLE: React.CSSProperties = { fontFamily: 'monospace', fontSize: '12px', color: '#5f6b7a', fontWeight: 400, whiteSpace: 'nowrap', flexShrink: 0 }; +const SUBTITLE_STYLE: React.CSSProperties = { color: '#5f6b7a', fontWeight: 400, fontSize: '12px' }; +const BODY_STYLE: React.CSSProperties = { paddingLeft: '4px', display: 'flex', flexDirection: 'column', gap: '4px' }; +const LINK_STYLE: React.CSSProperties = { color: '#0972d3', textDecoration: 'underline', cursor: 'pointer' }; diff --git a/packages/@aws-cdk/cdk-explorer/frontend/highlight.css b/packages/@aws-cdk/cdk-explorer/frontend/highlight.css index 8f184542a..438c3ea6e 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/highlight.css +++ b/packages/@aws-cdk/cdk-explorer/frontend/highlight.css @@ -15,3 +15,20 @@ animation: nav-highlight-fade 3s ease-out forwards; pointer-events: none; } + +/* Make expandable section headers stretch to full width */ +[class*="header-wrapper_"] { + width: 100%; +} +[class*="expand-button_"][class*="header-button_"] { + flex: 1 1 auto; +} +[class*="header-text_"][class*="header-label_"] { + flex: 1 1 auto; +} + +/* Tighten expanded content: reduce top gap and indent under header text */ +[class*="content_"][class*="content-footer_"] { + padding-top: 2px !important; + padding-left: 20px !important; +} diff --git a/packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts b/packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts index 5f60095e9..8acafc0ff 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts +++ b/packages/@aws-cdk/cdk-explorer/frontend/nav-types.ts @@ -6,4 +6,5 @@ export type NavigateHandler = (opts: { logicalId?: string; propertyPaths?: readonly string[]; color?: string; + constructPath?: string; }) => void; diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts index 954fce2e3..417bdb799 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts @@ -10,18 +10,6 @@ export const ASSEMBLY_CHANGED = 'assembly-changed'; /** The SSE event names the server may send. One for now. */ export type SseEventName = typeof ASSEMBLY_CHANGED; -export interface DirEntry { - readonly name: string; - /** Path relative to the app directory, usable as the next `dir`/`path` value. POSIX separators. */ - readonly path: string; - readonly type: 'dir' | 'file'; -} - -export interface FilesResponse { - readonly dir: string; - readonly entries: readonly DirEntry[]; -} - export interface FileResponse { readonly path: string; readonly content: string; diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts index b7706f1e4..652c5685b 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts @@ -6,7 +6,7 @@ import { ToolkitError } from '@aws-cdk/toolkit-lib'; import { type Router, type Express, type Response } from 'express'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); -import type { DirEntry, LineRange, TemplateResource, TemplateResponse, TreeResponse, ViolationsResponse, WebConstructNode, WebSourceLocation, WebViolation, WebViolationOccurrence } from './protocol'; +import type { LineRange, TemplateResource, TemplateResponse, TreeResponse, ViolationsResponse, WebConstructNode, WebSourceLocation, WebViolation, WebViolationOccurrence } from './protocol'; import { resolveWithinRoot } from './safe-path'; import { classifyReportSeverity, displaySeverity, severityRank } from './severity'; import type { AcquireAssemblyLock, AssemblyLock } from '../core/assembly-lock'; @@ -105,24 +105,6 @@ export function createApiRouter(options: ApiOptions): Router { res.json({ appDir }); }); - router.get('/files', (req, res) => { - const dir = typeof req.query.dir === 'string' ? req.query.dir : ''; - const resolved = resolveWithinRoot(appDir, dir); - if (!resolved) { - return res.status(403).json({ error: 'path escapes application directory' }); - } - let stat: fs.Stats; - try { - stat = fs.statSync(resolved); - } catch { - return res.status(404).json({ error: 'directory not found' }); - } - if (!stat.isDirectory()) { - return res.status(400).json({ error: 'not a directory' }); - } - return res.json({ dir: toPosix(path.relative(appDir, resolved)), entries: listDir(appDir, resolved) }); - }); - router.get('/file', (req, res) => { const requested = typeof req.query.path === 'string' ? req.query.path : ''; if (!requested) { @@ -472,21 +454,6 @@ function offsetRangeToLineRange(range: { start: number; end: number }, lineOffse }; } -function listDir(appDir: string, dir: string): DirEntry[] { - return fs.readdirSync(dir, { withFileTypes: true }) - .map((entry): DirEntry => ({ - name: entry.name, - path: toPosix(path.relative(appDir, path.join(dir, entry.name))), - type: entry.isDirectory() ? 'dir' : 'file', - })) - .sort(byTypeThenName); -} - -function byTypeThenName(a: DirEntry, b: DirEntry): number { - if (a.type !== b.type) return a.type === 'dir' ? -1 : 1; - return a.name.localeCompare(b.name); -} - /** Normalize OS separators to '/' so the API contract is stable across platforms. */ function toPosix(p: string): string { return p.split(path.sep).join('/'); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts index 97fc3379c..ada3bee89 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts @@ -44,34 +44,6 @@ describe('GET /api/health', () => { }); }); -describe('GET /api/files', () => { - test('lists the root directory with directories first', async () => { - const res = await request(app).get('/api/files'); - expect(res.status).toBe(200); - expect(res.body.dir).toBe(''); - expect(res.body.entries).toEqual([ - { name: 'lib', path: 'lib', type: 'dir' }, - { name: 'app.ts', path: 'app.ts', type: 'file' }, - ]); - }); - - test('lists a subdirectory', async () => { - const res = await request(app).get('/api/files').query({ dir: 'lib' }); - expect(res.status).toBe(200); - expect(res.body.entries).toEqual([{ name: 'stack.ts', path: path.join('lib', 'stack.ts'), type: 'file' }]); - }); - - test('rejects traversal outside the app directory with 403', async () => { - const res = await request(app).get('/api/files').query({ dir: '../..' }); - expect(res.status).toBe(403); - }); - - test('returns 404 for a missing directory', async () => { - const res = await request(app).get('/api/files').query({ dir: 'nope' }); - expect(res.status).toBe(404); - }); -}); - describe('GET /api/file', () => { test('returns file content', async () => { const res = await request(app).get('/api/file').query({ path: 'app.ts' }); From fc79c65192b5182cbbe71a7c1835236cb11f16aa Mon Sep 17 00:00:00 2001 From: Megha Narayanan <68804146+megha-narayanan@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:20:19 -0400 Subject: [PATCH 28/28] feat: explorer stale source banner (#1738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a banner to the web explorer's source pane that warns when the open file has been edited since the last synth, so its violations and diagnostics may be stale. The explorer reads source files live from disk, but violation source locations come from the cloud assembly and are frozen at the last synth. If you edit a file without re-synthing, the code shown updates while the violation markers stay where synth left them, so a marker can sit on the wrong line. The explorer never runs synths itself and has no direct signal that one happened, so it cannot keep the two in lockstep. This surfaces that gap honestly instead of showing misplaced markers silently. **Behavior** The source pane always shows the current file on disk. When the open file's last-modified time is newer than the current assembly's synth, a warning shows: "This file has been modified since the last synth, so its violations and diagnostics may be stale. Re-run cdk synth to refresh." The banner appears the moment you save. A source-tree watcher pushes an event over the existing SSE stream, so you do not have to re-open the file. After a synth completes and the explorer reloads the assembly, the open file is re-fetched and the banner clears. Options considered I looked at four ways to handle staleness: 1. Require synth-on-save in the web explorer. Keep the assembly always fresh by triggering a synth on every source change. Rejected: web synth-triggering was descoped, the explorer is a read-only viewer over cdk.out, and forcing a synth on every edit duplicates the LSP's job, adds a synth runner and lock coordination to the web process, and takes control of an expensive operation away from the user. 2. Snapshot and serve (caching). On synth start, cache the source files and serve that snapshot so the markers always line up with the displayed code, showing a banner when disk differs from the snapshot. Rejected for this change: it needs a per-generation content store, an atomic swap of assembly and snapshot together, and a full source-tree read per synth, and it shows the user older code rather than their current edits. It removes wrong-line markers entirely, but that failure is rare and low-impact, so the cost is not justified here. 3. Reference the synth finish. Read the source at synth finish and flag a file whose last-modified time is later than that. Simple, but it misses the in-flight case: an edit made while a synth is running lands before that synth finishes, so it would not be flagged even though the synth may not have picked it up. 4. Reference the synth start (chosen). Record when synth write-lock activity first appears (the synth.lock marker the toolkit's RWLock creates), and treat a file modified after that point as stale. This catches the in-flight edit that option 3 misses, at the cost of over-flagging by at most one synth's duration, which self-heals on the next assembly. It needs no synth machinery and keeps the explorer read-only. Design decisions Reference is synth start, not synth finish, for the reason in option 4. When no write-lock activity is observed for a generation (the synth predated the server, or the lock was too brief to catch), it falls back to the manifest mtime, which degrades gracefully to option 3. Staleness is decided server-side and returned as a stale flag on /api/file. The client only renders it; it cannot compute it, since it does not know synth timing. The assembly watcher is the single owner of the reference. It already observes both signals it needs, the synth.lock start and the assembly generation change, so it advances the reference once per generation before waking browsers, and the read endpoints only read it. Keeping this in one place avoids a reference that lags a synth or gets overwritten by concurrent reads. mtime, not a content hash. A save that does not change content gives a false stale flag. That is acceptable under the same best-effort bar: the worst case is a banner you did not need until the next synth. The banner covers any open source file, not only files carrying violations, since a stale navigation anchor matters too. It is scoped to the source pane; templates are regenerated by synth, so they are never "source". Screenshot 2026-07-17 at 12 31 29 PM Open questions: I know in my demo meeting we decided that the web shouldn't have synthing capabilities, but having a banner that says "run synth" without a button to directly run synth seems unintuitive. Happy to hear opinions on if we think this means we should add manual and / or auto synth back into the web (and how we should handle the queue of 1 then, just have it be in-process still?). ### Checklist - [ ] This change contains a major version upgrade for a dependency and I confirm all breaking changes are addressed - Release notes for the new version: --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .../@aws-cdk/cdk-explorer/frontend/App.tsx | 74 ++++++++-- .../@aws-cdk/cdk-explorer/frontend/api.ts | 17 ++- .../@aws-cdk/cdk-explorer/lib/api-private.ts | 1 + .../cdk-explorer/lib/core/assembly-watcher.ts | 21 ++- .../cdk-explorer/lib/core/source-watcher.ts | 130 ++++++++++++++++++ .../@aws-cdk/cdk-explorer/lib/lsp/server.ts | 17 +-- .../@aws-cdk/cdk-explorer/lib/web/protocol.ts | 17 ++- .../@aws-cdk/cdk-explorer/lib/web/routes.ts | 16 ++- .../@aws-cdk/cdk-explorer/lib/web/server.ts | 59 +++++++- .../cdk-explorer/lib/web/staleness.ts | 45 ++++++ .../test/core/assembly-watcher.test.ts | 27 +++- .../test/core/source-watcher.test.ts | 109 +++++++++++++++ .../cdk-explorer/test/web/routes.test.ts | 27 +++- .../cdk-explorer/test/web/server.test.ts | 42 +++++- .../cdk-explorer/test/web/staleness.test.ts | 50 +++++++ .../@aws-cdk/toolkit-lib/lib/api/rwlock.ts | 8 +- 16 files changed, 619 insertions(+), 41 deletions(-) create mode 100644 packages/@aws-cdk/cdk-explorer/lib/core/source-watcher.ts create mode 100644 packages/@aws-cdk/cdk-explorer/lib/web/staleness.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/core/source-watcher.test.ts create mode 100644 packages/@aws-cdk/cdk-explorer/test/web/staleness.test.ts diff --git a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx index 48c9d1f7c..be9950f6e 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/App.tsx +++ b/packages/@aws-cdk/cdk-explorer/frontend/App.tsx @@ -1,4 +1,5 @@ import Box from '@cloudscape-design/components/box'; +import Alert from '@cloudscape-design/components/alert'; import Button from '@cloudscape-design/components/button'; import Container from '@cloudscape-design/components/container'; import Header from '@cloudscape-design/components/header'; @@ -35,6 +36,9 @@ export function App(): JSX.Element { // Source pane state. const [sourceFile, setSourceFile] = React.useState(); const [sourceContent, setSourceContent] = React.useState(''); + // True when the open source file was edited after the current assembly's synth + // started, so its squiggles/nav anchors may be stale. Set from /api/file. + const [sourceStale, setSourceStale] = React.useState(false); // Template pane state. const [templateFile, setTemplateFile] = React.useState(); @@ -70,10 +74,33 @@ export function App(): JSX.Element { .catch((err) => setError(err instanceof Error ? err.message : String(err))); }, []); + // Re-read the currently open source file, refreshing both its content and its + // staleness flag. Used after a synth (assembly changed) to clear a banner, and + // on a source edit to raise one, without the user re-opening the file. + const refreshOpenSourceFile = React.useCallback(async (): Promise => { + const file = sourceFileRef.current; + if (!file) return; + try { + const res = await api.readFile(file); + setSourceContent(res.content); + setSourceStale(res.stale); + } catch { + // Keep the last good content; a later event re-tries. + } + }, []); + React.useEffect(() => { reload(); - return api.subscribe(reload); - }, [reload]); + return api.subscribe({ + onAssemblyChanged: () => { + reload(); + void refreshOpenSourceFile(); + }, + onSourceChanged: () => { + void refreshOpenSourceFile(); + }, + }); + }, [reload, refreshOpenSourceFile]); /** Navigate to a construct (from tree double-click or violation double-click). */ const navigate: NavigateHandler = React.useCallback(async (opts) => { @@ -92,9 +119,11 @@ export function App(): JSX.Element { const res = await api.readFile(opts.sourceLocation.file); setSourceFile(res.path); setSourceContent(res.content); + setSourceStale(res.stale); } catch { setSourceFile(opts.sourceLocation.file); setSourceContent(`// Could not load ${opts.sourceLocation.file}`); + setSourceStale(false); } } } @@ -131,6 +160,7 @@ export function App(): JSX.Element { const res = await api.readFile(resource.source.file); setSourceFile(res.path); setSourceContent(res.content); + setSourceStale(res.stale); } catch { return; } } setNav({ @@ -203,6 +233,7 @@ export function App(): JSX.Element { const res = await api.readFile(filePath); setSourceFile(res.path); setSourceContent(res.content); + setSourceStale(res.stale); } else { const data = await api.getTemplate(filePath); setTemplateFile(filePath); @@ -256,17 +287,26 @@ export function App(): JSX.Element { }>
    {sourceContent ? ( - +
    + {sourceStale && ( + + This file has been modified since the last synth, so its violations and diagnostics may be stale. Re-run cdk synth to refresh. + + )} +
    + +
    +
    ) : ( Double-click a construct to view its source. )} @@ -563,6 +603,14 @@ const FOLDER_BUTTON_STYLE: React.CSSProperties = { alignItems: 'center', }; const CODE_PANE_INNER_STYLE: React.CSSProperties = { height: '100%' }; +// Source pane stacks an optional staleness banner above the scrolling viewer. +const SOURCE_PANE_COLUMN_STYLE: React.CSSProperties = { + height: '100%', + display: 'flex', + flexDirection: 'column', + minHeight: 0, + gap: '8px', +}; const PICKER_ANCHOR_STYLE: React.CSSProperties = { position: 'relative', display: 'inline-flex', alignItems: 'center' }; const PICKER_DROPDOWN_STYLE: React.CSSProperties = { position: 'absolute', diff --git a/packages/@aws-cdk/cdk-explorer/frontend/api.ts b/packages/@aws-cdk/cdk-explorer/frontend/api.ts index 35d9e03c9..77d72fcd7 100644 --- a/packages/@aws-cdk/cdk-explorer/frontend/api.ts +++ b/packages/@aws-cdk/cdk-explorer/frontend/api.ts @@ -1,5 +1,6 @@ import { ASSEMBLY_CHANGED, + SOURCE_CHANGED, type FileResponse, type LineRange, type TemplateResource, @@ -44,13 +45,19 @@ export const api = { getViolations: (): Promise => getJson('/api/policy-validation'), getAppInfo: (): Promise => getJson('/api/info'), /** - * Subscribe to assembly-changed events from the server, invoking `onChange` - * whenever the cloud assembly is rewritten. Returns an unsubscribe that closes - * the underlying EventSource. + * Subscribe to the server's live-refresh stream. `onAssemblyChanged` fires + * when the cloud assembly is rewritten (re-fetch tree/violations); + * `onSourceChanged` fires when a source file is edited (re-check the open + * file's staleness). Returns an unsubscribe that closes the EventSource. */ - subscribe: (onChange: () => void): (() => void) => { + subscribe: (handlers: { onAssemblyChanged?: () => void; onSourceChanged?: () => void }): (() => void) => { const source = new EventSource('/api/events'); - source.addEventListener(ASSEMBLY_CHANGED, () => onChange()); + if (handlers.onAssemblyChanged) { + source.addEventListener(ASSEMBLY_CHANGED, () => handlers.onAssemblyChanged!()); + } + if (handlers.onSourceChanged) { + source.addEventListener(SOURCE_CHANGED, () => handlers.onSourceChanged!()); + } return () => source.close(); }, getTemplate: (file: string): Promise => getJson(`/api/template?file=${encodeURIComponent(file)}`), diff --git a/packages/@aws-cdk/cdk-explorer/lib/api-private.ts b/packages/@aws-cdk/cdk-explorer/lib/api-private.ts index 006d91e2d..811a6e738 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/api-private.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/api-private.ts @@ -2,3 +2,4 @@ export { findCreationStackTrace } from '../../toolkit-lib/lib/api/source-tracing/private/stack-source-tracing'; export { WATCH_EXCLUDE_DEFAULTS } from '../../toolkit-lib/lib/actions/watch/private/helpers'; export { createIgnoreMatcher } from '../../toolkit-lib/lib/util/glob-matcher'; +export { SYNTH_LOCK_FILE } from '../../toolkit-lib/lib/api/rwlock'; diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts index ce065f21e..03c839408 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/core/assembly-watcher.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { MANIFEST_FILE } from '@aws-cdk/cloud-assembly-api'; import { VALIDATION_REPORT_FILE } from '@aws-cdk/cloud-assembly-schema'; import * as chokidar from 'chokidar'; +import { SYNTH_LOCK_FILE } from '../api-private'; /** * The name of the construct tree metadata file emitted alongside the manifest. @@ -51,6 +52,12 @@ export interface AssemblyWatcherOptions { readonly assemblyDir: string; /** Invoked (debounced) when the assembly's signal files change. */ readonly onChange: () => void; + /** + * Invoked when synth write-lock activity (`synth.lock`) first appears, i.e. a + * synth started. `atMs` is the observation time. Lets a consumer date-stamp + * source-file staleness against synth start rather than synth finish. + */ + readonly onSynthActivity?: (atMs: number) => void; /** Receives non-fatal watcher errors. */ readonly onError?: (error: unknown) => void; /** @@ -90,9 +97,19 @@ export function startAssemblyWatcher(options: AssemblyWatcherOptions): AssemblyW const watcher = createWatcher(options.assemblyDir); - watcher.on('all', (_eventName, filePath) => { + watcher.on('all', (eventName, filePath) => { if (closed) return; - if (!ASSEMBLY_SIGNAL_FILES.has(path.basename(filePath))) return; + const base = path.basename(filePath); + if (base === SYNTH_LOCK_FILE) { + // toolkit-lib's `RWLock` creates this marker for the duration of a synth, + // so its appearance is our only signal that a synth has started (the + // explorer never starts synths itself). Record it, but never treat it as + // an assembly refresh: the lock's create/delete would otherwise cause + // spurious reloads. + if (eventName === 'add') options.onSynthActivity?.(Date.now()); + return; + } + if (!ASSEMBLY_SIGNAL_FILES.has(base)) return; if (timer) { clearTimeout(timer); } diff --git a/packages/@aws-cdk/cdk-explorer/lib/core/source-watcher.ts b/packages/@aws-cdk/cdk-explorer/lib/core/source-watcher.ts new file mode 100644 index 000000000..8fbefe812 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/core/source-watcher.ts @@ -0,0 +1,130 @@ +import * as chokidar from 'chokidar'; +import { WATCH_EXCLUDE_DEFAULTS, createIgnoreMatcher } from '../api-private'; +import type { FileWatcher } from './assembly-watcher'; + +// Coalesce an editor's burst of writes (e.g. save-all) into a single signal. +const DEBOUNCE_MS = 200; + +/** + * Paths under the app dir that never count as a source change: toolkit-lib's + * own watch exclusions, dependencies, our synth output (`cdk.out`), and + * dotfiles (editor configs, `.git`). Applied both at the chokidar level (so the + * tree is never traversed) and re-checked in the handler. + */ +export const SOURCE_WATCH_EXCLUDES = [ + ...WATCH_EXCLUDE_DEFAULTS, + '**/node_modules/**', + '**/cdk.out/**', + '.*', + '**/.*', + '**/.*/**', +]; + +/** A running source-tree watcher. */ +export interface SourceWatcher { + /** Stop watching and release the underlying file handles. */ + close(): Promise; +} + +export interface SourceWatcherOptions { + /** The application directory whose source tree is watched. */ + readonly appDir: string; + /** Invoked (debounced) when a non-ignored source file changes. */ + readonly onChange: () => void; + /** Receives non-fatal watcher errors. */ + readonly onError: (error: unknown) => void; + /** + * Factory for the underlying file watcher. Defaults to chokidar; overridden + * in tests with a fake so behavior is verified without real file IO. + */ + readonly createWatcher?: (appDir: string) => FileWatcher; +} + +// Thin wrapper over real chokidar; exercised via integration, not unit tests. +/* c8 ignore start */ +function defaultCreateWatcher(appDir: string): FileWatcher { + // chokidar applies the same ignore policy while traversing, so excluded paths + // (node_modules, cdk.out, dotfiles) are skipped at the source instead of + // streamed to the handler. It emits absolute paths, which the handler + // re-checks against the same policy. + return chokidar.watch(appDir, { + ignored: createIgnoreMatcher({ exclude: SOURCE_WATCH_EXCLUDES, rootDir: appDir }), + ignoreInitial: true, + }) as unknown as FileWatcher; +} +/* c8 ignore stop */ + +/** A debounced action: coalesces a burst of triggers into one delayed run. */ +interface Debounced { + /** Schedule `action`, resetting the delay if a run is already pending. */ + trigger(): void; + /** Drop a pending run, if any. */ + cancel(): void; +} + +/** + * Run `action` once, `delayMs` after the most recent `trigger()`. Each new + * trigger restarts the delay, so a burst collapses into a single trailing run. + * `cancel()` discards a pending run (used on close). This is a debounce, not a + * sleep: the pending run is cancelable and reset on every trigger. + */ +function debounce(action: () => void, delayMs: number): Debounced { + let timer: NodeJS.Timeout | undefined; + return { + trigger() { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + timer = undefined; + action(); + }, delayMs); + }, + cancel() { + if (timer) { + clearTimeout(timer); + timer = undefined; + } + }, + }; +} + +/** + * Watch a CDK app's source tree and fire `onChange` (debounced) when a + * non-ignored file changes. + */ +export function startSourceWatcher(options: SourceWatcherOptions): SourceWatcher { + const createWatcher = options.createWatcher ?? defaultCreateWatcher; + // Drop changes to ignored paths so they never raise a spurious signal. chokidar + // emits absolute paths, which the matcher's rootDir resolves before matching. + const shouldIgnore = createIgnoreMatcher({ exclude: SOURCE_WATCH_EXCLUDES, rootDir: options.appDir }); + + let closed = false; + const debounced = debounce(() => { + try { + options.onChange(); + } catch (error) { + options.onError(error); + } + }, DEBOUNCE_MS); + + const watcher = createWatcher(options.appDir); + + watcher.on('all', (_eventName, filePath) => { + if (closed) return; + if (shouldIgnore(filePath)) return; + debounced.trigger(); + }); + + watcher.on('error', (error) => { + options.onError(error); + }); + + return { + async close() { + closed = true; + debounced.cancel(); + await watcher.close(); + }, + }; +} diff --git a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts index 23b0fe5c8..b722ccebf 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/lsp/server.ts @@ -25,7 +25,7 @@ import { type Location, type RemoteConsole, } from 'vscode-languageserver/node'; -import { WATCH_EXCLUDE_DEFAULTS, createIgnoreMatcher } from '../api-private'; +import { createIgnoreMatcher } from '../api-private'; import { codeLensesForFile } from './codelens'; import { executeCommand, SUPPORTED_COMMANDS, type NotifySink } from './commands'; import { mapViolationsToDiagnostics } from './diagnostics'; @@ -45,6 +45,7 @@ import { type AssemblyWatcherOptions, } from '../core/assembly-watcher'; import { isWithinRoot } from '../core/source-resolver'; +import { SOURCE_WATCH_EXCLUDES } from '../core/source-watcher'; import type { SynthRunResult } from '../core/synth-runner'; /** @@ -399,18 +400,10 @@ export function createLspHandlers(options: LspHandlerOptions): LspHandlers { }, async onInitialized() { const projectDir = currentProjectDir(); - // Same exclusion logic as toolkit-lib's watch(): - // WATCH_EXCLUDE_DEFAULTS covers common non-source dirs, then we add cdk.out - // (our own output) and dotfiles (editor configs, .git, etc.) + // Ignore non-source paths (deps, cdk.out, dotfiles) using the same policy + // the web explorer's source watcher applies. shouldIgnore = createIgnoreMatcher({ - exclude: [ - ...WATCH_EXCLUDE_DEFAULTS, - '**/node_modules/**', - '**/cdk.out/**', - '.*', - '**/.*', - '**/.*/**', - ], + exclude: SOURCE_WATCH_EXCLUDES, rootDir: projectDir, }); await refreshFromAssembly(projectDir); diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts index 417bdb799..0897d16ad 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/protocol.ts @@ -7,12 +7,25 @@ */ export const ASSEMBLY_CHANGED = 'assembly-changed'; -/** The SSE event names the server may send. One for now. */ -export type SseEventName = typeof ASSEMBLY_CHANGED; +/** + * SSE event name sent when a source file under the app directory changes. Lets + * the SPA re-check the open file's staleness (and refresh its content) the + * instant it is edited, without waiting for the next assembly refresh. + */ +export const SOURCE_CHANGED = 'source-changed'; + +/** The SSE event names the server may send. */ +export type SseEventName = typeof ASSEMBLY_CHANGED | typeof SOURCE_CHANGED; export interface FileResponse { readonly path: string; readonly content: string; + /** + * True when the file on disk was modified after the synth that produced the + * currently loaded assembly started, so its highlighted lines and violations + * may be out of date. Best-effort (see `StalenessTracker`). + */ + readonly stale: boolean; } /** diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts index 652c5685b..307fbf139 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/routes.ts @@ -9,6 +9,7 @@ import express = require('express'); import type { LineRange, TemplateResource, TemplateResponse, TreeResponse, ViolationsResponse, WebConstructNode, WebSourceLocation, WebViolation, WebViolationOccurrence } from './protocol'; import { resolveWithinRoot } from './safe-path'; import { classifyReportSeverity, displaySeverity, severityRank } from './severity'; +import { StalenessTracker } from './staleness'; import type { AcquireAssemblyLock, AssemblyLock } from '../core/assembly-lock'; import { readAssembly as defaultReadAssembly, type AssemblyData, type AssemblyReadResult, type ConstructNode } from '../core/assembly-reader'; import type { SourceLocation } from '../core/source-resolver'; @@ -39,6 +40,14 @@ export interface ApiOptions { * mid-synth assembly. Built from the Toolkit in {@link registerApi}'s caller. */ readonly acquireAssemblyLock: AcquireAssemblyLock; + /** + * Tracks source-file staleness against synth timing; `/api/file` reads it to + * stamp each response. Injected by the server, which owns the reference: the + * assembly watcher advances it once per generation and feeds it synth-start + * activity. Defaults to a private tracker (never stale) for a standalone + * router with no watcher driving it. + */ + readonly staleness?: StalenessTracker; } export function createApiRouter(options: ApiOptions): Router { @@ -46,6 +55,7 @@ export function createApiRouter(options: ApiOptions): Router { const assemblyDir = options.assemblyDir ?? path.join(options.appDir, 'cdk.out'); const readAssembly = options.readAssembly ?? defaultReadAssembly; const acquireAssemblyLock = options.acquireAssemblyLock; + const staleness = options.staleness ?? new StalenessTracker(); let cachedAssembly: { result: AssemblyReadResult; mtimeMs: number } | undefined; /** mtime of the assembly's manifest, or undefined when no assembly exists yet. */ @@ -130,7 +140,11 @@ export function createApiRouter(options: ApiOptions): Router { if (isBinary(buffer)) { return res.status(415).json({ error: 'binary file cannot be displayed' }); } - return res.json({ path: toPosix(path.relative(appDir, resolved)), content: buffer.toString('utf-8') }); + return res.json({ + path: toPosix(path.relative(appDir, resolved)), + content: buffer.toString('utf-8'), + stale: staleness.isStale(stat.mtimeMs), + }); }); router.get('/tree', async (_req, res) => { diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts index 294b0b7bd..bfe9d3bc2 100644 --- a/packages/@aws-cdk/cdk-explorer/lib/web/server.ts +++ b/packages/@aws-cdk/cdk-explorer/lib/web/server.ts @@ -1,11 +1,14 @@ +import * as fs from 'fs'; import * as http from 'http'; import * as path from 'path'; +import { MANIFEST_FILE } from '@aws-cdk/cloud-assembly-api'; import { Toolkit, NonInteractiveIoHost } from '@aws-cdk/toolkit-lib'; // eslint-disable-next-line @typescript-eslint/no-require-imports import express = require('express'); import { SseBroadcaster } from './events'; -import { ASSEMBLY_CHANGED } from './protocol'; +import { ASSEMBLY_CHANGED, SOURCE_CHANGED } from './protocol'; import { registerApi } from './routes'; +import { StalenessTracker } from './staleness'; import { indexHtml, webAsset } from './web-assets'; import { toolkitAssemblyLock } from '../core/assembly-lock'; import { @@ -13,6 +16,11 @@ import { type AssemblyWatcher, type AssemblyWatcherOptions, } from '../core/assembly-watcher'; +import { + startSourceWatcher as defaultStartSourceWatcher, + type SourceWatcher, + type SourceWatcherOptions, +} from '../core/source-watcher'; export const DEFAULT_PORT = 4200; const MAX_PORT_ATTEMPTS = 100; @@ -35,6 +43,11 @@ export interface WebServerOptions { * overridden in tests with a fake to drive change events deterministically. */ readonly startAssemblyWatcher?: (options: AssemblyWatcherOptions) => AssemblyWatcher; + /** + * Starts the source-tree watcher that drives live staleness. Defaults to the + * real chokidar-backed watcher; overridden in tests with a fake. + */ + readonly startSourceWatcher?: (options: SourceWatcherOptions) => SourceWatcher; /** * Reports a non-fatal watcher error (live refresh stops updating). Defaults to * writing to stderr; the CLI command passes a sink that routes to its IoHost. @@ -61,16 +74,38 @@ export async function startWebServer(options: WebServerOptions = {}): Promise { + try { + return fs.statSync(path.join(assemblyDir, MANIFEST_FILE)).mtimeMs; + } catch { + return undefined; + } + }; + const app = express(); // The Toolkit provides the assembly read lock (via fromAssemblyDirectory(). // produce()); a non-interactive IoHost is fine here since stdout/stderr are // free in the web process, unlike the LSP's stdio channel. const toolkit = new Toolkit({ ioHost: new NonInteractiveIoHost() }); + // Owns source-file staleness: the assembly watcher advances its per-generation + // reference (see onChange below) and feeds it synth-start activity, and + // /api/file reads it. + const staleness = new StalenessTracker(); + // Anchor to the assembly present at startup. The watcher uses ignoreInitial so + // it never fires for an already-synthesized assembly; without this, a file + // edited before the server started would not read as stale until the next + // synth. No synth-start was observed, so this uses the manifest-mtime fallback. + const initialManifestMtime = manifestMtimeMs(); + if (initialManifestMtime !== undefined) staleness.onAssemblyRefreshed(initialManifestMtime); registerApi(app, { appDir, assemblyDir, acquireAssemblyLock: toolkitAssemblyLock(toolkit), + staleness, }); // Live-refresh stream: browsers subscribe here and re-fetch when the assembly @@ -109,11 +144,30 @@ export async function startWebServer(options: WebServerOptions = {}): Promise events.broadcast(ASSEMBLY_CHANGED), + onChange: () => { + // Advance the staleness reference to this new generation before waking + // browsers, so the /api/file they re-fetch reads the current reference. + // The watcher is the single owner of the reference: it observes both the + // synth-start lock (onSynthActivity) and the generation change here. + const mtime = manifestMtimeMs(); + if (mtime !== undefined) staleness.onAssemblyRefreshed(mtime); + events.broadcast(ASSEMBLY_CHANGED); + }, + onSynthActivity: (atMs) => staleness.noteSynthActivity(atMs), onError: options.onWatcherError ?? ((err) => process.stderr.write(`assembly watcher error: ${err instanceof Error ? err.message : String(err)}\n`)), }); + // Watch the app's source tree so an edit re-checks the open file's staleness + // (and refreshes its content) immediately, without waiting for the next synth. + const startSource = options.startSourceWatcher ?? defaultStartSourceWatcher; + const sourceWatcher = startSource({ + appDir, + onChange: () => events.broadcast(SOURCE_CHANGED), + onError: options.onWatcherError ?? ((err) => + process.stderr.write(`source watcher error: ${err instanceof Error ? err.message : String(err)}\n`)), + }); + let stopped = false; return { url: `http://${HOST}:${port}`, @@ -121,6 +175,7 @@ export async function startWebServer(options: WebServerOptions = {}): Promise((resolve) => { server.close(() => resolve()); diff --git a/packages/@aws-cdk/cdk-explorer/lib/web/staleness.ts b/packages/@aws-cdk/cdk-explorer/lib/web/staleness.ts new file mode 100644 index 000000000..425cae770 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/lib/web/staleness.ts @@ -0,0 +1,45 @@ +/** + * Tracks whether an open source file is out of date relative to the cloud + * assembly the explorer is currently showing. + * + * A file is stale when it was modified after the synth that produced the current + * assembly *started*, not merely after it finished. An edit made while a synth + * was in flight may not have been picked up by that synth, so its squiggles and + * navigation anchors can already be wrong; treating "modified after synth start" + * as stale catches that race. + * + * The reference timestamp for a generation is the moment synth write-lock + * activity (`synth.lock`) was first observed since the last refresh. If none was + * observed (the synth predated the server, or the lock was too brief to catch), + * it falls back to the assembly manifest's mtime, i.e. synth-finish time. This + * is a best-effort heuristic: at worst a squiggle sits on the wrong line until + * the next synth, which is acceptable. + */ +export class StalenessTracker { + /** Reference for the loaded generation; undefined until the first assembly read. */ + private reference: number | undefined; + + /** Earliest synth-activity timestamp seen since the last assembly refresh. */ + private synthActivitySince: number | undefined; + + /** Record synth write-lock activity (a synth started). Earliest since last refresh wins. */ + public noteSynthActivity(atMs: number): void { + if (this.synthActivitySince === undefined) { + this.synthActivitySince = atMs; + } + } + + /** + * Freeze the staleness reference for a newly loaded assembly generation. + * Prefers the observed synth start; falls back to the manifest mtime. + */ + public onAssemblyRefreshed(manifestMtimeMs: number): void { + this.reference = this.synthActivitySince ?? manifestMtimeMs; + this.synthActivitySince = undefined; + } + + /** True when a file last modified at `fileMtimeMs` is newer than the current reference. */ + public isStale(fileMtimeMs: number): boolean { + return this.reference !== undefined && fileMtimeMs > this.reference; + } +} diff --git a/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts index 89b414f54..070ad8f66 100644 --- a/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/core/assembly-watcher.test.ts @@ -36,12 +36,16 @@ class FakeWatcher implements FileWatcher { function setup() { const fake = new FakeWatcher(); const onChange = jest.fn(); + const onError = jest.fn(); + const onSynthActivity = jest.fn(); const watcher = startAssemblyWatcher({ assemblyDir: '/p/cdk.out', onChange, + onError, + onSynthActivity, createWatcher: () => fake, }); - return { fake, onChange, watcher }; + return { fake, onChange, onError, onSynthActivity, watcher }; } describe('Assembly Watcher', () => { @@ -71,6 +75,27 @@ describe('Assembly Watcher', () => { expect(onChange).not.toHaveBeenCalled(); }); + test('reports the write lock (synth.lock) appearing as synth activity', () => { + const { fake, onChange, onSynthActivity } = setup(); + + fake.emitFile('add', '/p/cdk.out/synth.lock'); + + expect(onSynthActivity).toHaveBeenCalledTimes(1); + expect(onSynthActivity).toHaveBeenCalledWith(expect.any(Number)); + // The write lock is never an assembly refresh. + jest.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('does not report synth activity for a read lock or a lock removal', () => { + const { fake, onSynthActivity } = setup(); + + fake.emitFile('add', '/p/cdk.out/read.12345.1.lock'); + fake.emitFile('unlink', '/p/cdk.out/synth.lock'); + + expect(onSynthActivity).not.toHaveBeenCalled(); + }); + test('ignores non-signal files such as templates', () => { const { fake, onChange } = setup(); diff --git a/packages/@aws-cdk/cdk-explorer/test/core/source-watcher.test.ts b/packages/@aws-cdk/cdk-explorer/test/core/source-watcher.test.ts new file mode 100644 index 000000000..964830c15 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/core/source-watcher.test.ts @@ -0,0 +1,109 @@ +import type { FileWatcher } from '../../lib/core/assembly-watcher'; +import { startSourceWatcher } from '../../lib/core/source-watcher'; + +type AnyListener = (...args: unknown[]) => void; + +/** A controllable in-memory stand-in for chokidar's FSWatcher. */ +class FakeWatcher implements FileWatcher { + public closed = false; + private readonly listeners: Record = {}; + + public on(event: string, listener: AnyListener): FileWatcher { + (this.listeners[event] ??= []).push(listener); + return this; + } + + public emitFile(eventName: string, filePath: string): void { + for (const listener of this.listeners.all ?? []) { + listener(eventName, filePath); + } + } + + public emit(event: string, ...args: unknown[]): void { + for (const listener of this.listeners[event] ?? []) { + listener(...args); + } + } + + public async close(): Promise { + this.closed = true; + } +} + +function setup() { + const fake = new FakeWatcher(); + const onChange = jest.fn(); + const onError = jest.fn(); + const watcher = startSourceWatcher({ + appDir: '/proj', + onChange, + onError, + createWatcher: () => fake, + }); + return { fake, onChange, onError, watcher }; +} + +describe('Source Watcher', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + test('fires a single debounced onChange for a burst of source edits', () => { + const { fake, onChange } = setup(); + + fake.emitFile('change', '/proj/lib/stack.ts'); + fake.emitFile('change', '/proj/app.ts'); + expect(onChange).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(200); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + test('ignores dependencies, synth output, and dotfiles', () => { + const { fake, onChange } = setup(); + + fake.emitFile('change', '/proj/node_modules/pkg/index.js'); + fake.emitFile('change', '/proj/cdk.out/tree.json'); + fake.emitFile('change', '/proj/.git/HEAD'); + + jest.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('close stops a pending onChange and closes the underlying watcher', async () => { + const { fake, onChange, watcher } = setup(); + + fake.emitFile('change', '/proj/lib/stack.ts'); + await watcher.close(); + + jest.advanceTimersByTime(500); + expect(onChange).not.toHaveBeenCalled(); + expect(fake.closed).toBe(true); + }); + + test('forwards watcher errors to onError', () => { + const { fake, onError } = setup(); + + fake.emit('error', new Error('boom')); + + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + }); + + test('forwards an onChange throw to onError instead of leaking from the timer', () => { + const fake = new FakeWatcher(); + const failure = new Error('handler blew up'); + const onError = jest.fn(); + startSourceWatcher({ + appDir: '/proj', + onChange: () => { + throw failure; + }, + onError, + createWatcher: () => fake, + }); + + fake.emitFile('change', '/proj/lib/stack.ts'); + expect(() => jest.advanceTimersByTime(200)).not.toThrow(); + + expect(onError).toHaveBeenCalledWith(failure); + }); +}); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts index ada3bee89..1d85b5bf4 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/routes.test.ts @@ -9,6 +9,7 @@ import express = require('express'); import request = require('supertest'); import type { AssemblyReadResult, ConstructNode } from '../../lib/core/assembly-reader'; import { createApiRouter } from '../../lib/web/routes'; +import { StalenessTracker } from '../../lib/web/staleness'; let appDir: string; let app: express.Express; @@ -48,7 +49,31 @@ describe('GET /api/file', () => { test('returns file content', async () => { const res = await request(app).get('/api/file').query({ path: 'app.ts' }); expect(res.status).toBe(200); - expect(res.body).toEqual({ path: 'app.ts', content: 'export const x = 1;\n' }); + expect(res.body).toEqual({ path: 'app.ts', content: 'export const x = 1;\n', stale: false }); + }); + + test('flags a file modified after the staleness reference as stale', async () => { + const staleness = new StalenessTracker(); + // Reference well before the fixture file was written, so it reads as stale. + staleness.onAssemblyRefreshed(0); + const a = express(); + a.use('/api', createApiRouter({ appDir, acquireAssemblyLock: noopAssemblyLock, staleness })); + + const res = await request(a).get('/api/file').query({ path: 'app.ts' }); + expect(res.status).toBe(200); + expect(res.body.stale).toBe(true); + }); + + test('does not flag a file older than the staleness reference', async () => { + const staleness = new StalenessTracker(); + // Reference far in the future, so no file can be newer than it. + staleness.onAssemblyRefreshed(Date.now() + 60_000); + const a = express(); + a.use('/api', createApiRouter({ appDir, acquireAssemblyLock: noopAssemblyLock, staleness })); + + const res = await request(a).get('/api/file').query({ path: 'app.ts' }); + expect(res.status).toBe(200); + expect(res.body.stale).toBe(false); }); test('requires a path parameter', async () => { diff --git a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts index ff0a46095..c7e625264 100644 --- a/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts +++ b/packages/@aws-cdk/cdk-explorer/test/web/server.test.ts @@ -1,4 +1,4 @@ -import { ASSEMBLY_CHANGED } from '../../lib/web/protocol'; +import { ASSEMBLY_CHANGED, SOURCE_CHANGED } from '../../lib/web/protocol'; import { startWebServer, DEFAULT_PORT, type WebServer } from '../../lib/web/server'; describe('Web Server', () => { @@ -115,4 +115,44 @@ describe('Web Server', () => { await reader.cancel(); }); + + test('starts a source watcher and closes it on stop', async () => { + let closed = false; + server = await startWebServer({ + startAssemblyWatcher: () => ({ close: async () => undefined }), + startSourceWatcher: (opts) => { + expect(opts.appDir).toBeDefined(); + return { + close: async () => { + closed = true; + }, + }; + }, + }); + + await server.stop(); + expect(closed).toBe(true); + }); + + test('broadcasts a source-changed event when the source watcher fires', async () => { + let fireSourceChange = (): void => undefined; + server = await startWebServer({ + startAssemblyWatcher: () => ({ close: async () => undefined }), + startSourceWatcher: (opts) => { + fireSourceChange = opts.onChange; + return { close: async () => undefined }; + }, + }); + + const res = await fetch(`${server.url}/api/events`); + const body = res.body; + if (!body) throw new Error('SSE response had no body'); + const reader = body.getReader(); + + fireSourceChange(); + const { value } = await reader.read(); + expect(new TextDecoder().decode(value)).toContain(`event: ${SOURCE_CHANGED}`); + + await reader.cancel(); + }); }); diff --git a/packages/@aws-cdk/cdk-explorer/test/web/staleness.test.ts b/packages/@aws-cdk/cdk-explorer/test/web/staleness.test.ts new file mode 100644 index 000000000..b36a6b802 --- /dev/null +++ b/packages/@aws-cdk/cdk-explorer/test/web/staleness.test.ts @@ -0,0 +1,50 @@ +import { StalenessTracker } from '../../lib/web/staleness'; + +describe('StalenessTracker', () => { + test('reports nothing stale before any assembly is loaded', () => { + const t = new StalenessTracker(); + expect(t.isStale(Number.MAX_SAFE_INTEGER)).toBe(false); + }); + + test('a file modified after the reference is stale; at or before is not', () => { + const t = new StalenessTracker(); + t.onAssemblyRefreshed(1_000); + expect(t.isStale(1_001)).toBe(true); + expect(t.isStale(1_000)).toBe(false); // equal is not newer + expect(t.isStale(999)).toBe(false); + }); + + test('prefers observed synth-start over the manifest mtime (option 2)', () => { + const t = new StalenessTracker(); + t.noteSynthActivity(500); // synth started at 500 + t.onAssemblyRefreshed(2_000); // ...and finished (manifest) at 2000 + // A file edited at 800 (during the synth) is newer than synth-start -> stale, + // even though it predates synth-finish. + expect(t.isStale(800)).toBe(true); + }); + + test('keeps the earliest synth-activity timestamp within a generation', () => { + const t = new StalenessTracker(); + t.noteSynthActivity(500); + t.noteSynthActivity(1_500); // later activity does not move the reference + t.onAssemblyRefreshed(3_000); + expect(t.isStale(600)).toBe(true); // newer than 500 + }); + + test('falls back to the manifest mtime when no synth activity was observed', () => { + const t = new StalenessTracker(); + t.onAssemblyRefreshed(2_000); + expect(t.isStale(1_500)).toBe(false); + expect(t.isStale(2_500)).toBe(true); + }); + + test('clears synth activity after a refresh so the next generation re-measures', () => { + const t = new StalenessTracker(); + t.noteSynthActivity(500); + t.onAssemblyRefreshed(2_000); // reference = 500 + // Next generation: no new activity observed -> falls back to its manifest mtime. + t.onAssemblyRefreshed(4_000); // reference = 4000 + expect(t.isStale(3_000)).toBe(false); + expect(t.isStale(4_500)).toBe(true); + }); +}); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts b/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts index cfbb18b5d..a5a714dfa 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/rwlock.ts @@ -2,6 +2,12 @@ import { promises as fs } from 'fs'; import * as path from 'path'; import { LockError } from '../toolkit/toolkit-error'; +/** + * The marker file a writer (i.e. a synth) creates in the locked directory for + * the duration of the write. + */ +export const SYNTH_LOCK_FILE = 'synth.lock'; + /** * A single-writer/multi-reader lock on a directory * @@ -21,7 +27,7 @@ export class RWLock { constructor(public readonly directory: string) { this.pidString = `${process.pid}`; - this.writerFile = path.join(this.directory, 'synth.lock'); + this.writerFile = path.join(this.directory, SYNTH_LOCK_FILE); } /**