├── .eslintrc.js ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .vscode-test.mjs ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── preview-customized.png ├── preview1.png └── preview2.png ├── jest.config.js ├── logo └── icon.png ├── package-lock.json ├── package.json ├── prettier.config.js ├── release-vsx-helper.js ├── release-vsx.sh ├── src ├── config.ts ├── decorations.ts ├── extension.ts ├── ignorePattern.ts ├── libnpmconfig.d.ts ├── npm.ts ├── npmConfig.ts ├── packageJson.ts ├── test-jest │ ├── npm.test.ts │ ├── packageJson.test.ts │ └── testdata │ │ ├── package-test1.json │ │ └── package-test2.json ├── test-vscode │ └── updateAll.test.ts ├── texteditor.ts ├── types.ts ├── updateAction.ts ├── updateAll.ts └── util │ ├── test-util.ts │ └── util.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/eslint-recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 11 | 'prettier', 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | ecmaVersion: 2021, 16 | ecmaFeatures: { 17 | jsx: true, 18 | }, 19 | project: ['./tsconfig.json'], 20 | sourceType: 'module', 21 | }, 22 | plugins: ['@typescript-eslint', 'no-only-tests'], 23 | rules: { 24 | // turn off unwanted rules: 25 | '@typescript-eslint/no-explicit-any': 'off', 26 | '@typescript-eslint/no-non-null-assertion': 'off', 27 | '@typescript-eslint/restrict-template-expressions': 'off', // this feels too verbose 28 | '@typescript-eslint/no-inferrable-types': 'off', // this brings very little value 29 | 30 | // activate extra rules: 31 | eqeqeq: ['error', 'smart'], 32 | curly: ['error'], 33 | 'no-console': ['error', { allow: ['debug', 'warn', 'error'] }], 34 | '@typescript-eslint/no-unnecessary-type-assertion': ['error'], 35 | '@typescript-eslint/no-extra-non-null-assertion': ['error'], 36 | '@typescript-eslint/no-unused-vars': [ 37 | 'error', 38 | { 39 | vars: 'all', 40 | varsIgnorePattern: '_.*', 41 | args: 'none', 42 | }, 43 | ], 44 | '@typescript-eslint/no-unnecessary-condition': ['error'], 45 | '@typescript-eslint/strict-boolean-expressions': [ 46 | 'error', 47 | { 48 | allowNullableBoolean: true, 49 | }, 50 | ], 51 | '@typescript-eslint/prefer-enum-initializers': ['error'], 52 | 'no-only-tests/no-only-tests': 'error', 53 | 'sort-imports': [ 54 | 'error', 55 | { 56 | ignoreDeclarationSort: true, 57 | }, 58 | ], 59 | '@typescript-eslint/no-misused-promises': [ 60 | 'error', 61 | { 62 | checksVoidReturn: { 63 | attributes: false, 64 | }, 65 | }, 66 | ], 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | .npmrc 6 | 7 | # local env files 8 | .env.local 9 | .env.development.local 10 | .env.test.local 11 | .env.production.local -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run pre-commit 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run pre-push 2 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli' 2 | 3 | export default defineConfig({ 4 | files: 'out/test-vscode/**/*.test.js', 5 | }) 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "${defaultBuildTask}" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .husky/** 2 | .vscode/** 3 | .vscode-test/** 4 | node_modules 5 | src/** 6 | 7 | .env.local 8 | .gitignore 9 | .vscode-test.mjs 10 | jest.config.js 11 | prettier.config.js 12 | release-vsx-helper.js 13 | release-vsx.sh 14 | 15 | **/tsconfig.json 16 | **/webpack.config.js 17 | **/*.map 18 | **/*.ts 19 | **/*.test.js 20 | out/test/** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "package-json-upgrade" extension will be documented in this file. 4 | 5 | ## Unreleased changes 6 | 7 | ## 3.2.1 8 | 9 | - Fix issue with crash in npm-registry-fetch dependency 10 | 11 | ## 3.2.0 12 | 13 | - Remove extra character in fallback update string 14 | - Make overview ruler color be affected by customized colors 15 | - Correctly identify versions with `x` wildcard as an existing version 16 | 17 | ## 3.1.0 18 | 19 | - Add option `showOverviewRulerColor` to enable hiding color indicators on the scrollbar for available updates. 20 | 21 | ## 3.0.1 22 | 23 | - Optimization to load faster 24 | 25 | ## 3.0.0 26 | 27 | - Fix an issue when large non-existing versions would not result in "version not found" text 28 | - Adjusted colors and text. If you dont like it, you can revert to the old look by adding this to your `settings.json`: 29 | 30 | Old dark theme: 31 | 32 | ``` 33 | "package-json-upgrade.decorationString": "\tUpdate available: %s", 34 | "package-json-upgrade.majorUpgradeColorOverwrite": "#E03419", 35 | "package-json-upgrade.minorUpgradeColorOverwrite": "#F8FF99", 36 | "package-json-upgrade.patchUpgradeColorOverwrite": "#19E034", 37 | "package-json-upgrade.prereleaseUpgradeColorOverwrite": "#EC33FF", 38 | ``` 39 | 40 | Old light theme: 41 | 42 | ``` 43 | "package-json-upgrade.decorationString": "\tUpdate available: %s", 44 | "package-json-upgrade.majorUpgradeColorOverwrite": "#C74632", 45 | "package-json-upgrade.minorUpgradeColorOverwrite": "#ABAB00", 46 | "package-json-upgrade.patchUpgradeColorOverwrite": "#009113", 47 | "package-json-upgrade.prereleaseUpgradeColorOverwrite": "#C433FF", 48 | ``` 49 | 50 | ## 2.1.3 51 | 52 | - Upgrades no longer shown in gitDiff view 53 | - Fix issue with flickering and disappearing upgrade text 54 | 55 | ## 2.1.2 56 | 57 | - Downgrade npm-registry-fetch to avoid issue with npm registry on localhost 58 | 59 | ## 2.1.1 60 | 61 | - Fixed issue with loading in json files associated with jsonc 62 | 63 | ## 2.1.0 64 | 65 | - The extension will also start on detected language "jsonc" as well as "json" 66 | 67 | ## 2.0.0 68 | 69 | - Package.json will now be decorated as soon as each dependency is loaded, instead of waiting for every dependency to finish. 70 | - Fix several issues related to fetching dependencies over and over again if opening the file several times. 71 | - Fix issue related to re-painting decorators when text has not changed. 72 | - Improve parsing of package.json. We now find the correct lines with correct AST-parsing instead of regexp-hack. 73 | - Added "loading" appearing on each line when dependency information is slow to load. This is configurable. 74 | 75 | ## 1.6.0 76 | 77 | - Add a warning when a non-existing version is used 78 | 79 | ## 1.5.3 80 | 81 | - Fix issue with "update all" command not respecting ignored version ranges 82 | - Fix issue with "update all" command not respecting ignored dependencies 83 | - Fix issue with "update all" command replacing "\*" with latest version 84 | 85 | ## 1.5.2 86 | 87 | - Add license and keywords 88 | 89 | ## 1.5.0 90 | 91 | - Add config to ignore semver ranges for specific dependencies 92 | 93 | ## 1.4.0 94 | 95 | - Add config for changing decoration string 96 | - Add ignorePatterns for dependencies 97 | 98 | ## 1.3.0 99 | 100 | - We no longer ignore latest-tag when current version is a prerelease. Instead latest-tag is ignored if current version is higher than latest. 101 | 102 | ## 1.2.3 103 | 104 | - Show all prereleases when current version is a prerelease. This fixes a bug when all releases were prereleases. 105 | 106 | ## 1.2.2 107 | 108 | - Fix bug in finding changelog 109 | 110 | ## 1.2.1 111 | 112 | - Fix crash when all releases for a dependency was prereleases 113 | - Fix issue with not detecting updates on prereleases using tilde or caret ranges 114 | 115 | ## 1.2.0 116 | 117 | - Do not suggest updates further than the "latest" dist-tag 118 | 119 | ## 1.1.1 120 | 121 | - Respect the per-project config file (/path/to/my/project/.npmrc) 122 | 123 | ## 1.1.0 124 | 125 | - Add support for prereleases 126 | 127 | ## 1.0.5 128 | 129 | - Avoid error message when version is '\*' or 'x' 130 | - Use the local npm configuration for all npm commands 131 | 132 | ## 1.0.4 133 | 134 | - Fix compatibility issue with v1.42.0 of vscode 135 | 136 | ## 1.0.3 137 | 138 | - Add configuration to change color of upgrade info text 139 | - Disable upgrade info text and code actions for peer dependencies since it doesn't make any sense to have them 140 | 141 | ## 1.0.2 142 | 143 | - Preserve ~ and ^ when updating 144 | - Add "update all" command 145 | 146 | ## 1.0.1 147 | 148 | - Fix parsing of versions containing ~ and ^ 149 | 150 | ## 1.0.0 151 | 152 | - Initial release 153 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Per Sandstrom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # package-json-upgrade 2 | 3 | Shows available updates in package.json. Offers quick actions to guide you in your updating. 4 | 5 | ## Preview 6 | 7 | The available updates are shown as color coded decoration to the right of each line. 8 | 9 | ![feature X](images/preview1.png) 10 | 11 | The extension adds code actions that are available through the quick fix-command. Default keyboard shortcut is "ctrl + ." 12 | 13 | These quick actions can update the dependency, but also links to the homepage and, if found, the changelog. 14 | 15 | ![feature X](images/preview2.png) 16 | 17 | The extension will pick up your npm configurations and use them, so it works with proxies, private npm registries and scopes. 18 | 19 | The extension also adds a command to update all dependencies in the package.json file. 20 | 21 | ## Extension Settings 22 | 23 | It is possible to add one or several regexp:s of dependencies that should be ignored by the extension. If you want to ignore all `@types` then your `settings.json` should look like this: 24 | 25 | ``` 26 | "package-json-upgrade.ignorePatterns": ["^@types/.+$"], 27 | ``` 28 | 29 | It is also possible to add ranges for versions that you wish to ignore. Lets say your application uses node v18. Then you can specify to ignore ">18" for "@types/node". The ranges should adhere to [node-semver](https://github.com/npm/node-semver?tab=readme-ov-file#ranges). The final json in your `settings.json` should look like this: 30 | 31 | ``` 32 | "package-json-upgrade.ignoreVersions": { 33 | "@types/node": ">18" 34 | }, 35 | ``` 36 | 37 | A config is available to control if the updates should always be shown when a package.json is opened, or if they should only be shown after triggering a command called "Toggle showing package.json available updates". This can be useful if you find that this extension is in the way when you are doing other work in your package.json file. 38 | 39 | Several more configurations exists. Check out your vscode settings for the complete list. Here is an example with customized colors and text: 40 | 41 | ![feature X](images/preview-customized.png) 42 | 43 | ## Links 44 | 45 | [Marketplace](https://marketplace.visualstudio.com/items?itemName=codeandstuff.package-json-upgrade) 46 | 47 | [How to install vscode extensions](https://code.visualstudio.com/docs/editor/extension-gallery) 48 | 49 | [ Logo Credit ](https://smashicons.com) 50 | -------------------------------------------------------------------------------- /images/preview-customized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsandstrom/package-json-upgrade/65903e61584b930d5c2f1b5a172b5d99b2894e85/images/preview-customized.png -------------------------------------------------------------------------------- /images/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsandstrom/package-json-upgrade/65903e61584b930d5c2f1b5a172b5d99b2894e85/images/preview1.png -------------------------------------------------------------------------------- /images/preview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsandstrom/package-json-upgrade/65903e61584b930d5c2f1b5a172b5d99b2894e85/images/preview2.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | rootDir: './src/test-jest', 6 | } 7 | -------------------------------------------------------------------------------- /logo/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgsandstrom/package-json-upgrade/65903e61584b930d5c2f1b5a172b5d99b2894e85/logo/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-json-upgrade", 3 | "displayName": "Package Json Upgrade", 4 | "description": "Shows available updates in package.json files. Offers quick fix command to update them and to show the changelog.", 5 | "version": "3.2.1", 6 | "publisher": "codeandstuff", 7 | "license": "MIT", 8 | "icon": "logo/icon.png", 9 | "engines": { 10 | "vscode": "^1.87.0" 11 | }, 12 | "categories": [ 13 | "Programming Languages", 14 | "Other" 15 | ], 16 | "keywords": [ 17 | "npm", 18 | "package.json", 19 | "dependencies", 20 | "upgrade", 21 | "update" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/pgsandstrom/package-json-upgrade.git" 26 | }, 27 | "activationEvents": [ 28 | "onLanguage:json", 29 | "onLanguage:jsonc" 30 | ], 31 | "main": "./out/extension.js", 32 | "contributes": { 33 | "configuration": { 34 | "type": "object", 35 | "title": "Package json upgrade", 36 | "properties": { 37 | "package-json-upgrade.showUpdatesAtStart": { 38 | "type": "boolean", 39 | "default": true, 40 | "description": "Available updates will be shown directly when a package.json is opened. Otherwise, this must be toggled with a command." 41 | }, 42 | "package-json-upgrade.showOverviewRulerColor": { 43 | "type": "boolean", 44 | "default": true, 45 | "description": "Show color indicators on the scrollbar for available updates." 46 | }, 47 | "package-json-upgrade.skipNpmConfig": { 48 | "type": "boolean", 49 | "default": false, 50 | "description": "Skip reading your npm configuration. Check this to skip using configuration such as proxies and caching that might be defined in your npm configuration. Try this out if the extension appears to not be working." 51 | }, 52 | "package-json-upgrade.majorUpgradeColorOverwrite": { 53 | "type": "string", 54 | "default": "", 55 | "description": "Specify a color (like #FF0000) to override the color of major upgrades. Leave empty for default color." 56 | }, 57 | "package-json-upgrade.minorUpgradeColorOverwrite": { 58 | "type": "string", 59 | "default": "", 60 | "description": "Specify a color (like #FFFF00) to override the color of minor upgrades. Leave empty for default color." 61 | }, 62 | "package-json-upgrade.patchUpgradeColorOverwrite": { 63 | "type": "string", 64 | "default": "", 65 | "description": "Specify a color (like #00FF00) to override the color of patch upgrades. Leave empty for default color." 66 | }, 67 | "package-json-upgrade.prereleaseUpgradeColorOverwrite": { 68 | "type": "string", 69 | "default": "", 70 | "description": "Specify a color (like #00FF00) to override the color of prerelease upgrades. Leave empty for default color." 71 | }, 72 | "package-json-upgrade.decorationString": { 73 | "type": "string", 74 | "default": "\t-> %s", 75 | "description": "Customize update string. %s will be replaced by version, so 'Update: %s' will result in 'Update: 1.0.1'." 76 | }, 77 | "package-json-upgrade.ignorePatterns": { 78 | "type": "array", 79 | "default": [], 80 | "description": "A list of regex pattern of packages to not show decoration string for. To ignore all material-ui packages the JSON should be [\"^(?=@material-ui).+$\"]" 81 | }, 82 | "package-json-upgrade.ignoreVersions": { 83 | "type": "object", 84 | "default": {}, 85 | "description": "Semver ranges of versions to ignore for specific packages. If you want to ignore all @types/node versions 12 or greater the JSON should be {\"@types/node\": \">=12\"}. If you want to add several ignored semver ranges you can do {\"@types/node\": [\"=12.0.0\", \"=12.0.1\"]}." 86 | }, 87 | "package-json-upgrade.msUntilRowLoading": { 88 | "type": "number", 89 | "default": 10000, 90 | "description": "Number of milliseconds until 'Loading...' is displayed on each dependency row that has not yet been loaded. 0 to disable, 1 to show immediately." 91 | } 92 | } 93 | }, 94 | "commands": [ 95 | { 96 | "command": "package-json-upgrade.toggle-show", 97 | "title": "Toggle showing package.json available updates" 98 | }, 99 | { 100 | "command": "package-json-upgrade.update-all", 101 | "title": "Update all dependencies in the current package.json file" 102 | } 103 | ] 104 | }, 105 | "scripts": { 106 | "vscode:prepublish": "webpack --mode production", 107 | "compile": "tsc -p ./", 108 | "watch": "tsc -watch -p ./", 109 | "pretest-vscode": "npm run compile", 110 | "test-vscode": "vscode-test", 111 | "test-jest": "jest", 112 | "test": "npm run test-jest && npm run test-vscode", 113 | "format": "prettier --write \"{,!(.vscode|.vscode-test|out|node_modules)/**/}!(package-lock).{json,md,js,jsx,ts,tsx,css,scss}\"", 114 | "lint": "eslint --ext .ts --ext .tsx --fix src", 115 | "package": "vsce package", 116 | "pre-commit": "lint-staged", 117 | "pre-push": "npm run lint && npm run typecheck && npm run test", 118 | "typecheck": "tsc --noEmit", 119 | "prepare": "husky" 120 | }, 121 | "lint-staged": { 122 | "*.{json,md,css,scss}": [ 123 | "prettier --write" 124 | ], 125 | "*.{ts,tsx}": [ 126 | "node ./node_modules/eslint/bin/eslint.js --fix", 127 | "prettier --write" 128 | ] 129 | }, 130 | "dependencies": { 131 | "@typescript-eslint/parser": "5.62.0", 132 | "libnpmconfig": "1.2.1", 133 | "node-fetch": "2.7.0", 134 | "npm-registry-fetch": "14.0.5", 135 | "semver": "7.7.1" 136 | }, 137 | "devDependencies": { 138 | "@jest/globals": "29.7.0", 139 | "@types/jest": "29.5.14", 140 | "@types/node": "22.13.9", 141 | "@types/node-fetch": "2.6.12", 142 | "@types/npm-registry-fetch": "8.0.7", 143 | "@types/semver": "7.5.8", 144 | "@types/vscode": "1.87.0", 145 | "@typescript-eslint/eslint-plugin": "5.62.0", 146 | "@vscode/vsce": "3.2.2", 147 | "@vscode/test-cli": "0.0.10", 148 | "@vscode/test-electron": "2.4.1", 149 | "cross-env": "7.0.3", 150 | "eslint": "8.57.0", 151 | "eslint-config-prettier": "9.1.0", 152 | "eslint-plugin-no-only-tests": "3.3.0", 153 | "husky": "9.1.7", 154 | "jest": "29.7.0", 155 | "lint-staged": "15.4.3", 156 | "prettier": "3.5.3", 157 | "ts-jest": "29.2.6", 158 | "ts-loader": "9.5.2", 159 | "ts-node": "10.9.2", 160 | "typescript": "5.8.2", 161 | "webpack": "5.98.0", 162 | "webpack-cli": "6.0.1" 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jsxBracketSameLine: false, 3 | printWidth: 100, 4 | semi: false, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'all', 8 | useTabs: false, 9 | } 10 | -------------------------------------------------------------------------------- /release-vsx-helper.js: -------------------------------------------------------------------------------- 1 | // quick ugly hax so I dont have to write this in bash 2 | const dirtyVer = process.argv[3] 3 | const ver = dirtyVer.replaceAll('"', '').replaceAll(',', '') 4 | console.log(ver) 5 | -------------------------------------------------------------------------------- /release-vsx.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Add the release token to .env.local as variable TOKEN. Like this: 4 | # TOKEN=my_token 5 | 6 | if [ ! -f .env.local ]; then 7 | echo ".env.local file not found!" 8 | exit 1 9 | fi 10 | 11 | # ugly hax to read .env file 12 | export $(grep -v '^#' .env.local | xargs -d '\n') 13 | 14 | DIRTY_VER=$(cat package.json | grep \"version\") 15 | VER=$(node release-vsx-helper.js $DIRTY_VER) 16 | 17 | DIRTY_NAME=$(cat package.json | grep \"name\") 18 | NAME=$(node release-vsx-helper.js $DIRTY_NAME) 19 | 20 | COMMAND="npx ovsx publish $NAME-$VER.vsix -p $TOKEN" 21 | 22 | echo 23 | echo "NAME: $NAME" 24 | echo "VERSION: $VER" 25 | echo "TOKEN: $TOKEN" 26 | echo 27 | echo "This command will be run:" 28 | echo 29 | echo $COMMAND 30 | echo 31 | read -p "Is that okay? (y/n) " -n 1 -r 32 | echo 33 | if [[ $REPLY =~ ^[Yy]$ ]] 34 | then 35 | echo "DOING RELEASE" 36 | echo 37 | echo 38 | echo 39 | $COMMAND 40 | fi -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | showUpdatesAtStart: boolean 3 | showOverviewRulerColor: boolean 4 | skipNpmConfig: boolean 5 | majorUpgradeColorOverwrite: string 6 | minorUpgradeColorOverwrite: string 7 | patchUpgradeColorOverwrite: string 8 | prereleaseUpgradeColorOverwrite: string 9 | decorationString: string 10 | ignorePatterns: string[] 11 | ignoreVersions: Record 12 | msUntilRowLoading: number 13 | } 14 | 15 | let currentConfig: Config | undefined 16 | 17 | export const getConfig = (): Config => { 18 | if (currentConfig === undefined) { 19 | throw 'config should be loaded' 20 | } 21 | return currentConfig 22 | } 23 | 24 | export const setConfig = (newConfig: Config) => { 25 | currentConfig = newConfig 26 | } 27 | -------------------------------------------------------------------------------- /src/decorations.ts: -------------------------------------------------------------------------------- 1 | import { ReleaseType } from 'semver' 2 | import { 3 | DecorationRenderOptions, 4 | OverviewRulerLane, 5 | TextEditorDecorationType, 6 | ThemableDecorationRenderOptions, 7 | window, 8 | } from 'vscode' 9 | import { getConfig } from './config' 10 | 11 | type DecorationTypeConfigurables = { 12 | overviewRulerColor: string // the color shown on the scrollbar 13 | light: ThemableDecorationRenderOptions 14 | dark: ThemableDecorationRenderOptions 15 | contentText: string 16 | } 17 | 18 | const decorateUpdatedPackage = ({ 19 | overviewRulerColor, 20 | light, 21 | dark, 22 | contentText, 23 | }: DecorationTypeConfigurables) => { 24 | const config = getConfig() 25 | const decorationType: DecorationRenderOptions = { 26 | isWholeLine: false, 27 | after: { 28 | margin: '2em', 29 | contentText, 30 | }, 31 | light, 32 | dark, 33 | } 34 | 35 | if (config.showOverviewRulerColor) { 36 | decorationType.overviewRulerLane = OverviewRulerLane.Right 37 | decorationType.overviewRulerColor = overviewRulerColor 38 | } 39 | 40 | return window.createTextEditorDecorationType(decorationType) 41 | } 42 | 43 | const decorateMajorUpdate = (contentText: string) => { 44 | const settingsColor = getConfig().majorUpgradeColorOverwrite 45 | return decorateUpdatedPackage({ 46 | overviewRulerColor: getCorrectColor(settingsColor, '#578EFF'), 47 | light: { after: { color: getCorrectColor(settingsColor, '#0028A3') } }, 48 | dark: { after: { color: getCorrectColor(settingsColor, '#578EFF') } }, 49 | contentText, 50 | }) 51 | } 52 | 53 | const decorateMinorUpdate = (contentText: string) => { 54 | const settingsColor = getConfig().minorUpgradeColorOverwrite 55 | return decorateUpdatedPackage({ 56 | overviewRulerColor: getCorrectColor(settingsColor, '#FFC757'), 57 | light: { after: { color: getCorrectColor(settingsColor, '#A37B00') } }, 58 | dark: { after: { color: getCorrectColor(settingsColor, '#FFC757') } }, 59 | contentText, 60 | }) 61 | } 62 | 63 | const decoratePatchUpdate = (contentText: string) => { 64 | const settingsColor = getConfig().patchUpgradeColorOverwrite 65 | return decorateUpdatedPackage({ 66 | overviewRulerColor: getCorrectColor(settingsColor, '#57FF73'), 67 | light: { after: { color: getCorrectColor(settingsColor, '#00A329') } }, 68 | dark: { after: { color: getCorrectColor(settingsColor, '#57FF73') } }, 69 | contentText, 70 | }) 71 | } 72 | 73 | const decoratePrereleaseUpdate = (contentText: string) => { 74 | const settingsColor = getConfig().prereleaseUpgradeColorOverwrite 75 | return decorateUpdatedPackage({ 76 | overviewRulerColor: getCorrectColor(settingsColor, '#FF57E3'), 77 | light: { after: { color: getCorrectColor(settingsColor, '#A3007A') } }, 78 | dark: { after: { color: getCorrectColor(settingsColor, '#FF57E3') } }, 79 | contentText, 80 | }) 81 | } 82 | 83 | const getCorrectColor = (settingsColor: string, defaultColor: string): string => { 84 | if (settingsColor === '') { 85 | return defaultColor 86 | } 87 | if (settingsColor.startsWith('#')) { 88 | return settingsColor 89 | } else { 90 | return `#${settingsColor}` 91 | } 92 | } 93 | 94 | export const decorateDiscreet = (contentText: string): TextEditorDecorationType => { 95 | return decorateUpdatedPackage({ 96 | overviewRulerColor: 'darkgray', 97 | light: { color: 'lightgray', after: { color: 'lightgray' } }, 98 | dark: { color: 'darkgray', after: { color: 'darkgray' } }, 99 | contentText, 100 | }) 101 | } 102 | 103 | // "major" | "premajor" | "minor" | "preminor" | "patch" | "prepatch" | "prerelease"; 104 | export const getDecoratorForUpdate = ( 105 | releaseType: ReleaseType | null, 106 | text: string, 107 | ): TextEditorDecorationType | undefined => { 108 | switch (releaseType) { 109 | case 'major': 110 | case 'premajor': 111 | return decorateMajorUpdate(text) 112 | case 'minor': 113 | case 'preminor': 114 | return decorateMinorUpdate(text) 115 | case 'patch': 116 | case 'prepatch': 117 | return decoratePatchUpdate(text) 118 | case 'prerelease': 119 | return decoratePrereleaseUpdate(text) 120 | case null: 121 | default: 122 | return undefined 123 | } 124 | } 125 | 126 | export function getUpdateDescription( 127 | latestVersion: string, 128 | currentVersionExisting: boolean, 129 | ): string { 130 | const versionString = getConfig().decorationString.replace('%s', latestVersion) 131 | if (currentVersionExisting) { 132 | return versionString 133 | } else { 134 | return `${versionString} (current version not found)` 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { Config, getConfig, setConfig } from './config' 3 | import { cleanNpmCache } from './npm' 4 | import { clearDecorations, handleFileDecoration } from './texteditor' 5 | import { UpdateAction } from './updateAction' 6 | import { updateAll } from './updateAll' 7 | 8 | export const OPEN_URL_COMMAND = 'package-json-upgrade.open-url-command' 9 | 10 | export function activate(context: vscode.ExtensionContext) { 11 | fixConfig() 12 | 13 | let showDecorations = getConfig().showUpdatesAtStart 14 | 15 | const onConfigChange = vscode.workspace.onDidChangeConfiguration((e) => { 16 | if (e.affectsConfiguration('package-json-upgrade')) { 17 | fixConfig() 18 | cleanNpmCache() 19 | clearDecorations() 20 | checkCurrentFiles(showDecorations) 21 | } 22 | }) 23 | 24 | const onDidChangeActiveTextEditor = vscode.window.onDidChangeActiveTextEditor( 25 | (texteditor: vscode.TextEditor | undefined) => { 26 | if (texteditor !== undefined) { 27 | // TODO is this really necessary? To clean everything. 28 | clearDecorations() 29 | if (showDecorations) { 30 | handleFileDecoration(texteditor.document) 31 | } 32 | } 33 | }, 34 | ) 35 | 36 | // TODO maybe have timeout on fetching dependencies instead? Now it looks weird when we delete rows 37 | let timeout: NodeJS.Timeout 38 | const onDidChangeTextDocument = vscode.workspace.onDidChangeTextDocument( 39 | (e: vscode.TextDocumentChangeEvent) => { 40 | // other sources can trigger document changes in whatever document. 41 | // Sometimes I get one from `git\scm0\input` when booting. 42 | // Then we can safely ignore the changes. 43 | if (e.document !== vscode.window.activeTextEditor?.document) { 44 | return 45 | } 46 | 47 | clearTimeout(timeout) 48 | timeout = setTimeout(() => { 49 | clearDecorations() 50 | if (showDecorations) { 51 | handleFileDecoration(e.document) 52 | } 53 | }, 500) 54 | }, 55 | ) 56 | 57 | checkCurrentFiles(showDecorations) 58 | 59 | // vscode.workspace.onDidOpenTextDocument((e: vscode.TextDocument) => {}) 60 | // vscode.workspace.onDidSaveTextDocument((e: vscode.TextDocument) => {}) 61 | // vscode.window.onDidChangeVisibleTextEditors((e: vscode.TextEditor[]) => {}) 62 | 63 | const toggleShowCommand = vscode.commands.registerCommand( 64 | 'package-json-upgrade.toggle-show', 65 | () => { 66 | showDecorations = !showDecorations 67 | checkCurrentFiles(showDecorations) 68 | }, 69 | ) 70 | 71 | const updateAllCommand = vscode.commands.registerCommand( 72 | 'package-json-upgrade.update-all', 73 | () => { 74 | updateAll(vscode.window.activeTextEditor) 75 | }, 76 | ) 77 | 78 | context.subscriptions.push( 79 | onConfigChange, 80 | onDidChangeActiveTextEditor, 81 | onDidChangeTextDocument, 82 | toggleShowCommand, 83 | updateAllCommand, 84 | ) 85 | 86 | activateCodeActionStuff(context) 87 | } 88 | 89 | const checkCurrentFiles = (showDecorations: boolean) => { 90 | vscode.window.visibleTextEditors.forEach((textEditor) => { 91 | if (showDecorations) { 92 | handleFileDecoration(textEditor.document) 93 | } else { 94 | clearDecorations() 95 | } 96 | }) 97 | } 98 | 99 | const activateCodeActionStuff = (context: vscode.ExtensionContext) => { 100 | context.subscriptions.push( 101 | vscode.languages.registerCodeActionsProvider( 102 | { pattern: '**/package.json' }, 103 | new UpdateAction(), 104 | { 105 | providedCodeActionKinds: UpdateAction.providedCodeActionKinds, 106 | }, 107 | ), 108 | ) 109 | 110 | context.subscriptions.push( 111 | vscode.commands.registerCommand(OPEN_URL_COMMAND, (url: string) => { 112 | void vscode.env.openExternal(vscode.Uri.parse(url)) 113 | }), 114 | ) 115 | } 116 | 117 | export function deactivate() { 118 | // 119 | } 120 | 121 | const fixConfig = () => { 122 | const workspaceConfig = vscode.workspace.getConfiguration('package-json-upgrade') 123 | const config: Config = { 124 | showUpdatesAtStart: workspaceConfig.get('showUpdatesAtStart') === true, 125 | showOverviewRulerColor: workspaceConfig.get('showOverviewRulerColor') === true, 126 | skipNpmConfig: workspaceConfig.get('skipNpmConfig') === true, 127 | majorUpgradeColorOverwrite: workspaceConfig.get('majorUpgradeColorOverwrite') ?? '', 128 | minorUpgradeColorOverwrite: workspaceConfig.get('minorUpgradeColorOverwrite') ?? '', 129 | patchUpgradeColorOverwrite: workspaceConfig.get('patchUpgradeColorOverwrite') ?? '', 130 | prereleaseUpgradeColorOverwrite: 131 | workspaceConfig.get('prereleaseUpgradeColorOverwrite') ?? '', 132 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 133 | decorationString: workspaceConfig.get('decorationString') || '\t-> %s', 134 | ignorePatterns: workspaceConfig.get('ignorePatterns') ?? [], 135 | ignoreVersions: 136 | workspaceConfig.get>('ignoreVersions') ?? {}, 137 | msUntilRowLoading: workspaceConfig.get('msUntilRowLoading') ?? 0, 138 | } 139 | setConfig(config) 140 | } 141 | -------------------------------------------------------------------------------- /src/ignorePattern.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { getConfig } from './config' 3 | 4 | export function getIgnorePattern(): RegExp[] { 5 | const ignorePatterns: RegExp[] = [] 6 | for (const pattern of getConfig().ignorePatterns) { 7 | try { 8 | ignorePatterns.push(new RegExp(pattern)) 9 | } catch (err) { 10 | void vscode.window.showErrorMessage( 11 | `Invalid ignore pattern ${pattern}${err instanceof Error ? `: ${err.message}` : ``}`, 12 | ) 13 | } 14 | } 15 | return ignorePatterns 16 | } 17 | 18 | export function isDependencyIgnored(dependencyName: string, ignorePatterns: RegExp[]) { 19 | for (const ignorePattern of ignorePatterns) { 20 | if (ignorePattern.exec(dependencyName) !== null) { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /src/libnpmconfig.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'libnpmconfig' 2 | -------------------------------------------------------------------------------- /src/npm.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | import * as npmRegistryFetch from 'npm-registry-fetch' 3 | import { 4 | ReleaseType, 5 | SemVer, 6 | coerce, 7 | diff, 8 | eq, 9 | gt, 10 | lte, 11 | satisfies, 12 | valid, 13 | validRange, 14 | } from 'semver' 15 | import { getConfig } from './config' 16 | import { getNpmConfig } from './npmConfig' 17 | import { AsyncState, Dict, Loader, StrictDict } from './types' 18 | 19 | export interface NpmLoader { 20 | asyncstate: AsyncState 21 | startTime: number 22 | promise?: Promise 23 | item?: T 24 | } 25 | 26 | interface PackageJson { 27 | dependencies: StrictDict 28 | devDependencies: StrictDict 29 | } 30 | 31 | interface PackageJsonDependency { 32 | versions: StrictDict 33 | } 34 | 35 | export interface NpmData { 36 | 'dist-tags': { 37 | latest: string 38 | next?: string // not used currently 39 | } 40 | versions: { 41 | [key in string]: VersionData 42 | } 43 | homepage?: string 44 | // repository: { 45 | // type: string 46 | // url: string 47 | // } 48 | } 49 | 50 | export interface VersionData { 51 | name: string 52 | version: string 53 | } 54 | 55 | export interface DependencyUpdateInfo { 56 | major?: VersionData 57 | minor?: VersionData 58 | patch?: VersionData 59 | prerelease?: VersionData 60 | validVersion: boolean // if the current version is valid semver 61 | existingVersion: boolean // if the current version exists on npm 62 | } 63 | 64 | export interface CacheItem { 65 | date: Date 66 | npmData: NpmData 67 | } 68 | 69 | let npmCache: Dict> = {} 70 | 71 | // dependencyname pointing to a potential changelog 72 | let changelogCache: Dict> = {} 73 | 74 | export const cleanNpmCache = () => { 75 | npmCache = {} 76 | changelogCache = {} 77 | } 78 | 79 | export const getAllCachedNpmData = () => { 80 | return npmCache 81 | } 82 | 83 | export const getCachedNpmData = (dependencyName: string) => { 84 | return npmCache[dependencyName] 85 | } 86 | 87 | export const setCachedNpmData = (newNpmCache: Dict>) => { 88 | npmCache = newNpmCache 89 | } 90 | 91 | export const getCachedChangelog = (dependencyName: string) => { 92 | return changelogCache[dependencyName] 93 | } 94 | 95 | export const getLatestVersion = ( 96 | npmData: NpmData, 97 | rawCurrentVersion: string, 98 | dependencyName: string, 99 | ): VersionData | undefined => { 100 | const ignoredVersions = getConfig().ignoreVersions[dependencyName] 101 | return getLatestVersionWithIgnoredVersions( 102 | npmData, 103 | rawCurrentVersion, 104 | dependencyName, 105 | ignoredVersions, 106 | ) 107 | } 108 | 109 | export const getLatestVersionWithIgnoredVersions = ( 110 | npmData: NpmData, 111 | rawCurrentVersion: string, 112 | dependencyName: string, 113 | ignoredVersions: string | undefined | string[], 114 | ): VersionData | undefined => { 115 | const possibleUpgrades = getPossibleUpgradesWithIgnoredVersions( 116 | npmData, 117 | rawCurrentVersion, 118 | dependencyName, 119 | ignoredVersions, 120 | ) 121 | return ( 122 | possibleUpgrades.major ?? 123 | possibleUpgrades.minor ?? 124 | possibleUpgrades.patch ?? 125 | possibleUpgrades.prerelease 126 | ) 127 | } 128 | 129 | export const getExactVersion = (rawVersion: string) => { 130 | return rawVersion.startsWith('~') || rawVersion.startsWith('^') 131 | ? rawVersion.substring(1) 132 | : rawVersion 133 | } 134 | 135 | export const isVersionPrerelease = (rawVersion: string) => { 136 | const version = getExactVersion(rawVersion) 137 | // regex gotten from https://github.com/semver/semver/blob/master/semver.md 138 | const result: RegExpExecArray | null = 139 | /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.exec( 140 | version, 141 | ) 142 | if (result === null) { 143 | return false 144 | } 145 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 146 | return result[4] != null 147 | } 148 | 149 | export const getPossibleUpgrades = ( 150 | npmData: NpmData, 151 | rawCurrentVersion: string, 152 | dependencyName: string, 153 | ): DependencyUpdateInfo => { 154 | const ignoredVersions = getConfig().ignoreVersions[dependencyName] 155 | return getPossibleUpgradesWithIgnoredVersions( 156 | npmData, 157 | rawCurrentVersion, 158 | dependencyName, 159 | ignoredVersions, 160 | ) 161 | } 162 | 163 | export const getPossibleUpgradesWithIgnoredVersions = ( 164 | npmData: NpmData, 165 | rawCurrentVersion: string, 166 | dependencyName: string, 167 | ignoredVersions: string | undefined | string[], 168 | ): DependencyUpdateInfo => { 169 | if (rawCurrentVersion === '*' || rawCurrentVersion === 'x') { 170 | return { validVersion: true, existingVersion: true } 171 | } 172 | 173 | const exactVersion = getExactVersion(rawCurrentVersion) 174 | 175 | const currentVersionIsPrerelease = isVersionPrerelease(exactVersion) 176 | 177 | const coercedVersion = currentVersionIsPrerelease ? exactVersion : coerce(exactVersion) 178 | if (coercedVersion === null) { 179 | return { validVersion: false, existingVersion: false } 180 | } 181 | 182 | const existingVersion = Object.values(npmData.versions).some((version) => 183 | eq(version.version, coercedVersion), 184 | ) 185 | 186 | const possibleUpgrades = getRawPossibleUpgradeList( 187 | npmData, 188 | dependencyName, 189 | ignoredVersions, 190 | coercedVersion, 191 | ) 192 | 193 | const helper = (releaseTypeList: ReleaseType[]) => { 194 | const matchingUpgradeTypes = possibleUpgrades.filter((version) => { 195 | const diffType = diff(version.version, coercedVersion) 196 | return diffType !== null && releaseTypeList.includes(diffType) 197 | }) 198 | return matchingUpgradeTypes.length === 0 199 | ? undefined 200 | : matchingUpgradeTypes.reduce((a, b) => (gt(a.version, b.version) ? a : b)) 201 | } 202 | 203 | // If we are at a prerelease, then show all pre-x. 204 | // This is partially done to account for when there are only pre-x versions. 205 | const majorUpgrade = helper(currentVersionIsPrerelease ? ['major', 'premajor'] : ['major']) 206 | const minorUpgrade = helper(currentVersionIsPrerelease ? ['minor', 'preminor'] : ['minor']) 207 | const patchUpgrade = helper(currentVersionIsPrerelease ? ['patch', 'prepatch'] : ['patch']) 208 | const prereleaseUpgrade = currentVersionIsPrerelease ? helper(['prerelease']) : undefined 209 | return { 210 | major: majorUpgrade, 211 | minor: minorUpgrade, 212 | patch: patchUpgrade, 213 | prerelease: prereleaseUpgrade, 214 | validVersion: true, 215 | existingVersion, 216 | } 217 | } 218 | 219 | const getRawPossibleUpgradeList = ( 220 | npmData: NpmData, 221 | dependencyName: string, 222 | ignoredVersions: string | undefined | string[], 223 | coercedVersion: string | SemVer, 224 | ) => { 225 | const latest = npmData['dist-tags'].latest 226 | return Object.values(npmData.versions) 227 | .filter((version) => valid(version.version)) 228 | .filter((version) => gt(version.version, coercedVersion)) 229 | .filter((version) => { 230 | if (ignoredVersions === undefined) { 231 | return true 232 | } 233 | if (Array.isArray(ignoredVersions)) { 234 | for (const ignoredVersion of ignoredVersions) { 235 | if (isVersionIgnored(version, dependencyName, ignoredVersion)) { 236 | return false 237 | } 238 | } 239 | return true 240 | } else { 241 | return !isVersionIgnored(version, dependencyName, ignoredVersions) 242 | } 243 | }) 244 | .filter((version) => { 245 | // If the current version is higher than latest, then we ignore the latest tag. 246 | // Otherwise, remove all versions higher than the latest tag 247 | return gt(coercedVersion, latest) || lte(version.version, latest) 248 | }) 249 | } 250 | 251 | const isVersionIgnored = (version: VersionData, dependencyName: string, ignoredVersion: string) => { 252 | if (validRange(ignoredVersion) === null) { 253 | console.warn( 254 | `invalid semver range detected in ignored version for depedency ${dependencyName}: ${ignoredVersion}`, 255 | ) 256 | return true 257 | } 258 | return satisfies(version.version, ignoredVersion) 259 | } 260 | 261 | export const refreshPackageJsonData = ( 262 | packageJsonString: string, 263 | packageJsonFilePath: string, 264 | ): Promise[] => { 265 | const cacheCutoff = new Date(new Date().getTime() - 1000 * 60 * 120) // 120 minutes 266 | 267 | try { 268 | const json = JSON.parse(packageJsonString) as PackageJson 269 | const dependencies = { 270 | ...json.dependencies, 271 | ...json.devDependencies, 272 | } 273 | 274 | const promises = Object.entries(dependencies) 275 | .map(([dependencyName, _version]) => { 276 | const cache = npmCache[dependencyName] 277 | if ( 278 | cache === undefined || 279 | cache.asyncstate === AsyncState.NotStarted || 280 | (cache.item !== undefined && cache.item.date.getTime() < cacheCutoff.getTime()) 281 | ) { 282 | return fetchNpmData(dependencyName, packageJsonFilePath) 283 | } else { 284 | return npmCache[dependencyName]?.promise 285 | } 286 | }) 287 | .filter((p): p is Promise => p !== undefined) 288 | 289 | return promises 290 | } catch (e) { 291 | console.warn(`Failed to parse package.json: ${packageJsonFilePath}`) 292 | return [Promise.resolve()] 293 | } 294 | } 295 | 296 | const fetchNpmData = (dependencyName: string, packageJsonPath: string) => { 297 | if ( 298 | npmCache[dependencyName] !== undefined && 299 | (npmCache[dependencyName].asyncstate === AsyncState.InProgress || 300 | npmCache[dependencyName].asyncstate === AsyncState.Rejected) 301 | ) { 302 | return npmCache[dependencyName].promise 303 | } 304 | 305 | const conf = { ...getNpmConfig(packageJsonPath), spec: dependencyName } 306 | const promise = npmRegistryFetch.json(dependencyName, conf) as unknown as Promise 307 | 308 | const startTime = new Date().getTime() 309 | npmCache[dependencyName] = { 310 | asyncstate: AsyncState.InProgress, 311 | promise, 312 | startTime, 313 | } 314 | 315 | promise 316 | .then((json) => { 317 | if (changelogCache[dependencyName] === undefined) { 318 | // we currently do not wait for this to speed things up 319 | void findChangelog(dependencyName, json) 320 | } 321 | npmCache[dependencyName] = { 322 | asyncstate: AsyncState.Fulfilled, 323 | startTime, 324 | item: { 325 | date: new Date(), 326 | npmData: json, 327 | }, 328 | } 329 | }) 330 | .catch((e) => { 331 | /* eslint-disable */ 332 | console.error(`failed to load dependency ${dependencyName}`) 333 | console.error(`status code: ${e?.statusCode}`) 334 | console.error(`uri: ${e?.uri}`) 335 | console.error(`message: ${e?.message}`) 336 | console.error(`config used: ${JSON.stringify(conf, null, 2)}`) 337 | console.error(`Entire error: ${JSON.stringify(e, null, 2)}`) 338 | /* eslint-enable */ 339 | npmCache[dependencyName] = { 340 | asyncstate: AsyncState.Rejected, 341 | startTime, 342 | } 343 | }) 344 | 345 | return promise 346 | } 347 | 348 | const findChangelog = async (dependencyName: string, npmData: NpmData) => { 349 | if (npmData.homepage === undefined) { 350 | return 351 | } 352 | // TODO support other stuff than github? 353 | const regexResult = /(https?:\/\/github\.com\/[-\w/.]*\/[-\w/.]*)(#[-\w/.]*)?/.exec( 354 | npmData.homepage, 355 | ) 356 | if (regexResult === null) { 357 | return 358 | } 359 | 360 | changelogCache[dependencyName] = { 361 | asyncstate: AsyncState.InProgress, 362 | } 363 | const baseGithubUrl = regexResult[1] 364 | const changelogUrl = `${baseGithubUrl}/blob/master/CHANGELOG.md` 365 | const result = await fetch(changelogUrl) 366 | if (result.status >= 200 && result.status < 300) { 367 | changelogCache[dependencyName] = { 368 | asyncstate: AsyncState.Fulfilled, 369 | item: changelogUrl, 370 | } 371 | } else { 372 | changelogCache[dependencyName] = { 373 | asyncstate: AsyncState.Rejected, 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/npmConfig.ts: -------------------------------------------------------------------------------- 1 | import * as config from 'libnpmconfig' 2 | import * as npmRegistryFetch from 'npm-registry-fetch' 3 | import { getConfig } from './config' 4 | import { Dict } from './types' 5 | 6 | let skippedNpmConfigLastTime: boolean | undefined 7 | 8 | const packageJsonPathToConfMap: Dict = {} 9 | 10 | export const getNpmConfig = (packageJsonPath: string): npmRegistryFetch.Options => { 11 | let conf = packageJsonPathToConfMap[packageJsonPath] 12 | const skipNpmConfig = getConfig().skipNpmConfig 13 | if (conf === undefined || skipNpmConfig !== skippedNpmConfigLastTime) { 14 | if (skipNpmConfig) { 15 | conf = {} 16 | console.debug('Defaulting to empty config') 17 | } else { 18 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call 19 | conf = config 20 | .read( 21 | { 22 | // here we can override config 23 | // currently disable cache since it seems to be buggy with npm-registry-fetch 24 | // the bug was supposedly fixed here: https://github.com/npm/npm-registry-fetch/issues/23 25 | // but I still have issues, and not enough time to investigate 26 | // TODO: Investigate why the cache causes issues 27 | cache: null, 28 | // registry: 'https://registry.npmjs.org', 29 | }, 30 | { cwd: packageJsonPath }, 31 | ) 32 | .toJSON() as npmRegistryFetch.Options 33 | packageJsonPathToConfMap[packageJsonPath] = conf 34 | } 35 | 36 | skippedNpmConfigLastTime = skipNpmConfig 37 | } 38 | return conf 39 | } 40 | -------------------------------------------------------------------------------- /src/packageJson.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { parse } from '@typescript-eslint/parser' 3 | import { TSESTree } from '@typescript-eslint/types' 4 | import { VariableDeclaration } from '@typescript-eslint/types/dist/generated/ast-spec' 5 | 6 | export interface DependencyGroups { 7 | startLine: number 8 | deps: Dependency[] 9 | } 10 | 11 | export interface Dependency { 12 | dependencyName: string 13 | currentVersion: string 14 | line: number 15 | } 16 | 17 | export const getDependencyFromLine = (jsonAsString: string, line: number) => { 18 | const dependencies = getDependencyInformation(jsonAsString) 19 | .map((d) => d.deps) 20 | .flat() 21 | 22 | return dependencies.find((d) => d.line === line) 23 | } 24 | 25 | export const getDependencyInformation = (jsonAsString: string): DependencyGroups[] => { 26 | const jsonAsTypescript = `let tmp=${jsonAsString}` 27 | 28 | const ast = parse(jsonAsTypescript, { 29 | loc: true, 30 | }) 31 | 32 | const variable = ast.body[0] as VariableDeclaration 33 | 34 | const tmp = variable.declarations[0] 35 | 36 | const init = tmp.init 37 | if (init == null || init.type !== 'ObjectExpression') { 38 | throw new Error(`unexpected type: ${init?.type}`) 39 | } 40 | 41 | const properties = init.properties as TSESTree.Property[] 42 | 43 | const dependencies = properties.find( 44 | (p) => (p.key as TSESTree.StringLiteral).value === 'dependencies', 45 | ) 46 | 47 | const devDependencies = properties.find( 48 | (p) => (p.key as TSESTree.StringLiteral).value === 'devDependencies', 49 | ) 50 | 51 | return [dependencies, devDependencies] 52 | .filter((i): i is TSESTree.Property => i !== undefined) 53 | .map(toDependencyGroup) 54 | } 55 | 56 | function toDependencyGroup(dependencyProperty: TSESTree.Property): DependencyGroups { 57 | if (dependencyProperty.value.type !== 'ObjectExpression') { 58 | throw new Error('unexpected type') 59 | } 60 | const dependencies = dependencyProperty.value.properties as TSESTree.Property[] 61 | 62 | const d = dependencies.map((dep) => { 63 | return { 64 | dependencyName: (dep.key as TSESTree.StringLiteral).value, 65 | currentVersion: (dep.value as TSESTree.StringLiteral).value, 66 | // TODO investigate exactly why we have "off by one" error 67 | line: dep.loc.end.line - 1, 68 | } 69 | }) 70 | 71 | return { 72 | startLine: dependencyProperty.loc.start.line - 1, 73 | deps: d, 74 | } 75 | } 76 | 77 | export const isPackageJson = (document: vscode.TextDocument) => { 78 | // Is checking both slashes necessary? Test on linux and mac. 79 | return document.fileName.endsWith('\\package.json') || document.fileName.endsWith('/package.json') 80 | } 81 | -------------------------------------------------------------------------------- /src/test-jest/npm.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { 4 | DependencyUpdateInfo, 5 | NpmData, 6 | VersionData, 7 | getLatestVersionWithIgnoredVersions, 8 | getPossibleUpgrades, 9 | getPossibleUpgradesWithIgnoredVersions, 10 | } from '../npm' 11 | import { Config, setConfig } from '../config' 12 | 13 | const testData: NpmData = { 14 | 'dist-tags': { 15 | latest: '2.1.1', 16 | }, 17 | versions: { 18 | '1.0.0': { 19 | name: 'dependencyName', 20 | version: '1.0.0', 21 | }, 22 | '1.0.1': { 23 | name: 'dependencyName', 24 | version: '1.0.1', 25 | }, 26 | '1.1.0': { 27 | name: 'dependencyName', 28 | version: '1.1.0', 29 | }, 30 | '1.1.1': { 31 | name: 'dependencyName', 32 | version: '1.1.1', 33 | }, 34 | '2.0.0-alpha.1': { 35 | name: 'dependencyName', 36 | version: '2.0.0-alpha.1', 37 | }, 38 | '2.0.0-alpha.2': { 39 | name: 'dependencyName', 40 | version: '2.0.0-alpha.2', 41 | }, 42 | '2.0.0': { 43 | name: 'dependencyName', 44 | version: '2.0.0', 45 | }, 46 | '2.1.0': { 47 | name: 'dependencyName', 48 | version: '2.1.0', 49 | }, 50 | '2.1.1': { 51 | name: 'dependencyName', 52 | version: '2.1.1', 53 | }, 54 | '3.0.0-alpha.1': { 55 | name: 'dependencyName', 56 | version: '3.0.0-alpha.1', 57 | }, 58 | '3.0.0-alpha.2': { 59 | name: 'dependencyName', 60 | version: '3.0.0-alpha.2', 61 | }, 62 | }, 63 | } 64 | 65 | describe('Npm Test Suite', () => { 66 | beforeAll(() => { 67 | const config: Config = { 68 | showUpdatesAtStart: true, 69 | showOverviewRulerColor: true, 70 | skipNpmConfig: true, 71 | majorUpgradeColorOverwrite: '', 72 | minorUpgradeColorOverwrite: '', 73 | patchUpgradeColorOverwrite: '', 74 | prereleaseUpgradeColorOverwrite: '', 75 | decorationString: '', 76 | ignorePatterns: [], 77 | ignoreVersions: {}, 78 | msUntilRowLoading: 6000, 79 | } 80 | setConfig(config) 81 | }) 82 | 83 | test('Major upgrade', () => { 84 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '1.1.1', 'dependencyName') 85 | const expected: DependencyUpdateInfo = { 86 | major: { name: 'dependencyName', version: '2.1.1' }, 87 | minor: undefined, 88 | patch: undefined, 89 | prerelease: undefined, 90 | validVersion: true, 91 | existingVersion: true, 92 | } 93 | assert.deepStrictEqual(result, expected) 94 | }) 95 | 96 | test('Minor upgrade', () => { 97 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '2.0.0', 'dependencyName') 98 | const expected: DependencyUpdateInfo = { 99 | major: undefined, 100 | minor: { name: 'dependencyName', version: '2.1.1' }, 101 | patch: undefined, 102 | prerelease: undefined, 103 | validVersion: true, 104 | existingVersion: true, 105 | } 106 | assert.deepStrictEqual(result, expected) 107 | }) 108 | 109 | test('Patch upgrade', () => { 110 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '2.1.0', 'dependencyName') 111 | const expected: DependencyUpdateInfo = { 112 | major: undefined, 113 | minor: undefined, 114 | patch: { name: 'dependencyName', version: '2.1.1' }, 115 | prerelease: undefined, 116 | validVersion: true, 117 | existingVersion: true, 118 | } 119 | assert.deepStrictEqual(result, expected) 120 | }) 121 | 122 | test('Many upgrades', () => { 123 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '1.0.0', 'dependencyName') 124 | const expected: DependencyUpdateInfo = { 125 | major: { name: 'dependencyName', version: '2.1.1' }, 126 | minor: { name: 'dependencyName', version: '1.1.1' }, 127 | patch: { name: 'dependencyName', version: '1.0.1' }, 128 | prerelease: undefined, 129 | validVersion: true, 130 | existingVersion: true, 131 | } 132 | assert.deepStrictEqual(result, expected) 133 | }) 134 | 135 | test('Invalid version', () => { 136 | const result: DependencyUpdateInfo = getPossibleUpgrades( 137 | testData, 138 | 'non-existing-version', 139 | 'dependencyName', 140 | ) 141 | const expected: DependencyUpdateInfo = { 142 | validVersion: false, 143 | existingVersion: false, 144 | } 145 | assert.deepStrictEqual(result, expected) 146 | }) 147 | 148 | test('Prerelease upgrade', () => { 149 | const result: DependencyUpdateInfo = getPossibleUpgrades( 150 | testData, 151 | '3.0.0-alpha.1', 152 | 'dependencyName', 153 | ) 154 | const expected: DependencyUpdateInfo = { 155 | major: undefined, 156 | minor: undefined, 157 | patch: undefined, 158 | prerelease: { name: 'dependencyName', version: '3.0.0-alpha.2' }, 159 | validVersion: true, 160 | existingVersion: true, 161 | } 162 | assert.deepStrictEqual(result, expected) 163 | }) 164 | 165 | test('Prerelease upgrade with inexact version', () => { 166 | const result: DependencyUpdateInfo = getPossibleUpgrades( 167 | testData, 168 | '^3.0.0-alpha.1', 169 | 'dependencyName', 170 | ) 171 | const expected: DependencyUpdateInfo = { 172 | major: undefined, 173 | minor: undefined, 174 | patch: undefined, 175 | prerelease: { name: 'dependencyName', version: '3.0.0-alpha.2' }, 176 | validVersion: true, 177 | existingVersion: true, 178 | } 179 | assert.deepStrictEqual(result, expected) 180 | }) 181 | 182 | test('Prerelease upgrade to final', () => { 183 | const result: DependencyUpdateInfo = getPossibleUpgrades( 184 | testData, 185 | '2.0.0-alpha.1', 186 | 'dependencyName', 187 | ) 188 | const expected: DependencyUpdateInfo = { 189 | existingVersion: true, 190 | major: { name: 'dependencyName', version: '2.1.1' }, 191 | minor: undefined, 192 | patch: undefined, 193 | prerelease: { name: 'dependencyName', version: '2.0.0-alpha.2' }, 194 | validVersion: true, 195 | } 196 | assert.deepStrictEqual(result, expected) 197 | }) 198 | 199 | const testDataWithLatest: NpmData = { 200 | 'dist-tags': { 201 | latest: '1.0.0', 202 | }, 203 | versions: { 204 | '1.0.0': { 205 | name: 'dependencyName', 206 | version: '1.0.0', 207 | }, 208 | '2.0.0': { 209 | name: 'dependencyName', 210 | version: '2.0.0', 211 | }, 212 | '2.0.1': { 213 | name: 'dependencyName', 214 | version: '2.0.1', 215 | }, 216 | }, 217 | } 218 | 219 | test('Latest dist-tag blocks major upgrade', () => { 220 | const result: DependencyUpdateInfo = getPossibleUpgrades( 221 | testDataWithLatest, 222 | '1.0.0', 223 | 'dependencyName', 224 | ) 225 | const expected: DependencyUpdateInfo = { 226 | major: undefined, 227 | minor: undefined, 228 | patch: undefined, 229 | prerelease: undefined, 230 | validVersion: true, 231 | existingVersion: true, 232 | } 233 | assert.deepStrictEqual(result, expected) 234 | }) 235 | 236 | test('Latest dist-tag ignored if current version is already higher than latest dist-tag', () => { 237 | const result: DependencyUpdateInfo = getPossibleUpgrades( 238 | testDataWithLatest, 239 | '2.0.0', 240 | 'dependencyName', 241 | ) 242 | const expected: DependencyUpdateInfo = { 243 | major: undefined, 244 | minor: undefined, 245 | patch: { name: 'dependencyName', version: '2.0.1' }, 246 | prerelease: undefined, 247 | validVersion: true, 248 | existingVersion: true, 249 | } 250 | assert.deepStrictEqual(result, expected) 251 | }) 252 | 253 | const testDataWithOnlyPrereleases: NpmData = { 254 | 'dist-tags': { 255 | latest: '2.0.0-build100', 256 | }, 257 | versions: { 258 | '1.0.0-build100': { 259 | name: 'dependencyName', 260 | version: '1.0.0-build100', 261 | }, 262 | '2.0.0-build100': { 263 | name: 'dependencyName', 264 | version: '2.0.0-build100', 265 | }, 266 | }, 267 | } 268 | 269 | test('Should work even if all releases are pre-releases', () => { 270 | const result: DependencyUpdateInfo = getPossibleUpgrades( 271 | testDataWithOnlyPrereleases, 272 | '1.0.1-build100', 273 | 'dependencyName', 274 | ) 275 | const expected: DependencyUpdateInfo = { 276 | major: { 277 | name: 'dependencyName', 278 | version: '2.0.0-build100', 279 | }, 280 | minor: undefined, 281 | patch: undefined, 282 | prerelease: undefined, 283 | validVersion: true, 284 | existingVersion: false, 285 | } 286 | assert.deepStrictEqual(result, expected) 287 | }) 288 | 289 | test('Ignored versions should work', () => { 290 | const result: DependencyUpdateInfo = getPossibleUpgradesWithIgnoredVersions( 291 | testData, 292 | '1.1.1', 293 | 'dependencyName', 294 | '>=2.1.1', 295 | ) 296 | const expected: DependencyUpdateInfo = { 297 | major: { name: 'dependencyName', version: '2.1.0' }, 298 | minor: undefined, 299 | patch: undefined, 300 | prerelease: undefined, 301 | validVersion: true, 302 | existingVersion: true, 303 | } 304 | assert.deepStrictEqual(result, expected) 305 | }) 306 | 307 | test('Multiple ignored versions should work', () => { 308 | const result: DependencyUpdateInfo = getPossibleUpgradesWithIgnoredVersions( 309 | testData, 310 | '1.1.1', 311 | 'dependencyName', 312 | ['=2.1.1', '=2.1.0'], 313 | ) 314 | const expected: DependencyUpdateInfo = { 315 | major: { name: 'dependencyName', version: '2.0.0' }, 316 | minor: undefined, 317 | patch: undefined, 318 | prerelease: undefined, 319 | validVersion: true, 320 | existingVersion: true, 321 | } 322 | assert.deepStrictEqual(result, expected) 323 | }) 324 | 325 | test('getLatestVersion major', () => { 326 | const result: VersionData | undefined = getLatestVersionWithIgnoredVersions( 327 | testData, 328 | '1.1.1', 329 | 'dependencyName', 330 | ['=2.1.1', '=2.1.0'], 331 | ) 332 | const expected: VersionData = { 333 | name: 'dependencyName', 334 | version: '2.0.0', 335 | } 336 | assert.deepStrictEqual(result, expected) 337 | }) 338 | 339 | test('getLatestVersion patch', () => { 340 | const result: VersionData | undefined = getLatestVersionWithIgnoredVersions( 341 | testData, 342 | '2.1.0', 343 | 'dependencyName', 344 | [], 345 | ) 346 | const expected: VersionData = { 347 | name: 'dependencyName', 348 | version: '2.1.1', 349 | } 350 | assert.deepStrictEqual(result, expected) 351 | }) 352 | 353 | test('getLatestVersion star', () => { 354 | const result: VersionData | undefined = getLatestVersionWithIgnoredVersions( 355 | testData, 356 | '*', 357 | 'dependencyName', 358 | [], 359 | ) 360 | assert.deepStrictEqual(result, undefined) 361 | }) 362 | 363 | test('existingVersion should work with caret', () => { 364 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '^1.1.1', 'dependencyName') 365 | const expected: DependencyUpdateInfo = { 366 | major: { name: 'dependencyName', version: '2.1.1' }, 367 | minor: undefined, 368 | patch: undefined, 369 | prerelease: undefined, 370 | validVersion: true, 371 | existingVersion: true, 372 | } 373 | assert.deepStrictEqual(result, expected) 374 | }) 375 | 376 | test('existingVersion should work with tilde', () => { 377 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '~1.1.1', 'dependencyName') 378 | const expected: DependencyUpdateInfo = { 379 | major: { name: 'dependencyName', version: '2.1.1' }, 380 | minor: undefined, 381 | patch: undefined, 382 | prerelease: undefined, 383 | validVersion: true, 384 | existingVersion: true, 385 | } 386 | assert.deepStrictEqual(result, expected) 387 | }) 388 | 389 | test('existingVersion should be true when version does not exist', () => { 390 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '1.1.11', 'dependencyName') 391 | const expected: DependencyUpdateInfo = { 392 | major: { name: 'dependencyName', version: '2.1.1' }, 393 | minor: undefined, 394 | patch: undefined, 395 | prerelease: undefined, 396 | validVersion: true, 397 | existingVersion: false, 398 | } 399 | assert.deepStrictEqual(result, expected) 400 | }) 401 | 402 | test('version such as 1.x should be correctly identified as a current version', () => { 403 | const result: DependencyUpdateInfo = getPossibleUpgrades(testData, '1.x', 'dependencyName') 404 | const expected: DependencyUpdateInfo = { 405 | major: { name: 'dependencyName', version: '2.1.1' }, 406 | minor: { name: 'dependencyName', version: '1.1.1' }, 407 | patch: { name: 'dependencyName', version: '1.0.1' }, 408 | prerelease: undefined, 409 | validVersion: true, 410 | existingVersion: true, 411 | } 412 | assert.deepStrictEqual(result, expected) 413 | }) 414 | }) 415 | -------------------------------------------------------------------------------- /src/test-jest/packageJson.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { readFileSync } from 'fs' 4 | import { getDependencyInformation } from '../packageJson' 5 | 6 | describe('packageJson', () => { 7 | test('should be able to correctly parse a simple package.json', () => { 8 | const packageJsonBuffer = readFileSync('./src/test-jest/testdata/package-test1.json') 9 | const packageJson = packageJsonBuffer.toString() 10 | const result = getDependencyInformation(packageJson) 11 | const dependencies = result.map((r) => r.deps).flat() 12 | if ( 13 | !dependencies.some( 14 | (dep) => 15 | dep.dependencyName === 'npm-registry-fetch' && 16 | dep.currentVersion === '12.0.0' && 17 | dep.line === 22, 18 | ) 19 | ) { 20 | assert.fail('did not find npm-registry-fetch') 21 | } 22 | 23 | if ( 24 | !dependencies.some( 25 | (dep) => 26 | dep.dependencyName === '@types/npm-registry-fetch' && 27 | dep.currentVersion === '8.0.4' && 28 | dep.line === 30, 29 | ) 30 | ) { 31 | assert.fail('did not find @types/npm-registry-fetch') 32 | } 33 | 34 | assert.ok('nice') 35 | }) 36 | 37 | test('should be able to correctly parse another simple package.json', () => { 38 | const packageJsonBuffer = readFileSync('./src/test-jest/testdata/package-test2.json') 39 | const packageJson = packageJsonBuffer.toString() 40 | const result = getDependencyInformation(packageJson) 41 | assert.deepStrictEqual(result, [ 42 | { 43 | deps: [ 44 | { currentVersion: '7.2.0', dependencyName: '@types/glob', line: 12 }, 45 | { currentVersion: '2.6.7', dependencyName: 'node-fetch', line: 13 }, 46 | ], 47 | startLine: 11, 48 | }, 49 | ]) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/test-jest/testdata/package-test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-json-upgrade", 3 | "displayName": "Package Json Upgrade", 4 | "description": "Shows available updates in package.json files. Offers quick fix command to update them and to show the changelog.", 5 | "version": "1.6.0", 6 | "publisher": "codeandstuff", 7 | "license": "MIT", 8 | "icon": "logo/icon.png", 9 | "engines": { 10 | "vscode": "^1.41.1" 11 | }, 12 | "categories": ["Programming Languages", "Other"], 13 | "keywords": ["npm", "package.json", "dependencies", "upgrade", "update"], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/pgsandstrom/package-json-upgrade.git" 17 | }, 18 | "activationEvents": ["onLanguage:json"], 19 | "main": "./out/extension.js", 20 | "dependencies": { 21 | "libnpmconfig": "1.2.1", 22 | "node-fetch": "2.6.7", 23 | "npm-registry-fetch": "12.0.0", 24 | "semver": "7.3.5" 25 | }, 26 | "devDependencies": { 27 | "@types/glob": "7.2.0", 28 | "@types/mocha": "9.1.0", 29 | "@types/node": "14.18.9", 30 | "@types/node-fetch": "2.6.1", 31 | "@types/npm-registry-fetch": "8.0.4", 32 | "@types/semver": "7.3.9", 33 | "@types/vscode": "1.41.0", 34 | "@typescript-eslint/eslint-plugin": "5.12.0", 35 | "@typescript-eslint/parser": "5.12.0", 36 | "cross-env": "7.0.3", 37 | "eslint": "8.9.0", 38 | "eslint-config-prettier": "8.3.0", 39 | "glob": "7.2.0", 40 | "husky": "7.0.4", 41 | "lint-staged": "12.3.4", 42 | "mocha": "9.2.0", 43 | "prettier": "2.5.1", 44 | "ts-loader": "9.2.6", 45 | "typescript": "4.5.5", 46 | "vsce": "2.6.7", 47 | "vscode-test": "1.6.1", 48 | "webpack": "5.69.0", 49 | "webpack-cli": "4.9.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test-jest/testdata/package-test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": ["dependencies"], 3 | "foo": "dependencies", 4 | "bar": { 5 | "dependencies": { 6 | "libnpmconfig": "1.2.1", 7 | "node-fetch": "2.6.7", 8 | "npm-registry-fetch": "12.0.0", 9 | "semver": "7.3.5" 10 | } 11 | }, 12 | "dependencies": { 13 | "@types/glob": "7.2.0", 14 | "node-fetch": "2.6.7" 15 | }, 16 | "devDependencies---LAME": { 17 | "@types/glob": "7.2.0", 18 | "@types/npm-registry-fetch": "8.0.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test-vscode/updateAll.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as assert from 'assert' 3 | 4 | import { updateAll } from '../updateAll' 5 | import { CacheItem, NpmLoader, setCachedNpmData } from '../npm' 6 | import { AsyncState, Dict } from '../types' 7 | import { Config, setConfig } from '../config' 8 | 9 | const packageJsonTestContent = ` 10 | { 11 | "dependencies": { 12 | }, 13 | "devDependencies": { 14 | "@emotion/babel-plugin": "^11.0.0-next.12" 15 | } 16 | } 17 | ` 18 | 19 | const npmCache: Dict> = { 20 | '@emotion/babel-plugin': { 21 | asyncstate: AsyncState.Fulfilled, 22 | startTime: 0, 23 | item: { 24 | date: new Date('2020-09-14T11:01:26.768Z'), 25 | npmData: { 26 | 'dist-tags': { next: '11.0.0-next.10', latest: '11.0.0-next.17' }, 27 | versions: { 28 | '11.0.0-next.10': { 29 | name: '@emotion/babel-plugin', 30 | version: '11.0.0-next.10', 31 | }, 32 | '11.0.0-next.11': { 33 | name: '@emotion/babel-plugin', 34 | version: '11.0.0-next.11', 35 | }, 36 | '11.0.0-next.12': { 37 | name: '@emotion/babel-plugin', 38 | version: '11.0.0-next.12', 39 | }, 40 | '11.0.0-next.13': { 41 | name: '@emotion/babel-plugin', 42 | version: '11.0.0-next.13', 43 | }, 44 | '11.0.0-next.15': { 45 | name: '@emotion/babel-plugin', 46 | version: '11.0.0-next.15', 47 | }, 48 | '11.0.0-next.16': { 49 | name: '@emotion/babel-plugin', 50 | version: '11.0.0-next.16', 51 | }, 52 | '11.0.0-next.17': { 53 | name: '@emotion/babel-plugin', 54 | version: '11.0.0-next.17', 55 | }, 56 | }, 57 | homepage: 'https://emotion.sh', 58 | }, 59 | }, 60 | }, 61 | } 62 | 63 | suite('UpdateAll Test Suite', () => { 64 | test('When all releases are prereleases', async function () { 65 | const config: Config = { 66 | showUpdatesAtStart: true, 67 | showOverviewRulerColor: true, 68 | skipNpmConfig: true, 69 | majorUpgradeColorOverwrite: '', 70 | minorUpgradeColorOverwrite: '', 71 | patchUpgradeColorOverwrite: '', 72 | prereleaseUpgradeColorOverwrite: '', 73 | decorationString: '', 74 | ignorePatterns: [], 75 | ignoreVersions: {}, 76 | msUntilRowLoading: 6000, 77 | } 78 | setConfig(config) 79 | 80 | const uri = vscode.Uri.parse(`./tmp/package.json`) 81 | await vscode.workspace.fs.writeFile(uri, Buffer.from(packageJsonTestContent)) 82 | const packageJsonTest = await vscode.workspace.openTextDocument(uri) 83 | const textDocument = await vscode.window.showTextDocument(packageJsonTest) 84 | 85 | const expected = [ 86 | { 87 | range: [ 88 | { line: 5, character: 0 }, 89 | { line: 5, character: 46 }, 90 | ], 91 | text: ' "@emotion/babel-plugin": "^11.0.0-next.17"', 92 | }, 93 | ] 94 | 95 | setCachedNpmData(npmCache) 96 | 97 | const result = updateAll(textDocument) 98 | 99 | assert.deepStrictEqual(JSON.stringify(result), JSON.stringify(expected)) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /src/texteditor.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { decorateDiscreet, getDecoratorForUpdate, getUpdateDescription } from './decorations' 3 | import { getIgnorePattern, isDependencyIgnored } from './ignorePattern' 4 | import { getCachedNpmData, getPossibleUpgrades, refreshPackageJsonData } from './npm' 5 | import { DependencyGroups, getDependencyInformation, isPackageJson } from './packageJson' 6 | import { AsyncState } from './types' 7 | import { TextEditorDecorationType } from 'vscode' 8 | import { getConfig } from './config' 9 | 10 | interface DecorationWrapper { 11 | line: number 12 | text: string 13 | decoration: TextEditorDecorationType 14 | } 15 | 16 | function isDiffView() { 17 | const schemes = vscode.window.visibleTextEditors.map((editor) => editor.document.uri.scheme) 18 | return schemes.length === 2 && schemes.includes('git') && schemes.includes('file') 19 | } 20 | 21 | // If a user opens the same package.json several times quickly, several "loads" of decorators will 22 | // be ongoing at the same time. So here we keep track of the latest start time and only use that. 23 | const decorationStart: Record = {} 24 | 25 | let rowToDecoration: Record = {} 26 | 27 | export const handleFileDecoration = (document: vscode.TextDocument) => { 28 | if (isDiffView()) { 29 | return 30 | } 31 | 32 | if (!isPackageJson(document)) { 33 | return 34 | } 35 | 36 | const startTime = new Date().getTime() 37 | decorationStart[document.fileName] = startTime 38 | 39 | void loadDecoration(document, startTime) 40 | } 41 | 42 | const loadDecoration = async (document: vscode.TextDocument, startTime: number) => { 43 | const text = document.getText() 44 | const dependencyGroups = getDependencyInformation(text) 45 | 46 | const textEditor = getTextEditorFromDocument(document) 47 | if (textEditor === undefined) { 48 | return 49 | } 50 | 51 | const promises = refreshPackageJsonData(document.getText(), document.uri.fsPath) 52 | 53 | try { 54 | await Promise.race([...promises, Promise.resolve()]) 55 | } catch (e) { 56 | // 57 | } 58 | 59 | // initial paint 60 | const stillLoading = promises.length !== 0 61 | paintDecorations(document, dependencyGroups, stillLoading, startTime) 62 | 63 | return waitForPromises(promises, document, dependencyGroups, startTime) 64 | } 65 | 66 | const waitForPromises = async ( 67 | promises: Promise[], 68 | document: vscode.TextDocument, 69 | dependencyGroups: DependencyGroups[], 70 | startTime: number, 71 | ) => { 72 | let newSettled = false 73 | 74 | if (promises.length === 0) { 75 | return 76 | } 77 | 78 | promises.forEach((promise) => { 79 | void promise 80 | .then(() => { 81 | newSettled = true 82 | }) 83 | .catch(() => { 84 | // 85 | }) 86 | }) 87 | 88 | const interval = setInterval(() => { 89 | if (newSettled === true) { 90 | newSettled = false 91 | paintDecorations(document, dependencyGroups, true, startTime) 92 | } 93 | }, 1000) 94 | 95 | await Promise.allSettled(promises) 96 | 97 | clearInterval(interval) 98 | 99 | return paintDecorations(document, dependencyGroups, false, startTime) 100 | } 101 | 102 | const paintDecorations = ( 103 | document: vscode.TextDocument, 104 | dependencyGroups: DependencyGroups[], 105 | stillLoading: boolean, 106 | startTime: number, 107 | ) => { 108 | if (decorationStart[document.fileName] !== startTime) { 109 | return 110 | } 111 | 112 | const textEditor = getTextEditorFromDocument(document) 113 | if (textEditor === undefined) { 114 | return 115 | } 116 | 117 | const ignorePatterns = getIgnorePattern() 118 | 119 | if (stillLoading) { 120 | paintLoadingOnDependencyGroups(dependencyGroups, document, textEditor) 121 | } else { 122 | clearLoadingOnDependencyGroups(dependencyGroups) 123 | } 124 | 125 | const dependencies = dependencyGroups.map((d) => d.deps).flat() 126 | 127 | dependencies.forEach((dep) => { 128 | if (isDependencyIgnored(dep.dependencyName, ignorePatterns)) { 129 | return 130 | } 131 | 132 | const lineText = document.lineAt(dep.line).text 133 | 134 | const range = new vscode.Range( 135 | new vscode.Position(dep.line, lineText.length), 136 | new vscode.Position(dep.line, lineText.length), 137 | ) 138 | 139 | const npmCache = getCachedNpmData(dep.dependencyName) 140 | if (npmCache === undefined) { 141 | return 142 | } 143 | 144 | if (npmCache.asyncstate === AsyncState.Rejected) { 145 | const text = 'Dependency not found' 146 | const notFoundDecoration = decorateDiscreet(text) 147 | if (updateCache(notFoundDecoration, range.start.line, text)) { 148 | setDecorator(notFoundDecoration, textEditor, range) 149 | } 150 | return 151 | } 152 | 153 | if (npmCache.item === undefined) { 154 | const msUntilRowLoading = getConfig().msUntilRowLoading 155 | if ( 156 | msUntilRowLoading !== 0 && 157 | (msUntilRowLoading < 100 || 158 | npmCache.startTime + getConfig().msUntilRowLoading < new Date().getTime()) 159 | ) { 160 | const text = 'Loading...' 161 | const decorator = decorateDiscreet(text) 162 | if (updateCache(decorator, range.start.line, text)) { 163 | setDecorator(decorator, textEditor, range) 164 | } 165 | } 166 | return 167 | } 168 | 169 | const possibleUpgrades = getPossibleUpgrades( 170 | npmCache.item.npmData, 171 | dep.currentVersion, 172 | dep.dependencyName, 173 | ) 174 | 175 | let decorator: TextEditorDecorationType | undefined 176 | let text: string | undefined 177 | if (possibleUpgrades.major !== undefined) { 178 | // TODO add info about patch version? 179 | text = getUpdateDescription(possibleUpgrades.major.version, possibleUpgrades.existingVersion) 180 | decorator = getDecoratorForUpdate('major', text) 181 | } else if (possibleUpgrades.minor !== undefined) { 182 | text = getUpdateDescription(possibleUpgrades.minor.version, possibleUpgrades.existingVersion) 183 | decorator = getDecoratorForUpdate('minor', text) 184 | } else if (possibleUpgrades.patch !== undefined) { 185 | text = getUpdateDescription(possibleUpgrades.patch.version, possibleUpgrades.existingVersion) 186 | decorator = getDecoratorForUpdate('patch', text) 187 | } else if (possibleUpgrades.prerelease !== undefined) { 188 | text = getUpdateDescription( 189 | possibleUpgrades.prerelease.version, 190 | possibleUpgrades.existingVersion, 191 | ) 192 | decorator = getDecoratorForUpdate('prerelease', text) 193 | } else if (possibleUpgrades.validVersion === false) { 194 | text = 'Failed to parse version' 195 | decorator = decorateDiscreet(text) 196 | } else if (possibleUpgrades.existingVersion === false) { 197 | text = 'current version not found' 198 | decorator = decorateDiscreet(text) 199 | } 200 | 201 | if (decorator === undefined || text === undefined) { 202 | return 203 | } 204 | 205 | if (updateCache(decorator, range.start.line, text)) { 206 | setDecorator(decorator, textEditor, range) 207 | } 208 | }) 209 | } 210 | 211 | const paintLoadingOnDependencyGroups = ( 212 | dependencyGroups: DependencyGroups[], 213 | document: vscode.TextDocument, 214 | textEditor: vscode.TextEditor, 215 | ) => { 216 | dependencyGroups.forEach((lineLimit) => { 217 | const lineText = document.lineAt(lineLimit.startLine).text 218 | const range = new vscode.Range( 219 | new vscode.Position(lineLimit.startLine, lineText.length), 220 | new vscode.Position(lineLimit.startLine, lineText.length), 221 | ) 222 | const text = 'Loading updates...' 223 | const loadingUpdatesDecoration = decorateDiscreet(text) 224 | if (updateCache(loadingUpdatesDecoration, range.start.line, text)) { 225 | setDecorator(loadingUpdatesDecoration, textEditor, range) 226 | } 227 | }) 228 | } 229 | 230 | const clearLoadingOnDependencyGroups = (dependencyGroups: DependencyGroups[]) => { 231 | dependencyGroups.forEach((lineLimit) => { 232 | const current = rowToDecoration[lineLimit.startLine] 233 | if (current) { 234 | current.decoration.dispose() 235 | rowToDecoration[lineLimit.startLine] = undefined 236 | } 237 | }) 238 | } 239 | 240 | const setDecorator = ( 241 | decorator: TextEditorDecorationType, 242 | textEditor: vscode.TextEditor, 243 | range: vscode.Range, 244 | ) => { 245 | textEditor.setDecorations(decorator, [ 246 | { 247 | range, 248 | }, 249 | ]) 250 | } 251 | 252 | const getTextEditorFromDocument = (document: vscode.TextDocument) => { 253 | return vscode.window.visibleTextEditors.find((textEditor) => { 254 | return textEditor.document === document 255 | }) 256 | } 257 | 258 | export const clearDecorations = () => { 259 | Object.values(rowToDecoration).forEach((v) => { 260 | v?.decoration.dispose() 261 | }) 262 | rowToDecoration = {} 263 | } 264 | 265 | const updateCache = (decoration: TextEditorDecorationType, line: number, text: string) => { 266 | const current = rowToDecoration[line] 267 | if (current === undefined || current.text !== text) { 268 | if (current) { 269 | current.decoration.dispose() 270 | } 271 | rowToDecoration[line] = { 272 | decoration, 273 | line, 274 | text, 275 | } 276 | return true 277 | } else { 278 | return false 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type StrictDict = { [key in K]: V } 2 | 3 | export type Dict = { [key in K]: V | undefined } 4 | 5 | export enum AsyncState { 6 | NotStarted = 'NOT_STARTED', 7 | InProgress = 'IN_PROGRESS', 8 | Fulfilled = 'FULFILLED', 9 | Rejected = 'REJECTED', 10 | } 11 | 12 | export interface Loader { 13 | asyncstate: AsyncState 14 | item?: T 15 | } 16 | -------------------------------------------------------------------------------- /src/updateAction.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { OPEN_URL_COMMAND } from './extension' 3 | import { getCachedChangelog, getCachedNpmData, getExactVersion, getPossibleUpgrades } from './npm' 4 | import { getDependencyFromLine, isPackageJson } from './packageJson' 5 | import { replaceLastOccuranceOf } from './util/util' 6 | 7 | export class UpdateAction implements vscode.CodeActionProvider { 8 | public static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix] 9 | 10 | public provideCodeActions( 11 | document: vscode.TextDocument, 12 | range: vscode.Range, 13 | ): vscode.CodeAction[] | undefined { 14 | if (isPackageJson(document) === false) { 15 | return 16 | } 17 | 18 | if (range.isSingleLine === false) { 19 | return 20 | } 21 | 22 | const dep = getDependencyFromLine(document.getText(), range.start.line) 23 | if (dep === undefined) { 24 | return 25 | } 26 | const npmCache = getCachedNpmData(dep.dependencyName) 27 | if (npmCache === undefined || npmCache.item === undefined) { 28 | return 29 | } 30 | 31 | const lineText = document.lineAt(range.start.line).text 32 | const wholeLineRange = new vscode.Range(range.start.line, 0, range.start.line, lineText.length) 33 | const actions: vscode.CodeAction[] = [] 34 | 35 | const possibleUpgrades = getPossibleUpgrades( 36 | npmCache.item.npmData, 37 | dep.currentVersion, 38 | dep.dependencyName, 39 | ) 40 | if (possibleUpgrades.major !== undefined) { 41 | actions.push( 42 | this.createFix( 43 | document, 44 | wholeLineRange, 45 | 'major', 46 | dep.currentVersion, 47 | possibleUpgrades.major.version, 48 | ), 49 | ) 50 | } 51 | if (possibleUpgrades.minor !== undefined) { 52 | actions.push( 53 | this.createFix( 54 | document, 55 | wholeLineRange, 56 | 'minor', 57 | dep.currentVersion, 58 | possibleUpgrades.minor.version, 59 | ), 60 | ) 61 | } 62 | if (possibleUpgrades.patch !== undefined) { 63 | actions.push( 64 | this.createFix( 65 | document, 66 | wholeLineRange, 67 | 'patch', 68 | dep.currentVersion, 69 | possibleUpgrades.patch.version, 70 | ), 71 | ) 72 | } 73 | if (possibleUpgrades.prerelease !== undefined) { 74 | actions.push( 75 | this.createFix( 76 | document, 77 | wholeLineRange, 78 | 'prerelease', 79 | dep.currentVersion, 80 | possibleUpgrades.prerelease.version, 81 | ), 82 | ) 83 | } 84 | 85 | if (npmCache.item.npmData.homepage !== undefined) { 86 | const commandAction = this.createHomepageCommand(npmCache.item.npmData.homepage) 87 | actions.push(commandAction) 88 | } 89 | 90 | const changelog = getCachedChangelog(dep.dependencyName) 91 | if (changelog !== undefined && changelog.item !== undefined) { 92 | const commandAction = this.createChangelogCommand(changelog.item) 93 | actions.push(commandAction) 94 | } 95 | 96 | return actions 97 | } 98 | 99 | private createFix( 100 | document: vscode.TextDocument, 101 | range: vscode.Range, 102 | type: string, 103 | rawCurrentVersion: string, 104 | newVersion: string, 105 | ): vscode.CodeAction { 106 | const lineText = document.lineAt(range.start.line).text 107 | const currentVersion = getExactVersion(rawCurrentVersion) 108 | const newLineText = replaceLastOccuranceOf(lineText, currentVersion, newVersion) 109 | 110 | const fix = new vscode.CodeAction( 111 | `Do ${type} upgrade to ${newVersion}`, 112 | vscode.CodeActionKind.Empty, 113 | ) 114 | fix.edit = new vscode.WorkspaceEdit() 115 | fix.edit.replace(document.uri, range, newLineText) 116 | return fix 117 | } 118 | 119 | private createHomepageCommand(url: string): vscode.CodeAction { 120 | const action = new vscode.CodeAction('Open homepage', vscode.CodeActionKind.Empty) 121 | action.command = { 122 | command: OPEN_URL_COMMAND, 123 | title: 'Open homepage', 124 | tooltip: 'This will open the dependency homepage.', 125 | arguments: [url], 126 | } 127 | return action 128 | } 129 | 130 | private createChangelogCommand(url: string): vscode.CodeAction { 131 | const action = new vscode.CodeAction('Open changelog', vscode.CodeActionKind.Empty) 132 | action.command = { 133 | command: OPEN_URL_COMMAND, 134 | title: 'Open changelog', 135 | tooltip: 'This will open the dependency changelog.', 136 | arguments: [url], 137 | } 138 | return action 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/updateAll.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { getIgnorePattern, isDependencyIgnored } from './ignorePattern' 3 | import { getCachedNpmData, getExactVersion, getLatestVersion } from './npm' 4 | import { getDependencyInformation, isPackageJson } from './packageJson' 5 | import { replaceLastOccuranceOf } from './util/util' 6 | 7 | export interface UpdateEdit { 8 | range: vscode.Range 9 | text: string 10 | } 11 | 12 | export const updateAll = (textEditor?: vscode.TextEditor): UpdateEdit[] => { 13 | if (textEditor === undefined) { 14 | return [] 15 | } 16 | 17 | const document = textEditor.document 18 | 19 | if (isPackageJson(document)) { 20 | const ignorePatterns = getIgnorePattern() 21 | 22 | const dependencies = getDependencyInformation(document.getText()) 23 | .map((d) => d.deps) 24 | .flat() 25 | const edits: UpdateEdit[] = dependencies 26 | .map((dep) => { 27 | const lineText = document.lineAt(dep.line).text 28 | const wholeLineRange = new vscode.Range(dep.line, 0, dep.line, lineText.length) 29 | 30 | if (isDependencyIgnored(dep.dependencyName, ignorePatterns)) { 31 | return 32 | } 33 | 34 | const npmCache = getCachedNpmData(dep.dependencyName) 35 | if (npmCache?.item === undefined) { 36 | return 37 | } 38 | 39 | const latestVersion = getLatestVersion( 40 | npmCache.item.npmData, 41 | dep.currentVersion, 42 | dep.dependencyName, 43 | ) 44 | if (latestVersion === undefined) { 45 | return 46 | } 47 | 48 | const currentExactVersion = getExactVersion(dep.currentVersion) 49 | const newLineText = replaceLastOccuranceOf( 50 | lineText, 51 | currentExactVersion, 52 | latestVersion.version, 53 | ) 54 | return { 55 | range: wholeLineRange, 56 | text: newLineText, 57 | } 58 | }) 59 | .filter((edit): edit is UpdateEdit => edit !== undefined) 60 | 61 | void textEditor.edit((editBuilder: vscode.TextEditorEdit) => { 62 | edits.forEach((edit) => { 63 | editBuilder.replace(edit.range, edit.text) 64 | }) 65 | }) 66 | return edits 67 | } else { 68 | void vscode.window.showWarningMessage( 69 | 'Update failed: File not recognized as valid package.json', 70 | ) 71 | return [] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/util/test-util.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | 3 | export const logToFile = async (content: string, fileName?: string) => { 4 | if (fileName === undefined) { 5 | fileName = 'test-log' 6 | } 7 | const uri = vscode.Uri.parse(`./tmp/${fileName}`) 8 | await vscode.workspace.fs.writeFile(uri, Buffer.from(content)) 9 | } 10 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Object.keys but keeps type safety 3 | */ 4 | export function objectKeys(obj: T): Array { 5 | const entries = Object.keys(obj) 6 | return entries as Array 7 | } 8 | 9 | /** 10 | * Object.entries but keeps type safety 11 | */ 12 | export function objectEntries( 13 | obj: Record, 14 | ): Array<[K, V]> { 15 | const entries = Object.entries(obj) 16 | return entries as Array<[K, V]> 17 | } 18 | 19 | export const replaceLastOccuranceOf = (s: string, replace: string, replaceWith: string) => { 20 | const indexOfReplace = s.lastIndexOf(replace) 21 | if (indexOfReplace !== -1) { 22 | return ( 23 | s.substring(0, indexOfReplace) + replaceWith + s.substring(indexOfReplace + replace.length) 24 | ) 25 | } else { 26 | return s 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "module": "commonjs", 5 | "target": "es2020", 6 | "outDir": "out", 7 | "lib": ["es2020"], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true 11 | }, 12 | "exclude": ["node_modules", ".vscode-test"] 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict' 4 | 5 | const path = require('path') 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname, 'out'), 15 | filename: 'extension.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]', 18 | }, 19 | devtool: 'source-map', 20 | externals: { 21 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader', 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | } 41 | module.exports = config 42 | --------------------------------------------------------------------------------