├── .vscode └── settings.json ├── deps.ts ├── dev_deps.ts ├── README.md ├── LICENSE ├── dedep.ts ├── utils_test.ts └── utils.ts /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export * as colors from "https://deno.land/std@0.114.0/fmt/colors.ts" 2 | -------------------------------------------------------------------------------- /dev_deps.ts: -------------------------------------------------------------------------------- 1 | export { assertEquals } from 'https://deno.land/std@0.51.0/testing/asserts.ts' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dedep 2 | 3 | Manage dependency versions for Deno. 4 | 5 | ![preview](https://user-images.githubusercontent.com/8784712/82181325-e3e43180-9914-11ea-9274-772696935c61.png) 6 | 7 | ## Install 8 | 9 | ```bash 10 | deno install -A https://denopkg.com/egoist/dedep@latest/dedep.ts 11 | ``` 12 | 13 | ## Usage 14 | 15 | Organize all external imports in `deps.ts`: 16 | 17 | ```ts 18 | export * as colors from 'https://deno.land/std/fmt/colors.ts' 19 | 20 | export { cac } from 'https://unpkg.com/cac@6.5.8/mod.js' 21 | ``` 22 | 23 | Run `dedep` to retrieve latest version of each imported module. 24 | 25 | You can also check another file with `dedep [file]`, run `dedep help` to get all command-line usage. 26 | 27 | Supports: 28 | 29 | - https://deno.land/std 30 | - https://denopkg.com 31 | - https://unpkg.com 32 | - https://pika.dev 33 | 34 | ## License 35 | 36 | MIT © [EGOIST (Kevin Titor)](https://github.com/sponsors/egoist) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 EGOIST (Kevin Titor) <0x142857@gmail.com> 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. -------------------------------------------------------------------------------- /dedep.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env deno 2 | import { colors } from "./deps.ts" 3 | import { 4 | getDeps, 5 | getPkgLatestVersion, 6 | getLatestGitTag, 7 | getLatestStdVersion, 8 | semverCompare, 9 | } from "./utils.ts" 10 | 11 | const file = Deno.args[0] || "deps.ts" 12 | 13 | if (file === "help") { 14 | console.log(` 15 | ${colors.bold("dedep [file]")} 16 | ${colors.underline(colors.dim("https://github.com/egoist/dedep"))} 17 | 18 | Examples: 19 | 20 | $ dedep # Check deps.ts 21 | $ dedep exports.ts # Check exports.ts 22 | `) 23 | Deno.exit() 24 | } 25 | 26 | try { 27 | await Deno.stat(file) 28 | } catch (error) { 29 | console.log(colors.red(`${file} does not exist`)) 30 | Deno.exit(1) 31 | } 32 | 33 | const decoder = new TextDecoder("utf-8") 34 | const data = await Deno.readFile(file) 35 | const text = decoder.decode(data) 36 | 37 | // Let's keep this simple for now, no need to use something like babel to parse the file 38 | // Instead just do string replace 39 | 40 | const deps = getDeps(text) 41 | 42 | console.log(`\n Fetching latest version..\n`) 43 | 44 | await Promise.all( 45 | deps 46 | .filter((dep) => dep.type !== "unknown") 47 | .map(async (dep) => { 48 | const latestVersion = 49 | dep.type === "npm" 50 | ? await getPkgLatestVersion(dep.name) 51 | : dep.type === "std" 52 | ? await getLatestStdVersion() 53 | : await getLatestGitTag(dep.name) 54 | console.log(` ${colors.dim(colors.underline(dep.url))}`) 55 | const needsUpgrade = 56 | dep.emptyVersion || semverCompare(latestVersion, dep.version) === 1 57 | 58 | console.log( 59 | ` ${colors.bold(dep.name)} Latest: ${colors[ 60 | needsUpgrade ? "red" : "green" 61 | ](latestVersion)} Current: ${dep.emptyVersion ? "empty" : dep.version}` 62 | ) 63 | console.log() 64 | }) 65 | ) 66 | 67 | for (const dep of deps) { 68 | if (dep.type === "unknown") { 69 | console.log(` ${colors.dim(colors.underline(dep.url))}`) 70 | console.log(` ${colors.yellow("unknown this dep")}`) 71 | console.log() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /utils_test.ts: -------------------------------------------------------------------------------- 1 | import { getPkgLatestVersion, getLatestGitTag, getDeps } from './utils.ts' 2 | import { assertEquals } from './dev_deps.ts' 3 | 4 | Deno.test('latest npm version', async () => { 5 | const version = await getPkgLatestVersion('cac') 6 | assertEquals(typeof version, 'string') 7 | }) 8 | 9 | Deno.test('latest git tag', async () => { 10 | const tag = await getLatestGitTag('cacjs/cac') 11 | assertEquals(typeof tag, 'string') 12 | }) 13 | 14 | Deno.test('get deps', () => { 15 | const deps = getDeps(` 16 | export * from 'https://denopkg.com/cacjs/cac' 17 | export * from 'https://denopkg.com/cacjs/cac@v0.1.1' 18 | export * from 'https://unpkg.com/cac@0.1.1' 19 | export * from 'https://unpkg.com/cac' 20 | export * from 'https://deno.land/std/http' 21 | export * from 'https://deno.land/std@v0.1.1/http' 22 | export * from 'https://deno.land/std@v0.1.1/http/server.ts' 23 | export * as abc from 'https://deno.land/x/abc/mod.ts' 24 | `) 25 | assertEquals(deps, [ 26 | { 27 | type: 'github', 28 | url: 'https://denopkg.com/cacjs/cac', 29 | name: 'cacjs/cac', 30 | version: 'master', 31 | emptyVersion: true, 32 | }, 33 | { 34 | type: 'github', 35 | url: 'https://denopkg.com/cacjs/cac@v0.1.1', 36 | name: 'cacjs/cac', 37 | version: 'v0.1.1', 38 | emptyVersion: false, 39 | }, 40 | { 41 | type: 'npm', 42 | url: 'https://unpkg.com/cac@0.1.1', 43 | name: 'cac', 44 | version: '0.1.1', 45 | emptyVersion: false, 46 | }, 47 | { 48 | type: 'npm', 49 | url: 'https://unpkg.com/cac', 50 | name: 'cac', 51 | version: 'latest', 52 | emptyVersion: true, 53 | }, 54 | { 55 | type: 'std', 56 | url: 'https://deno.land/std/http', 57 | name: 'denoland/deno', 58 | version: 'master', 59 | emptyVersion: true, 60 | }, 61 | { 62 | type: 'std', 63 | url: 'https://deno.land/std@v0.1.1/http', 64 | name: 'denoland/deno', 65 | version: 'v0.1.1', 66 | emptyVersion: false, 67 | }, 68 | { 69 | type: 'std', 70 | url: 'https://deno.land/std@v0.1.1/http/server.ts', 71 | name: 'denoland/deno', 72 | version: 'v0.1.1', 73 | emptyVersion: false, 74 | }, 75 | { 76 | type: 'unknown', 77 | url: 'https://deno.land/x/abc/mod.ts', 78 | name: '', 79 | version: '', 80 | emptyVersion: true, 81 | } 82 | ]) 83 | }) 84 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | export function getDeps(text: string) { 2 | const deps: Array<{ 3 | type: "npm" | "github" | "std" | "unknown" 4 | url: string 5 | name: string 6 | version: string 7 | emptyVersion: boolean 8 | }> = [] 9 | 10 | const SCOPED_PKG_RE = /^\/(@[^\/]+)\/([^\/]+)/ 11 | const PKG_RE = /^\/([^\/]+)/ 12 | 13 | text.replace(/\s+from\s+['"`](.+)['"`]/g, (_, p1) => { 14 | if (/^https:\/\//.test(p1)) { 15 | const { hostname, pathname } = new URL("", p1) 16 | let m: RegExpExecArray | null = null 17 | 18 | if (hostname === "unpkg.com" || hostname === "cdn.pika.dev") { 19 | if ((m = SCOPED_PKG_RE.exec(pathname))) { 20 | const version = m[2].split("@")[1] 21 | deps.push({ 22 | url: p1, 23 | type: "npm", 24 | name: m[1] + "/" + m[2].split("@")[0], 25 | version: version || "latest", 26 | emptyVersion: !version, 27 | }) 28 | } else if ((m = PKG_RE.exec(pathname))) { 29 | const version = m[1].split("@")[1] 30 | deps.push({ 31 | type: "npm", 32 | url: p1, 33 | name: m[1].split("@")[0], 34 | version: version || "latest", 35 | emptyVersion: !version, 36 | }) 37 | } else { 38 | handlerUnkown(p1) 39 | } 40 | } else if (hostname === "deno.land") { 41 | if ((m = /^\/std(?:@([^\/]+))?/.exec(pathname))) { 42 | deps.push({ 43 | type: "std", 44 | url: p1, 45 | name: "denoland/deno", 46 | version: m[1] || "master", 47 | emptyVersion: !m[1], 48 | }) 49 | } else { 50 | handlerUnkown(p1) 51 | } 52 | } else if (hostname === "denopkg.com") { 53 | if ((m = /\/([^\/]+)\/([^\/]+)/.exec(pathname))) { 54 | const version = m[2].split("@")[1] 55 | deps.push({ 56 | type: "github", 57 | url: p1, 58 | name: `${m[1]}/${m[2].split("@")[0]}`, 59 | version: version || "master", 60 | emptyVersion: !version, 61 | }) 62 | } 63 | } else { 64 | handlerUnkown(p1) 65 | } 66 | } 67 | return "" 68 | }) 69 | 70 | function handlerUnkown(url: string) { 71 | deps.push({ 72 | type: "unknown", 73 | url, 74 | name: "", 75 | version: "", 76 | emptyVersion: true, 77 | }) 78 | } 79 | 80 | return deps 81 | } 82 | 83 | export async function getPkgLatestVersion(name: string) { 84 | const json = await fetch(`https://registry.npmjs.org/${name}`).then((res) => 85 | res.json() 86 | ) 87 | 88 | return json["dist-tags"]["latest"] 89 | } 90 | 91 | // https://github.com/substack/semver-compare/blob/master/index.js 92 | export function semverCompare(a: string, b: string) { 93 | const pa = a.split(".") 94 | const pb = b.split(".") 95 | for (let i = 0; i < 3; i++) { 96 | const na = Number(pa[i]) 97 | const nb = Number(pb[i]) 98 | if (na > nb) return 1 99 | if (nb > na) return -1 100 | if (!isNaN(na) && isNaN(nb)) return 1 101 | if (isNaN(na) && !isNaN(nb)) return -1 102 | } 103 | return 0 104 | } 105 | 106 | // https://github.com/sindresorhus/semver-regex/blob/master/index.js 107 | const isSemver = (v: string) => 108 | /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?(?=$|\s)/gi 109 | 110 | export async function getLatestGitTag(repo: string) { 111 | const json = await fetch( 112 | `https://api.github.com/repos/${repo}/git/refs/tags` 113 | ).then((res) => res.json()) 114 | const tags = json 115 | .map((v: any) => v.ref.replace(/^refs\/tags\/v?/, "")) 116 | .filter((v: string) => isSemver(v)) 117 | return tags.sort((a: string, b: string) => -semverCompare(a, b))[0] 118 | } 119 | 120 | export async function getLatestStdVersion() { 121 | const text = await fetch( 122 | `https://denopkg.com/denoland/deno_std@main/version.ts` 123 | ).then((res) => res.text()) 124 | 125 | return text.match(/const VERSION = "(.+)"/)![1] 126 | } 127 | --------------------------------------------------------------------------------