diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..1a2fb332 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f335ebfb..01fd0ae9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "npmlog": "^6.0.2", "pkginfo": "^0.4.1", "promptly": "^3.2.0", + "semver": "^7.5.4", + "ts-brand": "^0.0.2", "update-notifier": "^5.1.0", "yaml": "^2.0.1" }, @@ -42,10 +44,11 @@ "@types/lodash": "^4.14.199", "@types/mocha": "^10.0.3", "@types/node": "^14.18.63", + "@types/node-fetch": "^2.6.9", "@types/npmlog": "^4.1.4", "@types/pkginfo": "^0.4.1", "@types/promptly": "^3.0.3", - "@types/request": "^2.48.11", + "@types/request": "^2.48.12", "@types/test-console": "^2.0.1", "@types/update-notifier": "^6.0.5", "@typescript-eslint/eslint-plugin": "^6.8.0", @@ -212,6 +215,15 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/eslint-parser": { "version": "7.18.2", "dev": true, @@ -229,6 +241,15 @@ "eslint": "^7.5.0 || ^8.0.0" } }, + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", @@ -283,6 +304,15 @@ "yallist": "^3.0.2" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -884,29 +914,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@npmcli/fs/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/move-file": { "version": "2.0.0", "license": "MIT", @@ -1212,31 +1219,6 @@ "semantic-release": ">=19.0.0" } }, - "node_modules/@semantic-release/npm/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@semantic-release/npm/node_modules/semver": { - "version": "7.3.7", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@semantic-release/release-notes-generator": { "version": "10.0.3", "dev": true, @@ -1285,9 +1267,10 @@ } }, "node_modules/@types/caseless": { - "version": "0.12.4", - "dev": true, - "license": "MIT" + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "dev": true }, "node_modules/@types/cli-table": { "version": "0.3.2", @@ -1357,9 +1340,10 @@ "license": "MIT" }, "node_modules/@types/node-fetch": { - "version": "2.6.6", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "form-data": "^4.0.0" @@ -1427,9 +1411,10 @@ } }, "node_modules/@types/request": { - "version": "2.48.11", + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", "dev": true, - "license": "MIT", "dependencies": { "@types/caseless": "*", "@types/node": "*", @@ -1439,8 +1424,9 @@ }, "node_modules/@types/request/node_modules/form-data": { "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", "dev": true, - "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -1474,9 +1460,10 @@ "license": "MIT" }, "node_modules/@types/tough-cookie": { - "version": "4.0.4", - "dev": true, - "license": "MIT" + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true }, "node_modules/@types/update-notifier": { "version": "6.0.5", @@ -1673,31 +1660,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/parser": { "version": "6.8.0", "dev": true, @@ -1805,31 +1767,6 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { "version": "6.8.0", "dev": true, @@ -1854,31 +1791,6 @@ "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "6.8.0", "dev": true, @@ -2040,16 +1952,6 @@ "node": ">=0.10.0" } }, - "node_modules/another-npm-registry-client/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/another-npm-registry-client/node_modules/npmlog": { "version": "4.1.2", "license": "ISC", @@ -2061,19 +1963,6 @@ "set-blocking": "~2.0.0" } }, - "node_modules/another-npm-registry-client/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/another-npm-registry-client/node_modules/string-width": { "version": "1.0.2", "license": "MIT", @@ -2893,6 +2782,15 @@ "node": ">=10" } }, + "node_modules/conventional-changelog-writer/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/conventional-commits-filter": { "version": "2.0.7", "dev": true, @@ -3395,31 +3293,6 @@ "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/eslint-plugin-vue/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-vue/node_modules/semver": { - "version": "7.3.7", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "dev": true, @@ -4916,6 +4789,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "dev": true, @@ -5068,20 +4949,6 @@ "node": ">=10" } }, - "node_modules/meow/node_modules/semver": { - "version": "7.3.7", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/meow/node_modules/type-fest": { "version": "0.18.1", "dev": true, @@ -5726,29 +5593,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm-registry-fetch/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm-registry-fetch/node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm-registry-fetch/node_modules/validate-npm-package-name": { "version": "4.0.0", "license": "ISC", @@ -8283,6 +8127,14 @@ "node": ">=8" } }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -9132,10 +8984,10 @@ "node": ">=8" } }, - "node_modules/semantic-release/node_modules/semver": { - "version": "7.3.7", - "dev": true, - "license": "ISC", + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9146,14 +8998,6 @@ "node": ">=10" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/semver-diff": { "version": "3.1.1", "license": "MIT", @@ -9164,6 +9008,14 @@ "node": ">=8" } }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/semver-regex": { "version": "3.1.4", "dev": true, @@ -9175,6 +9027,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "6.0.0", "dev": true, @@ -9817,6 +9680,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-brand": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ts-brand/-/ts-brand-0.0.2.tgz", + "integrity": "sha512-UhSzWY4On9ZHIj6DKkRYVN/8OaprbLAZ3b/Y2AJwdl6oozSABsQ0PvwDh4vOVdkvOtWQOkIrjctZ1kj8YfF3jA==" + }, "node_modules/ts-mocha": { "version": "10.0.0", "dev": true, @@ -10072,29 +9940,6 @@ "url": "https://github.com/yeoman/update-notifier?sponsor=1" } }, - "node_modules/update-notifier/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/update-notifier/node_modules/semver": { - "version": "7.3.7", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/uri-js": { "version": "4.4.1", "license": "BSD-2-Clause", @@ -10210,31 +10055,6 @@ "node": ">=4.0" } }, - "node_modules/vue-eslint-parser/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/vue-eslint-parser/node_modules/semver": { - "version": "7.3.7", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "dev": true, @@ -10560,6 +10380,14 @@ "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "@babel/eslint-parser": { @@ -10569,6 +10397,14 @@ "eslint-scope": "^5.1.1", "eslint-visitor-keys": "^2.1.0", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "@babel/generator": { @@ -10618,6 +10454,12 @@ "yallist": "^3.0.2" } }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -11036,20 +10878,6 @@ "requires": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.7", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@npmcli/move-file": { @@ -11277,22 +11105,6 @@ "registry-auth-token": "^4.0.0", "semver": "^7.1.2", "tempy": "^1.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.7", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@semantic-release/release-notes-generator": { @@ -11324,7 +11136,9 @@ "version": "2.0.0" }, "@types/caseless": { - "version": "0.12.4", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", "dev": true }, "@types/cli-table": { @@ -11384,7 +11198,9 @@ "dev": true }, "@types/node-fetch": { - "version": "2.6.6", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", "dev": true, "requires": { "@types/node": "*", @@ -11444,7 +11260,9 @@ } }, "@types/request": { - "version": "2.48.11", + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", "dev": true, "requires": { "@types/caseless": "*", @@ -11455,6 +11273,8 @@ "dependencies": { "form-data": { "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", "dev": true, "requires": { "asynckit": "^0.4.0", @@ -11484,7 +11304,9 @@ "dev": true }, "@types/tough-cookie": { - "version": "4.0.4", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, "@types/update-notifier": { @@ -11586,22 +11408,6 @@ "natural-compare": "^1.4.0", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/parser": { @@ -11648,22 +11454,6 @@ "is-glob": "^4.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/utils": { @@ -11677,22 +11467,6 @@ "@typescript-eslint/types": "6.8.0", "@typescript-eslint/typescript-estree": "6.8.0", "semver": "^7.5.4" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/visitor-keys": { @@ -11802,12 +11576,6 @@ "number-is-nan": "^1.0.0" } }, - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, "npmlog": { "version": "4.1.2", "optional": true, @@ -11818,12 +11586,6 @@ "set-blocking": "~2.0.0" } }, - "semver": { - "version": "7.3.7", - "requires": { - "lru-cache": "^6.0.0" - } - }, "string-width": { "version": "1.0.2", "optional": true, @@ -12334,6 +12096,14 @@ "semver": "^6.0.0", "split": "^1.0.0", "through2": "^4.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "conventional-commits-filter": { @@ -12684,22 +12454,6 @@ "postcss-selector-parser": "^6.0.9", "semver": "^7.3.5", "vue-eslint-parser": "^8.0.1" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.7", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "eslint-scope": { @@ -13580,6 +13334,13 @@ "version": "3.1.0", "requires": { "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } } }, "make-error": { @@ -13682,13 +13443,6 @@ "validate-npm-package-license": "^3.0.1" } }, - "semver": { - "version": "7.3.7", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "type-fest": { "version": "0.18.1", "dev": true @@ -15693,20 +15447,6 @@ "validate-npm-package-name": "^4.0.0" } }, - "semver": { - "version": "7.3.7", - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, "validate-npm-package-name": { "version": "4.0.0", "requires": { @@ -15854,6 +15594,13 @@ "registry-auth-token": "^4.0.0", "registry-url": "^5.0.0", "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } } }, "parent-module": { @@ -16377,25 +16124,38 @@ "resolve-from": { "version": "5.0.0", "dev": true - }, - "semver": { - "version": "7.3.7", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + } + } }, "semver-diff": { "version": "3.1.1", "requires": { "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } } }, "semver-regex": { @@ -16834,6 +16594,11 @@ "dev": true, "requires": {} }, + "ts-brand": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ts-brand/-/ts-brand-0.0.2.tgz", + "integrity": "sha512-UhSzWY4On9ZHIj6DKkRYVN/8OaprbLAZ3b/Y2AJwdl6oozSABsQ0PvwDh4vOVdkvOtWQOkIrjctZ1kj8YfF3jA==" + }, "ts-mocha": { "version": "10.0.0", "dev": true, @@ -16984,20 +16749,6 @@ "semver": "^7.3.4", "semver-diff": "^3.1.1", "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.7", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "uri-js": { @@ -17076,20 +16827,6 @@ "estraverse": { "version": "5.3.0", "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.3.7", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } } } }, diff --git a/package.json b/package.json index e9108219..36cbf1c4 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,11 @@ "@types/lodash": "^4.14.199", "@types/mocha": "^10.0.3", "@types/node": "^14.18.63", + "@types/node-fetch": "^2.6.9", "@types/npmlog": "^4.1.4", "@types/pkginfo": "^0.4.1", "@types/promptly": "^3.0.3", - "@types/request": "^2.48.11", + "@types/request": "^2.48.12", "@types/test-console": "^2.0.1", "@types/update-notifier": "^6.0.5", "@typescript-eslint/eslint-plugin": "^6.8.0", @@ -81,6 +82,8 @@ "npmlog": "^6.0.2", "pkginfo": "^0.4.1", "promptly": "^3.2.0", + "semver": "^7.5.4", + "ts-brand": "^0.0.2", "update-notifier": "^5.1.0", "yaml": "^2.0.1" } diff --git a/src/cmd-add.ts b/src/cmd-add.ts index 0490b7ec..1380f34f 100644 --- a/src/cmd-add.ts +++ b/src/cmd-add.ts @@ -1,17 +1,22 @@ import log from "./logger"; import url from "url"; -import { isUrlVersion } from "./utils/pkg-version"; -import { atVersion, splitPkgName } from "./utils/pkg-name"; -import { GlobalOptions, PkgName, ScopedRegistry } from "./types/global"; +import { isPackageUrl } from "./types/package-url"; +import { GlobalOptions, ScopedRegistry } from "./types/global"; import { tryGetLatestVersion } from "./utils/pkg-info"; import { loadManifest, saveManifest } from "./utils/manifest"; import { env, parseEnv } from "./utils/env"; - import { compareEditorVersion, tryParseEditorVersion, } from "./utils/editor-version"; import { fetchPackageDependencies, fetchPackageInfo } from "./registry-client"; +import { DomainName, isDomainName } from "./types/domain-name"; +import { SemanticVersion } from "./types/semantic-version"; +import { + packageReference, + PackageReference, + splitPackageReference, +} from "./types/package-reference"; export type AddOptions = { test?: boolean; @@ -27,7 +32,7 @@ type AddResult = { }; export const add = async function ( - pkgs: PkgName | PkgName[], + pkgs: PackageReference | PackageReference[], options: AddOptions ): Promise { if (!Array.isArray(pkgs)) pkgs = [pkgs]; @@ -55,7 +60,7 @@ const _add = async function ({ testables, force, }: { - pkg: PkgName; + pkg: PackageReference; testables?: boolean; force?: boolean; }): Promise { @@ -64,9 +69,9 @@ const _add = async function ({ // is upstream package flag let isUpstreamPackage = false; // parse name - const split = splitPkgName(pkg); - const name = split.name; - let version = split.version; + const split = splitPackageReference(pkg); + const name = split[0]; + let version = split[1]; // load manifest const manifest = loadManifest(); @@ -76,8 +81,8 @@ const _add = async function ({ manifest.dependencies = {}; } // packages that added to scope registry - const pkgsInScope: PkgName[] = []; - if (version === undefined || !isUrlVersion(version)) { + const pkgsInScope: DomainName[] = []; + if (version === undefined || !isPackageUrl(version)) { // verify name let pkgInfo = await fetchPackageInfo(name); if (!pkgInfo && env.upstream) { @@ -89,10 +94,11 @@ const _add = async function ({ return { code: 1, dirty }; } // verify version - const versions = Object.keys(pkgInfo.versions); + const versions = Object.keys(pkgInfo.versions) as SemanticVersion[]; // eslint-disable-next-line require-atomic-updates - if (!version || version == "latest") version = tryGetLatestVersion(pkgInfo); - if (versions.filter((x) => x == version).length <= 0) { + if (!version || version === "latest") + version = tryGetLatestVersion(pkgInfo); + if (versions.filter((x) => x === version).length <= 0) { log.warn( "404", `version ${version} is not a valid choice of: ${versions @@ -170,7 +176,7 @@ const _add = async function ({ if (!depObj.resolved) log.notice( "suggest", - `to install ${atVersion( + `to install ${packageReference( depObj.name, depObj.version )} or a replaceable version manually` @@ -193,7 +199,7 @@ const _add = async function ({ manifest.dependencies[name] = version; if (!oldVersion) { // Log the added package - log.notice("manifest", `added ${atVersion(name, version)}`); + log.notice("manifest", `added ${packageReference(name, version)}`); dirty = true; } else if (oldVersion != version) { // Log the modified package version @@ -201,7 +207,7 @@ const _add = async function ({ dirty = true; } else { // Log the existed package - log.notice("manifest", `existed ${atVersion(name, version)}`); + log.notice("manifest", `existed ${packageReference(name, version)}`); } if (!isUpstreamPackage) { // add to scopedRegistries @@ -227,7 +233,7 @@ const _add = async function ({ const entry = manifest.scopedRegistries.filter(filterEntry)[0]; // apply pkgsInScope const scopesSet = new Set(entry.scopes || []); - pkgsInScope.push(env.namespace); + if (isDomainName(env.namespace)) pkgsInScope.push(env.namespace); pkgsInScope.forEach((name) => { if (!scopesSet.has(name)) { scopesSet.add(name); diff --git a/src/cmd-deps.ts b/src/cmd-deps.ts index ee63424b..0ab8fd37 100644 --- a/src/cmd-deps.ts +++ b/src/cmd-deps.ts @@ -1,20 +1,30 @@ import log from "./logger"; -import { atVersion, splitPkgName } from "./utils/pkg-name"; -import { GlobalOptions, PkgName, PkgVersion } from "./types/global"; +import { GlobalOptions } from "./types/global"; import { parseEnv } from "./utils/env"; import { fetchPackageDependencies } from "./registry-client"; +import { DomainName } from "./types/domain-name"; +import { isPackageUrl } from "./types/package-url"; +import { + packageReference, + PackageReference, + splitPackageReference, + VersionReference, +} from "./types/package-reference"; export type DepsOptions = { deep?: boolean; _global: GlobalOptions; }; -export const deps = async function (pkg: PkgName, options: DepsOptions) { +export const deps = async function ( + pkg: PackageReference, + options: DepsOptions +) { // parse env const envOk = await parseEnv(options, { checkPath: false }); if (!envOk) return 1; // parse name - const { name, version } = splitPkgName(pkg); + const [name, version] = splitPackageReference(pkg); // deps await _deps({ name, version, deep: options.deep }); return 0; @@ -25,11 +35,13 @@ const _deps = async function ({ version, deep, }: { - name: PkgName; - version: PkgVersion | undefined; + name: DomainName; + version: VersionReference | undefined; deep?: boolean; }) { - // eslint-disable-next-line no-unused-vars + if (version !== undefined && isPackageUrl(version)) + throw new Error("Cannot get dependencies for url-version"); + const [depsValid, depsInvalid] = await fetchPackageDependencies({ name, version, @@ -38,7 +50,7 @@ const _deps = async function ({ depsValid .filter((x) => !x.self) .forEach((x) => - log.notice("dependency", `${atVersion(x.name, x.version)}`) + log.notice("dependency", `${packageReference(x.name, x.version)}`) ); depsInvalid .filter((x) => !x.self) @@ -46,6 +58,6 @@ const _deps = async function ({ let reason = "unknown"; if (x.reason == "package404") reason = "missing dependency"; else if (x.reason == "version404") reason = "missing dependency version"; - log.warn(reason, atVersion(x.name, x.version)); + log.warn(reason, packageReference(x.name, x.version)); }); }; diff --git a/src/cmd-login.ts b/src/cmd-login.ts index e5198e47..fbb010d3 100644 --- a/src/cmd-login.ts +++ b/src/cmd-login.ts @@ -6,13 +6,18 @@ import { assertIsNpmClientError, getNpmClient } from "./registry-client"; import log from "./logger"; -import { GlobalOptions, Registry } from "./types/global"; +import { GlobalOptions } from "./types/global"; import { getUpmConfigDir, loadUpmConfig, saveUpmConfig, } from "./utils/upm-config"; import { parseEnv } from "./utils/env"; +import { + RegistryUrl, + registryUrl, + removeTrailingSlash, +} from "./types/registry-url"; export type LoginOptions = { username?: string; @@ -33,9 +38,9 @@ export const login = async function (options: LoginOptions) { options.password = await promptly.password("Password: "); if (!options.email) options.email = await promptly.prompt("Email: "); if (!options._global.registry) - options._global.registry = await promptly.prompt("Registry: ", { - validator: [validateRegistry], - }); + options._global.registry = (await promptly.prompt("Registry: ", { + validator: [registryUrl], + })) as RegistryUrl; let token: string | null = null; let _auth: string | null = null; if (options.basicAuth) { @@ -48,7 +53,7 @@ export const login = async function (options: LoginOptions) { username: options.username, password: options.password, email: options.email, - registry: options._global.registry, + registry: options._global.registry as RegistryUrl, }); if (result.code == 1) return result.code; if (!result.token) { @@ -58,7 +63,7 @@ export const login = async function (options: LoginOptions) { token = result.token; // write npm token await writeNpmToken({ - registry: options._global.registry, + registry: options._global.registry as RegistryUrl, token: result.token, }); } @@ -69,7 +74,7 @@ export const login = async function (options: LoginOptions) { alwaysAuth: options.alwaysAuth || false, basicAuth: options.basicAuth || false, email: options.email, - registry: options._global.registry, + registry: options._global.registry as RegistryUrl, token, }); }; @@ -86,7 +91,7 @@ const npmLogin = async function ({ username: string; password: string; email: string; - registry: Registry; + registry: RegistryUrl; }) { const client = getNpmClient(); try { @@ -125,7 +130,7 @@ const writeNpmToken = async function ({ registry, token, }: { - registry: Registry; + registry: RegistryUrl; token: string; }) { const configPath = getNpmrcPath(); @@ -160,7 +165,7 @@ export const getNpmrcPath = function () { */ export const generateNpmrcLines = function ( content: string, - registry: Registry, + registry: RegistryUrl, token: string ) { let lines = content ? content.split("\n") : []; @@ -189,16 +194,6 @@ export const generateNpmrcLines = function ( return lines; }; -/** - * http protocal validator - * @param {*} value - */ -export const validateRegistry = function (value: Registry): Registry { - if (!/http(s?):\/\//.test(value)) - throw new Error("The registry address should starts with http(s)://"); - return value; -}; - /** * Write npm token to Unity */ @@ -214,7 +209,7 @@ const writeUnityToken = async function ({ alwaysAuth: boolean; basicAuth: boolean; email: string; - registry: Registry; + registry: RegistryUrl; token: string | null; }) { // Create config dir if necessary @@ -223,7 +218,7 @@ const writeUnityToken = async function ({ const config = (await loadUpmConfig(configDir)) || {}; if (!config.npmAuth) config.npmAuth = {}; // Remove ending slash of registry - if (registry.endsWith("/")) registry = registry.replace(/\/$/, ""); + registry = removeTrailingSlash(registry); if (basicAuth) { if (_auth === null) throw new Error("Auth is null"); diff --git a/src/cmd-remove.ts b/src/cmd-remove.ts index b051367d..b6c93c93 100644 --- a/src/cmd-remove.ts +++ b/src/cmd-remove.ts @@ -1,15 +1,20 @@ import log from "./logger"; -import { atVersion, splitPkgName } from "./utils/pkg-name"; -import { GlobalOptions, PkgName, ScopedRegistry } from "./types/global"; +import { GlobalOptions, ScopedRegistry } from "./types/global"; import { loadManifest, saveManifest } from "./utils/manifest"; import { env, parseEnv } from "./utils/env"; +import { isDomainName } from "./types/domain-name"; +import { + packageReference, + PackageReference, + splitPackageReference, +} from "./types/package-reference"; export type RemoveOptions = { _global: GlobalOptions; }; export const remove = async function ( - pkgs: PkgName[] | PkgName, + pkgs: PackageReference[] | PackageReference, options: RemoveOptions ) { if (!Array.isArray(pkgs)) pkgs = [pkgs]; @@ -29,15 +34,18 @@ export const remove = async function ( return result.code; }; -const _remove = async function (pkg: PkgName) { +const _remove = async function (pkg: PackageReference) { // dirty flag let dirty = false; // parse name - const split = splitPkgName(pkg); - const name = split.name; - let version = split.version; + const split = splitPackageReference(pkg); + const name = split[0]; + let version = split[1]; if (version) { - log.warn("", `please replace '${atVersion(name, version)}' with '${name}'`); + log.warn( + "", + `please replace '${packageReference(name, version)}' with '${name}'` + ); return { code: 1, dirty }; } // load manifest @@ -49,7 +57,7 @@ const _remove = async function (pkg: PkgName) { if (manifest.dependencies) { version = manifest.dependencies[name]; if (version) { - log.notice("manifest", `removed ${atVersion(name, version)}`); + log.notice("manifest", `removed ${packageReference(name, version)}`); delete manifest.dependencies[name]; dirty = true; } else pkgsNotFound.push(pkg); @@ -68,7 +76,7 @@ const _remove = async function (pkg: PkgName) { if (index > -1) { entry.scopes.splice(index, 1); const scopesSet = new Set(entry.scopes); - scopesSet.add(env.namespace); + if (isDomainName(env.namespace)) scopesSet.add(env.namespace); entry.scopes = Array.from(scopesSet).sort(); dirty = true; } diff --git a/src/cmd-search.ts b/src/cmd-search.ts index 2dcb471b..e1d4a4ec 100644 --- a/src/cmd-search.ts +++ b/src/cmd-search.ts @@ -5,32 +5,28 @@ import log from "./logger"; import { is404Error, isHttpError } from "./utils/error-type-guards"; import * as os from "os"; import assert from "assert"; -import { - GlobalOptions, - PkgInfo, - PkgName, - PkgVersion, - Registry, - ReverseDomainName, -} from "./types/global"; +import { GlobalOptions, PkgInfo } from "./types/global"; import { tryGetLatestVersion } from "./utils/pkg-info"; import { env, parseEnv } from "./utils/env"; +import { DomainName } from "./types/domain-name"; +import { SemanticVersion } from "./types/semantic-version"; +import { RegistryUrl } from "./types/registry-url"; type DateString = string; -type TableRow = [PkgName, PkgVersion, DateString, ""]; +type TableRow = [DomainName, SemanticVersion, DateString, ""]; export type SearchOptions = { _global: GlobalOptions; }; export type SearchedPkgInfo = Omit & { - versions: Record; + versions: Record; }; export type OldSearchResult = | SearchedPkgInfo[] - | Record; + | Record; // Get npm fetch options const getNpmFetchOptions = function (): Options { @@ -45,7 +41,7 @@ const getNpmFetchOptions = function (): Options { const searchEndpoint = async function ( keyword: string, - registry?: Registry + registry?: RegistryUrl ): Promise { if (!registry) registry = env.registry; try { diff --git a/src/cmd-view.ts b/src/cmd-view.ts index b9f76248..0208cf66 100644 --- a/src/cmd-view.ts +++ b/src/cmd-view.ts @@ -1,24 +1,35 @@ import chalk from "chalk"; import log from "./logger"; import assert from "assert"; -import { atVersion, splitPkgName } from "./utils/pkg-name"; -import { GlobalOptions, PkgInfo, PkgName } from "./types/global"; +import { GlobalOptions, PkgInfo } from "./types/global"; import { tryGetLatestVersion } from "./utils/pkg-info"; import { env, parseEnv } from "./utils/env"; import { fetchPackageInfo } from "./registry-client"; +import { DomainName } from "./types/domain-name"; +import { + packageReference, + PackageReference, + splitPackageReference, +} from "./types/package-reference"; export type ViewOptions = { _global: GlobalOptions; }; -export const view = async function (pkg: PkgName, options: ViewOptions) { +export const view = async function ( + pkg: PackageReference, + options: ViewOptions +) { // parse env const envOk = await parseEnv(options, { checkPath: false }); if (!envOk) return 1; // parse name - const { name, version } = splitPkgName(pkg); + const [name, version] = splitPackageReference(pkg); if (version) { - log.warn("", `please replace '${atVersion(name, version)}' with '${name}'`); + log.warn( + "", + `please replace '${packageReference(name, version)}' with '${name}'` + ); return 1; } // verify name @@ -79,7 +90,7 @@ const printInfo = function (pkg: PkgInfo) { if (dependencies && Object.keys(dependencies).length > 0) { console.log(); console.log("dependencies"); - Object.keys(dependencies) + (Object.keys(dependencies) as DomainName[]) .sort() .forEach((n) => console.log(chalk.yellow(n) + ` ${dependencies[n]}`)); } diff --git a/src/registry-client.ts b/src/registry-client.ts index 2e0152be..99d20e32 100644 --- a/src/registry-client.ts +++ b/src/registry-client.ts @@ -8,18 +8,14 @@ import RegClient, { import log from "./logger"; import request from "request"; import assert, { AssertionError } from "assert"; -import { - Dependency, - NameVersionPair, - PkgInfo, - PkgName, - PkgVersion, - Registry, -} from "./types/global"; +import { Dependency, NameVersionPair, PkgInfo } from "./types/global"; import { env } from "./utils/env"; -import { atVersion, isInternalPackage } from "./utils/pkg-name"; import _ from "lodash"; import { tryGetLatestVersion } from "./utils/pkg-info"; +import { DomainName, isInternalPackage } from "./types/domain-name"; +import { SemanticVersion } from "./types/semantic-version"; +import { packageReference } from "./types/package-reference"; +import { RegistryUrl } from "./types/registry-url"; export type NpmClient = { rawClient: RegClient; @@ -96,8 +92,8 @@ export const getNpmClient = (): NpmClient => { }; // Fetch package info json from registry export const fetchPackageInfo = async function ( - name: PkgName, - registry?: Registry + name: DomainName, + registry?: RegistryUrl ): Promise { if (!registry) registry = env.registry; const pkgPath = `${registry}/${name}`; @@ -124,15 +120,13 @@ export const fetchPackageDependencies = async function ({ version, deep, }: { - name: PkgName; - version: PkgVersion | undefined; + name: DomainName; + version: SemanticVersion | "latest" | undefined; deep?: boolean; }): Promise<[Dependency[], Dependency[]]> { log.verbose( "dependency", - `fetch: ${ - version !== undefined ? atVersion(name, version) : name - } deep=${deep}` + `fetch: ${packageReference(name, version)} deep=${deep}` ); // a list of pending dependency {name, version} const pendingList: NameVersionPair[] = [{ name, version }]; @@ -144,7 +138,7 @@ export const fetchPackageDependencies = async function ({ const depsInvalid = []; // cached dict: {pkg-name: pkgInfo} const cachedPacakgeInfoDict: Record< - PkgVersion, + DomainName, { pkgInfo: PkgInfo; upstream: boolean } > = {}; while (pendingList.length > 0) { @@ -155,11 +149,16 @@ export const fetchPackageDependencies = async function ({ processedList.push(entry); // create valid depedenency structure const depObj: Dependency = { - ...entry, + name: entry.name, + /* + NOTE: entry.version could also be "latest" or undefiend. + Later code guarantees that in that case depObj.version will be replaced + with a valid-semantic version. So we can assert the value here safely + */ + version: entry.version as SemanticVersion, internal: isInternalPackage(entry.name), upstream: false, self: entry.name == name, - version: "", reason: null, }; if (!depObj.internal) { @@ -209,9 +208,10 @@ export const fetchPackageDependencies = async function ({ if (!versions.find((x) => x == entry.version)) { log.warn( "404", - `package ${ - version !== undefined ? atVersion(name, version) : name - } is not a valid choice of ${versions.reverse().join(", ")}` + `package ${packageReference( + name, + version + )} is not a valid choice of ${versions.reverse().join(", ")}` ); depObj.reason = "version404"; // eslint-disable-next-line require-atomic-updates @@ -222,10 +222,16 @@ export const fetchPackageDependencies = async function ({ } // add dependencies to pending list if (depObj.self || deep) { - const deps: NameVersionPair[] = _.toPairs( - pkgInfo.versions[entry.version]["dependencies"] - ).map((x: [PkgName, PkgVersion]): NameVersionPair => { - return { name: x[0], version: x[1] }; + const deps: NameVersionPair[] = ( + _.toPairs(pkgInfo.versions[entry.version]["dependencies"]) as [ + DomainName, + SemanticVersion + ][] + ).map((x): NameVersionPair => { + return { + name: x[0], + version: x[1], + }; }); deps.forEach((x) => pendingList.push(x)); } @@ -233,13 +239,9 @@ export const fetchPackageDependencies = async function ({ depsValid.push(depObj); log.verbose( "dependency", - `${ - entry.version !== undefined - ? atVersion(entry.name, entry.version) - : entry.name - } ${depObj.internal ? "[internal] " : ""}${ - depObj.upstream ? "[upstream]" : "" - }` + `${packageReference(entry.name, entry.version)} ${ + depObj.internal ? "[internal] " : "" + }${depObj.upstream ? "[upstream]" : ""}` ); } } diff --git a/src/types/another-npm-registry-client.d.ts b/src/types/another-npm-registry-client.d.ts index 1cdce144..b74b52ad 100644 --- a/src/types/another-npm-registry-client.d.ts +++ b/src/types/another-npm-registry-client.d.ts @@ -1,5 +1,7 @@ +import { Response } from "request"; +import { PkgInfo } from "./global"; + declare module "another-npm-registry-client" { - import request from "request"; export type NpmAuth = | { username: string; @@ -24,7 +26,7 @@ declare module "another-npm-registry-client" { error: Error | null, data: TData, raw: string, - res: request.Response + res: Response ) => void; export default class RegClient { diff --git a/src/types/domain-name.ts b/src/types/domain-name.ts new file mode 100644 index 00000000..ecb94487 --- /dev/null +++ b/src/types/domain-name.ts @@ -0,0 +1,83 @@ +import { Brand } from "ts-brand"; +import assert from "assert"; + +/** + * A string matching the format of a domain name. + * @example com.unity + * @example com.my-company + */ +export type DomainName = Brand; + +const segmentRegex = /^(?!.*--|^-.*|.*-$)[a-zA-Z0-9-]+$/; + +export const openUpmReverseDomainName = domainName("com.openupm"); + +function domainSegmentsIn(hostName: string): string[] { + return hostName.split("."); +} + +/** + * Creates a namespace by reversing the TDL of a host-name. + * @param hostname The host-name to reverse. + * @example unity.com becomes com.unity. + * @example registry.npmjs.org becomes org.npmjs. + * @example my-school.ac.at becomes at.ac.my-school + */ +export function namespaceFor(hostname: string): DomainName { + const segments = domainSegmentsIn(hostname); + const namespaceSegments = (function () { + const count = segments.length; + + /* + NOTE: This function does not handle domains with longer extensions + such as registry.example.team.com. In this case it would incorrectly only + return "com.team" even though "example" is also part of the domain. + Let's just hope this does not happen for now 🤞 + */ + + // 2-part domains, like unity.com + if (count < 3) return segments; + // Domains with two short extensions like my-school.ac.at + if (segments[count - 1].length <= 3 && segments[count - 2].length <= 3) + return segments.slice(count - 3); + // Domains with one extension such as registry.npmjs.org + return segments.slice(count - 2); + })(); + return namespaceSegments.reverse().join(".") as DomainName; +} + +/** + * Checks if a string is a domain name. Only does basic syntax validation. + * Does not check for correct segment-count etc. + * @param s The string + */ +export function isDomainName(s: string): s is DomainName { + const segments = domainSegmentsIn(s); + if (segments === null || segments.length === 0) return false; + return segments.every((segment) => segmentRegex.test(segment)); +} + +/** + * Detect if the given package name is an internal package + * @param name The name of the package + */ +export const isInternalPackage = (name: DomainName): boolean => { + const internals = [ + "com.unity.ugui", + "com.unity.2d.sprite", + "com.unity.2d.tilemap", + "com.unity.package-manager-ui", + "com.unity.ugui", + ]; + return /com.unity.modules/i.test(name) || internals.includes(name); +}; + +/** + * Constructs a domain-name from a string. + * @param s The string. + * @throws assert.AssertionError if string is not in valid format + */ +export function domainName(s: string): DomainName { + assert(isDomainName(s), `"${s}" is a domain name`); + return s; +} diff --git a/src/types/global.ts b/src/types/global.ts index 306f93a9..85d2f82c 100644 --- a/src/types/global.ts +++ b/src/types/global.ts @@ -1,15 +1,13 @@ import { NpmAuth } from "another-npm-registry-client"; - -export type PkgVersion = string; - -export type ReverseDomainName = string; - -export type PkgName = ReverseDomainName | `${ReverseDomainName}@${PkgVersion}`; +import { IpAddress } from "./ip-address"; +import { DomainName } from "./domain-name"; +import { PackageUrl } from "./package-url"; +import { SemanticVersion } from "./semantic-version"; +import { PackageId } from "./package-id"; +import { RegistryUrl } from "./registry-url"; export type Region = "us" | "cn"; -export type Registry = string; - export type EditorVersion = { major: number; minor: number; @@ -27,12 +25,12 @@ export type Env = { color: boolean; systemUser: boolean; wsl: boolean; - npmAuth?: Record; - auth: Record; + npmAuth?: Record; + auth: Record; upstream: boolean; - upstreamRegistry: string; - registry: string; - namespace: string; + upstreamRegistry: RegistryUrl; + registry: RegistryUrl; + namespace: DomainName | IpAddress; editorVersion: string | null; region: Region; manifestPath: string; @@ -51,15 +49,15 @@ export type Contact = { }; export type PkgVersionInfo = { - _id?: PkgName; + _id?: PackageId; _nodeVersion?: string; _npmVersion?: string; _rev?: string; name: string; - version: string; + version: SemanticVersion; unity?: string; unityRelease?: string; - dependencies?: Record; + dependencies?: Record; license?: string; displayName?: string; description?: string; @@ -74,29 +72,33 @@ export type PkgVersionInfo = { }; export type PkgInfo = { - name: ReverseDomainName; - _id?: PkgName; + name: DomainName; + _id?: DomainName; _rev?: string; _attachments?: Record; readme?: string; - versions: Record; - "dist-tags"?: { latest?: PkgVersion }; - version?: PkgVersion; + versions: Record; + "dist-tags"?: { latest?: SemanticVersion }; + version?: SemanticVersion; description?: string; keywords?: string[]; - time: Record<"created" | "modified" | PkgVersion, string>; + time: { + [key: SemanticVersion]: string; + created?: string; + modified?: string; + }; date?: Date; users?: Record; }; export type NameVersionPair = { - name: PkgName; - version: PkgVersion | undefined; + name: DomainName; + version: SemanticVersion | "latest" | undefined; }; export type Dependency = { - name: PkgName; - version: PkgVersion; + name: DomainName; + version?: SemanticVersion; upstream: boolean; self: boolean; internal: boolean; @@ -106,18 +108,18 @@ export type Dependency = { export type ScopedRegistry = { name: string; - url: string; - scopes: PkgName[]; + url: RegistryUrl; + scopes: DomainName[]; }; export type PkgManifest = { - dependencies: Record; + dependencies: Record; scopedRegistries?: ScopedRegistry[]; testables?: string[]; }; export type GlobalOptions = { - registry?: Registry; + registry?: string; verbose?: boolean; color?: boolean; upstream?: boolean; @@ -133,5 +135,5 @@ export type UpmAuth = { } & ({ token: string } | { _auth: string }); export type UPMConfig = { - npmAuth?: Record; + npmAuth?: Record; }; diff --git a/src/types/ip-address.ts b/src/types/ip-address.ts new file mode 100644 index 00000000..5a9de148 --- /dev/null +++ b/src/types/ip-address.ts @@ -0,0 +1,15 @@ +import { Brand } from "ts-brand"; +import net from "node:net"; + +/** + * A string that is either a valid v4 or v6 ip-address + */ +export type IpAddress = Brand; + +/** + * Checks if a string is valid {@link IpAddress} + * @param s The string + */ +export function isIpAddress(s: string): s is IpAddress { + return net.isIPv4(s) || net.isIPv6(s); +} diff --git a/src/types/npm-registry-fetch.d.ts b/src/types/npm-registry-fetch.d.ts index 3dedc984..ea3f1149 100644 --- a/src/types/npm-registry-fetch.d.ts +++ b/src/types/npm-registry-fetch.d.ts @@ -1,4 +1,4 @@ -import { Response } from "request"; +import { Response } from "node-fetch"; declare module "npm-registry-fetch" { class HttpErrorBase extends Error { diff --git a/src/types/package-id.ts b/src/types/package-id.ts new file mode 100644 index 00000000..5290502b --- /dev/null +++ b/src/types/package-id.ts @@ -0,0 +1,33 @@ +import { DomainName, isDomainName } from "./domain-name"; +import { isSemanticVersion, SemanticVersion } from "./semantic-version"; +import { trySplitAtFirstOccurrenceOf } from "../utils/string-utils"; + +/** + * Represents a package at a specific version. The version is here is a + * concrete semantic version. + * @example com.abc.my-package@1.2.3 + */ +export type PackageId = `${DomainName}@${SemanticVersion}`; + +/** + * Checks if a string is a package-id + * @param s The string + */ +export function isPackageId(s: string): s is PackageId { + const [name, version] = trySplitAtFirstOccurrenceOf(s, "@"); + return ( + isDomainName(name) && version !== undefined && isSemanticVersion(version) + ); +} + +/** + * Constructs a package-id + * @param name The package name + * @param version The version + */ +export function packageId( + name: DomainName, + version: SemanticVersion +): PackageId { + return `${name}@${version}`; +} diff --git a/src/types/package-reference.ts b/src/types/package-reference.ts new file mode 100644 index 00000000..9fc30d13 --- /dev/null +++ b/src/types/package-reference.ts @@ -0,0 +1,73 @@ +import { DomainName, isDomainName } from "./domain-name"; +import { isSemanticVersion, SemanticVersion } from "./semantic-version"; +import { isPackageUrl, PackageUrl } from "./package-url"; +import { trySplitAtFirstOccurrenceOf } from "../utils/string-utils"; +import assert from "assert"; + +/** + * A string with the format of one of the supported version tags. + * NOTE: Currently we only support "latest" + */ +type PackageTag = "latest"; + +/** + * Reference to a version, either directly by a semantic version or via an + * url or tag. + */ +export type VersionReference = SemanticVersion | PackageUrl | PackageTag; + +/** + * References a package. Could be just the name or a reference to a specific + * version. Not as specific as a {@link PackageId} as other version-formats + * besides semantic versions, such as "latest" are also allowed + */ +export type PackageReference = DomainName | `${DomainName}@${VersionReference}`; + +/** + * Checks if a string is a version-reference + * @param s The string + */ +function isVersionReference(s: string): s is VersionReference { + return s === "latest" || isSemanticVersion(s) || isPackageUrl(s); +} + +/** + * Checks if a string is a package-reference + * @param s The string + */ +export function isPackageReference(s: string): s is PackageReference { + const [name, version] = trySplitAtFirstOccurrenceOf(s, "@"); + return ( + isDomainName(name) && (version === undefined || isVersionReference(version)) + ); +} + +/** + * Split a package-reference into the name and version if present. + * @param reference The reference + */ +export function splitPackageReference( + reference: PackageReference +): [DomainName, VersionReference | undefined] { + return trySplitAtFirstOccurrenceOf(reference, "@") as [ + DomainName, + VersionReference | undefined + ]; +} + +/** + * Constructs a package-reference + * @param name The package-name. Will be validated to be a {@link DomainName} + * @param version Optional version-reference. Will be validated to be a {@link VersionReference} + */ +export function packageReference( + name: string, + version?: string +): PackageReference { + assert(isDomainName(name), `${name} is valid package-name`); + assert( + version === undefined || isVersionReference(version), + `"${version}" is valid version-reference` + ); + return version !== undefined ? `${name}@${version}` : name; +} diff --git a/src/types/package-url.ts b/src/types/package-url.ts new file mode 100644 index 00000000..59e9b25a --- /dev/null +++ b/src/types/package-url.ts @@ -0,0 +1,19 @@ +import { Brand } from "ts-brand"; + +/** + * A string of an url pointing to a local or remote package + */ +export type PackageUrl = Brand; + +const isGit = (version: string): boolean => version.startsWith("git"); + +const isHttp = (version: string): boolean => version.startsWith("http"); + +const isLocal = (version: string): boolean => version.startsWith("file"); + +/** + * Checks if a version is a package-url + * @param version The version + */ +export const isPackageUrl = (version: string): version is PackageUrl => + isGit(version) || isHttp(version) || isLocal(version); diff --git a/src/types/registry-url.ts b/src/types/registry-url.ts new file mode 100644 index 00000000..b3f20478 --- /dev/null +++ b/src/types/registry-url.ts @@ -0,0 +1,45 @@ +import { Brand } from "ts-brand"; +import assert from "assert"; + +/** + * A string of a http-based registry-url + */ +export type RegistryUrl = Brand; + +/** + * Checks that a string is a valid registry + * @param s The string + */ +export function isRegistryUrl(s: string): s is RegistryUrl { + return /http(s?):\/\//.test(s); +} + +/** + * Constructs a registry-url + * @param s The string + * @throws assert.AssertionError if string does not have valid format + */ +export function registryUrl(s: string): RegistryUrl { + assert(isRegistryUrl(s), `"${s}" is url`); + return s; +} + +/** + * Removes trailing slash from a registry-url + * @param registry The url + */ +export function removeTrailingSlash(registry: RegistryUrl): RegistryUrl { + if (registry.endsWith("/")) return registry.slice(0, -1) as RegistryUrl; + return registry; +} + +/** + * Attempts to coerce a string into a registry-url, by + * - Prepending http if it is missing + * - Removing trailing slashes + * @param s The string + */ +export function coerceRegistryUrl(s: string): RegistryUrl { + if (!s.toLowerCase().startsWith("http")) s = "http://" + s; + return removeTrailingSlash(registryUrl(s)); +} diff --git a/src/types/semantic-version.ts b/src/types/semantic-version.ts new file mode 100644 index 00000000..d7e16b45 --- /dev/null +++ b/src/types/semantic-version.ts @@ -0,0 +1,26 @@ +import { Brand } from "ts-brand"; +import semver from "semver/preload"; +import assert from "assert"; + +/** + * A string with a semantic-version format + * @see https://semver.org/ + */ +export type SemanticVersion = Brand; + +/** + * Checks if a string is a semantic version + * @param s The string + */ +export function isSemanticVersion(s: string): s is SemanticVersion { + return semver.parse(s) !== null; +} + +/** + * Constructs a semantic version from a string. + * @param s The string. Will be validated. + */ +export function semanticVersion(s: string): SemanticVersion { + assert(isSemanticVersion(s), `"${s}" is a semantic version`); + return s; +} diff --git a/src/utils/env.ts b/src/utils/env.ts index c49b6f1e..bca13744 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -1,40 +1,34 @@ import { Env, GlobalOptions } from "../types/global"; import log from "../logger"; import chalk from "chalk"; -import url from "url"; -import net from "node:net"; import { loadUpmConfig } from "./upm-config"; import path from "path"; import fs from "fs"; import yaml from "yaml"; +import { isIpAddress } from "../types/ip-address"; +import { namespaceFor, openUpmReverseDomainName } from "../types/domain-name"; +import { + coerceRegistryUrl, + RegistryUrl, + registryUrl, +} from "../types/registry-url"; +import url from "url"; + +export const env: Env = {}; -export const env: Env = { - auth: {}, - color: false, - cwd: "", - editorVersion: null, - manifestPath: "", - namespace: "", - region: "us", - registry: "", - systemUser: false, - upstream: false, - upstreamRegistry: "", - wsl: false, -}; // Parse env export const parseEnv = async function ( options: { _global: GlobalOptions } & Record, { checkPath }: { checkPath: unknown } ) { // set defaults - env.registry = "https://package.openupm.com"; - env.namespace = "com.openupm"; + env.registry = registryUrl("https://package.openupm.com"); env.cwd = ""; env.manifestPath = ""; + env.namespace = openUpmReverseDomainName; env.upstream = true; env.color = true; - env.upstreamRegistry = "https://packages.unity.com"; + env.upstreamRegistry = registryUrl("https://packages.unity.com"); env.systemUser = false; env.wsl = false; env.editorVersion = null; @@ -56,22 +50,18 @@ export const parseEnv = async function ( if (options._global.upstream === false) env.upstream = false; // region cn if (options._global.cn === true) { - env.registry = "https://package.openupm.cn"; - env.upstreamRegistry = "https://packages.unity.cn"; + env.registry = registryUrl("https://package.openupm.cn"); + env.upstreamRegistry = registryUrl("https://packages.unity.cn"); env.region = "cn"; log.notice("region", "cn"); } // registry if (options._global.registry) { - let registry = options._global.registry; - if (!registry.toLowerCase().startsWith("http")) - registry = "http://" + registry; - if (registry.endsWith("/")) registry = registry.slice(0, -1); - env.registry = registry; + env.registry = coerceRegistryUrl(options._global.registry); // TODO: Check hostname for null - const hostname = url.parse(registry).hostname as string; - if (net.isIP(hostname)) env.namespace = hostname; - else env.namespace = hostname.split(".").reverse().slice(0, 2).join("."); + const hostname = url.parse(env.registry).hostname as string; + if (isIpAddress(hostname)) env.namespace = hostname; + else env.namespace = namespaceFor(hostname); } // auth if (options._global.systemUser) env.systemUser = true; @@ -79,9 +69,9 @@ export const parseEnv = async function ( const upmConfig = await loadUpmConfig(); if (upmConfig) { env.npmAuth = upmConfig.npmAuth; - if (env.npmAuth) { - for (const reg in env.npmAuth) { - const regAuth = env.npmAuth[reg]; + if (env.npmAuth !== undefined) { + (Object.keys(env.npmAuth) as RegistryUrl[]).forEach((reg) => { + const regAuth = env.npmAuth![reg]; if ("token" in regAuth) { env.auth[reg] = { token: regAuth.token, @@ -104,7 +94,7 @@ export const parseEnv = async function ( ); log.warn("env.auth", regAuth); } - } + }); } } // log.verbose("env.npmAuth", env.npmAuth); diff --git a/src/utils/pkg-info.ts b/src/utils/pkg-info.ts index 1b225b13..62616842 100644 --- a/src/utils/pkg-info.ts +++ b/src/utils/pkg-info.ts @@ -1,8 +1,11 @@ -import { PkgInfo, PkgVersion } from "../types/global"; +import { PkgInfo } from "../types/global"; +import { SemanticVersion } from "../types/semantic-version"; const hasLatestDistTag = ( pkgInfo: Partial -): pkgInfo is Partial & { "dist-tags": { latest: PkgVersion } } => { +): pkgInfo is Partial & { + "dist-tags": { latest: SemanticVersion }; +} => { return pkgInfo["dist-tags"]?.["latest"] !== undefined; }; @@ -11,9 +14,9 @@ const hasLatestDistTag = ( * @param pkgInfo The package. All properties are assumed to be potentially missing */ export const tryGetLatestVersion = function (pkgInfo: { - "dist-tags"?: { latest?: PkgVersion }; - version?: PkgVersion; -}): PkgVersion | undefined { + "dist-tags"?: { latest?: SemanticVersion }; + version?: SemanticVersion; +}): SemanticVersion | undefined { if (hasLatestDistTag(pkgInfo)) return pkgInfo["dist-tags"].latest; else if (pkgInfo.version) return pkgInfo.version; }; diff --git a/src/utils/pkg-name.ts b/src/utils/pkg-name.ts deleted file mode 100644 index e663f4b1..00000000 --- a/src/utils/pkg-name.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PkgName, PkgVersion, ReverseDomainName } from "../types/global"; - -/** - * Split package-name, which may include a version into the actual name of the - * package and the version if it exists - */ -export const splitPkgName = function (pkgName: PkgName): { - name: ReverseDomainName; - version: PkgVersion | undefined; -} { - const segments = pkgName.split("@"); - const name = segments[0]; - const version = - segments.length > 1 - ? segments.slice(1, segments.length).join("@") - : undefined; - return { name, version }; -}; - -/** - * Merges a package name and version to create a package name for that specific version - * @param name The name of the package - * @param version The version of the package - */ -export const atVersion = ( - name: ReverseDomainName, - version: PkgVersion -): PkgName => `${name}@${version}`; - -/** - * Detect if the given package name is an internal package - * @param name The name of the package - */ -export const isInternalPackage = (name: ReverseDomainName): boolean => { - const internals = [ - "com.unity.ugui", - "com.unity.2d.sprite", - "com.unity.2d.tilemap", - "com.unity.package-manager-ui", - "com.unity.ugui", - ]; - return /com.unity.modules/i.test(name) || internals.includes(name); -}; diff --git a/src/utils/pkg-version.ts b/src/utils/pkg-version.ts deleted file mode 100644 index 171f37cd..00000000 --- a/src/utils/pkg-version.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PkgVersion } from "../types/global"; - -const isGit = (version: PkgVersion): boolean => version.startsWith("git"); - -const isHttp = (version: PkgVersion): boolean => version.startsWith("http"); - -const isLocal = (version: PkgVersion): boolean => version.startsWith("file"); - -export const isUrlVersion = (version: PkgVersion): boolean => - isGit(version) || isHttp(version) || isLocal(version); diff --git a/src/utils/string-utils.ts b/src/utils/string-utils.ts new file mode 100644 index 00000000..1e57281b --- /dev/null +++ b/src/utils/string-utils.ts @@ -0,0 +1,8 @@ +export function trySplitAtFirstOccurrenceOf( + s: string, + split: string +): [string, string | undefined] { + const elements = s.split(split); + if (elements.length === 1) return [s, undefined]; + return [elements[0], elements.slice(1).join(split)]; +} diff --git a/test/data-pkg-info.ts b/test/data-pkg-info.ts new file mode 100644 index 00000000..278089be --- /dev/null +++ b/test/data-pkg-info.ts @@ -0,0 +1,115 @@ +import { PkgInfo, PkgVersionInfo } from "../src/types/global"; +import assert from "assert"; +import { DomainName, isDomainName } from "../src/types/domain-name"; +import { + isSemanticVersion, + SemanticVersion, +} from "../src/types/semantic-version"; +import { packageId } from "../src/types/package-id"; + +/** + * Builder class for {@link PkgVersionInfo} + */ +class VersionInfoBuilder { + readonly version: PkgVersionInfo; + + constructor(name: DomainName, version: SemanticVersion) { + this.version = { + name, + _id: packageId(name, version), + version, + dependencies: {}, + contributors: [], + }; + } + + /** + * Add a dependency to this version + * @param name The name of the dependency + * @param version The version + */ + addDependency(name: string, version: string): VersionInfoBuilder { + assert(isDomainName(name), `${name} is domain name`); + assert(isSemanticVersion(version), `${version} is semantic version`); + this.version.dependencies![name] = version; + return this; + } + + /** + * Set an arbitrary value on the version + * @param key The key + * @param value The value + */ + set< + TKey extends keyof Omit< + PkgVersionInfo, + "version" | "name" | "dependencies" | "_id" + > + >(key: TKey, value: PkgVersionInfo[TKey]): VersionInfoBuilder { + this.version[key] = value; + return this; + } +} + +/** + * Builder class for {@link PkgInfo} + */ +class PackageInfoBuilder { + readonly package: PkgInfo; + + constructor(name: DomainName) { + this.package = { + name, + _id: name, + versions: {}, + time: {}, + users: {}, + _attachments: {}, + }; + } + + /** + * Adds a version to this package + * @param version The name of the version + * @param build A builder function + */ + addVersion( + version: string, + build?: (builder: VersionInfoBuilder) => unknown + ): PackageInfoBuilder { + assert(isSemanticVersion(version), `${version} is semantic version`); + const builder = new VersionInfoBuilder(this.package.name, version); + if (build !== undefined) build(builder); + this.package.versions[version] = builder.version; + this.package["dist-tags"] = { + latest: version, + }; + return this; + } + + set< + TKey extends keyof Omit< + PkgInfo, + "name" | "version" | "versions" | "dist-tags" | "_id" + > + >(key: TKey, value: PkgInfo[TKey]): PackageInfoBuilder { + this.package[key] = value; + return this; + } +} + +/** + * Helper for building a {@link PkgInfo} object. Does validation and also + * sets repeated properties for you + * @param name The name of the package + * @param build A builder function + */ +export function buildPackageInfo( + name: string, + build?: (builder: PackageInfoBuilder) => unknown +): PkgInfo { + assert(isDomainName(name), `${name} is domain name`); + const builder = new PackageInfoBuilder(name); + if (build !== undefined) build(builder); + return builder.package; +} diff --git a/test/data-pkg-manifest.ts b/test/data-pkg-manifest.ts new file mode 100644 index 00000000..ea6404fe --- /dev/null +++ b/test/data-pkg-manifest.ts @@ -0,0 +1,87 @@ +import { PkgManifest } from "../src/types/global"; +import assert from "assert"; +import { domainName, isDomainName } from "../src/types/domain-name"; +import { exampleRegistryUrl } from "./mock-registry"; +import { isSemanticVersion } from "../src/types/semantic-version"; + +/** + * Builder class for {@link PkgManifest} + */ +class PkgManifestBuilder { + readonly manifest: PkgManifest; + + constructor() { + this.manifest = { + dependencies: {}, + }; + } + + /** + * Add a scope to the manifests scoped registry + * @param name The name of the scope + */ + addScope(name: string): PkgManifestBuilder { + assert(isDomainName(name), `${name} is domain name`); + + if (this.manifest.scopedRegistries === undefined) + this.manifest.scopedRegistries = [ + { + name: "example.com", + scopes: [domainName("com.example")], + url: exampleRegistryUrl, + }, + ]; + + const registry = this.manifest.scopedRegistries![0]; + registry.scopes = [name, ...registry.scopes]; + registry.scopes.sort(); + + return this; + } + + /** + * Add a testable to the manifest + * @param name The packages name + */ + addTestable(name: string): PkgManifestBuilder { + assert(isDomainName(name), `${name} is domain name`); + if (this.manifest.testables === undefined) this.manifest.testables = []; + this.manifest.testables.push(name); + this.manifest.testables.sort(); + return this; + } + + /** + * Add a dependency to the manifests scoped registry + * @param name The packages name + * @param version The packages version + * @param withScope Whether to also add the package to the scope + * @param testable Whether to also add the package to the testables + */ + addDependency( + name: string, + version: string, + withScope: boolean, + testable: boolean + ): PkgManifestBuilder { + assert(isDomainName(name), `${name} is domain name`); + assert(isSemanticVersion(version), `${version} is semantic version`); + if (withScope) this.addScope(name); + if (testable) this.addTestable(name); + this.manifest.dependencies[name] = version; + return this; + } +} + +/** + * Builder function for {@link PkgManifest}. All dependencies will be put + * into a default scoped-registry referencing an example registry + * @param build A builder function. + */ +export function buildPackageManifest( + build?: (builder: PkgManifestBuilder) => unknown +) { + const builder = new PkgManifestBuilder(); + if (build !== undefined) build(builder); + return builder.manifest; +} diff --git a/test/manifest-assertions.ts b/test/manifest-assertions.ts index c1ba79c8..9cbbd2c6 100644 --- a/test/manifest-assertions.ts +++ b/test/manifest-assertions.ts @@ -1,6 +1,9 @@ -import { PkgManifest, PkgName, PkgVersion } from "../src/types/global"; +import { PkgManifest } from "../src/types/global"; import { loadManifest } from "../src/utils/manifest"; import should from "should"; +import { DomainName } from "../src/types/domain-name"; +import { SemanticVersion } from "../src/types/semantic-version"; +import { PackageUrl } from "../src/types/package-url"; export function shouldHaveManifest(): PkgManifest { const manifest = loadManifest(); @@ -15,8 +18,8 @@ export function shouldHaveNoManifest() { export function shouldHaveDependency( manifest: PkgManifest, - name: PkgName, - version: PkgVersion + name: DomainName, + version: SemanticVersion | PackageUrl ) { should(manifest.dependencies[name]).equal(version); } @@ -24,13 +27,17 @@ export function shouldHaveDependency( export function shouldNotHaveAnyDependencies(manifest: PkgManifest) { should(manifest.dependencies).be.empty(); } -export function shouldNotHaveDependency(manifest: PkgManifest, name: PkgName) { + +export function shouldNotHaveDependency( + manifest: PkgManifest, + name: DomainName +) { should(manifest.dependencies[name]).be.undefined(); } export function shouldHaveRegistryWithScopes( manifest: PkgManifest, - scopes: PkgName[] + scopes: DomainName[] ) { should(manifest.scopedRegistries).not.be.undefined(); manifest diff --git a/test/mock-registry.ts b/test/mock-registry.ts index 113fd0d8..bcddc0e9 100644 --- a/test/mock-registry.ts +++ b/test/mock-registry.ts @@ -1,9 +1,13 @@ -import { PkgInfo, PkgName } from "../src/types/global"; +import { PkgInfo } from "../src/types/global"; import nock from "nock"; import { SearchEndpointResult } from "./types"; +import { domainName, isDomainName } from "../src/types/domain-name"; +import assert from "assert"; +import { registryUrl } from "../src/types/registry-url"; -export const unityRegistryUrl = "https://packages.unity.com"; -export const exampleRegistryUrl = "http://example.com"; +export const unityRegistryUrl = registryUrl("https://packages.unity.com"); +export const exampleRegistryUrl = registryUrl("http://example.com"); +export const exampleRegistryReverseDomain = domainName("com.example"); export function startMockRegistry() { if (!nock.isActive()) nock.activate(); @@ -23,7 +27,8 @@ export function registerRemoteUpstreamPkg(pkg: PkgInfo) { nock(exampleRegistryUrl).persist().get(`/${pkg.name}`).reply(404); } -export function registerMissingPackage(name: PkgName) { +export function registerMissingPackage(name: string) { + assert(isDomainName(name)); nock(exampleRegistryUrl).persist().get(`/${name}`).reply(404); nock(unityRegistryUrl).persist().get(`/${name}`).reply(404); } diff --git a/test/test-cmd-add.ts b/test/test-cmd-add.ts index b24d2a0d..ab5910da 100644 --- a/test/test-cmd-add.ts +++ b/test/test-cmd-add.ts @@ -1,6 +1,5 @@ import "should"; import { add, AddOptions } from "../src/cmd-add"; -import { PkgInfo, PkgManifest } from "../src/types/global"; import { exampleRegistryUrl, registerMissingPackage, @@ -15,8 +14,30 @@ import { shouldHaveDependency, shouldHaveManifest, } from "./manifest-assertions"; +import { buildPackageInfo } from "./data-pkg-info"; +import { buildPackageManifest } from "./data-pkg-manifest"; +import { domainName } from "../src/types/domain-name"; +import { PackageUrl } from "../src/types/package-url"; +import { semanticVersion } from "../src/types/semantic-version"; +import { packageReference } from "../src/types/package-reference"; describe("cmd-add.ts", function () { + const packageMissing = domainName("pkg-not-exist"); + const packageA = domainName("com.base.package-a"); + const packageB = domainName("com.base.package-b"); + const packageC = domainName("com.base.package-c"); + const packageD = domainName("com.base.package-d"); + const packageUp = domainName("com.upstream.package-up"); + const packageLowerEditor = domainName( + "com.base.package-with-lower-editor-version" + ); + const packageHigherEditor = domainName( + "com.base.package-with-higher-editor-version" + ); + const packageWrongEditor = domainName( + "com.base.package-with-wrong-editor-version" + ); + const options: AddOptions = { _global: { registry: exampleRegistryUrl, @@ -49,187 +70,65 @@ describe("cmd-add.ts", function () { }; describe("add", function () { let mockConsole: MockConsole = null!; - const remotePkgInfoA: PkgInfo = { - name: "com.base.package-a", - versions: { - "0.1.0": { - name: "com.base.package-a", - version: "0.1.0", - dependencies: {}, - }, - "1.0.0": { - name: "com.base.package-a", - version: "1.0.0", - dependencies: {}, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoB: PkgInfo = { - name: "com.base.package-b", - versions: { - "1.0.0": { - name: "com.base.package-b", - version: "1.0.0", - dependencies: {}, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoC: PkgInfo = { - name: "com.base.package-c", - versions: { - "1.0.0": { - name: "com.base.package-c", - version: "1.0.0", - dependencies: { - "com.base.package-d": "1.0.0", - "com.unity.modules.x": "1.0.0", - }, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoD: PkgInfo = { - name: "com.base.package-d", - versions: { - "1.0.0": { - name: "com.base.package-d", - version: "1.0.0", - dependencies: { - "com.upstream.package-up": "1.0.0", - }, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoWithLowerEditorVersion: PkgInfo = { - name: "com.base.package-with-lower-editor-version", - versions: { - "1.0.0": { - name: "com.base.package-with-lower-editor-version", - version: "1.0.0", - unity: "2019.1", - unityRelease: "0b1", - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoWithHigherEditorVersion: PkgInfo = { - name: "com.base.package-with-higher-editor-version", - versions: { - "1.0.0": { - name: "com.base.package-with-higher-editor-version", - version: "1.0.0", - unity: "2020.2", - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoWithWrongEditorVersion: PkgInfo = { - name: "com.base.package-with-wrong-editor-version", - versions: { - "1.0.0": { - name: "com.base.package-with-wrong-editor-version", - version: "1.0.0", - unity: "2020", - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoUp: PkgInfo = { - name: "com.upstream.package-up", - versions: { - "1.0.0": { - name: "com.upstream.package-up", - version: "1.0.0", - dependencies: {}, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const defaultManifest: PkgManifest = { - dependencies: {}, - }; - const expectedManifestA: PkgManifest = { - dependencies: { - "com.base.package-a": "1.0.0", - }, - scopedRegistries: [ - { - name: "example.com", - scopes: ["com.base.package-a", "com.example"], - url: exampleRegistryUrl, - }, - ], - }; - const expectedManifestAB: PkgManifest = { - dependencies: { - "com.base.package-a": "1.0.0", - "com.base.package-b": "1.0.0", - }, - scopedRegistries: [ - { - name: "example.com", - scopes: ["com.base.package-a", "com.base.package-b", "com.example"], - url: exampleRegistryUrl, - }, - ], - }; - const expectedManifestC: PkgManifest = { - dependencies: { - "com.base.package-c": "1.0.0", - }, - scopedRegistries: [ - { - name: "example.com", - scopes: ["com.base.package-c", "com.base.package-d", "com.example"], - url: exampleRegistryUrl, - }, - ], - }; - const expectedManifestUpstream: PkgManifest = { - dependencies: { - "com.upstream.package-up": "1.0.0", - }, - }; - const expectedManifestTestable: PkgManifest = { - dependencies: { - "com.base.package-a": "1.0.0", - }, - scopedRegistries: [ - { - name: "example.com", - scopes: ["com.base.package-a", "com.example"], - url: exampleRegistryUrl, - }, - ], - testables: ["com.base.package-a"], - }; + + const remotePkgInfoA = buildPackageInfo(packageA, (pkg) => + pkg.addVersion("0.1.0").addVersion("1.0.0") + ); + const remotePkgInfoB = buildPackageInfo(packageB, (pkg) => + pkg.addVersion("1.0.0") + ); + const remotePkgInfoC = buildPackageInfo(packageC, (pkg) => + pkg.addVersion("1.0.0", (version) => + version + .addDependency(packageD, "1.0.0") + .addDependency("com.unity.modules.x", "1.0.0") + ) + ); + const remotePkgInfoD = buildPackageInfo(packageD, (pkg) => + pkg.addVersion("1.0.0", (version) => { + return version.addDependency(packageUp, "1.0.0"); + }) + ); + const remotePkgInfoWithLowerEditorVersion = buildPackageInfo( + packageLowerEditor, + (pkg) => + pkg.addVersion("1.0.0", (version) => + version.set("unity", "2019.1").set("unityRelease", "0b1") + ) + ); + const remotePkgInfoWithHigherEditorVersion = buildPackageInfo( + packageHigherEditor, + (pkg) => + pkg.addVersion("1.0.0", (version) => version.set("unity", "2020.2")) + ); + const remotePkgInfoWithWrongEditorVersion = buildPackageInfo( + packageWrongEditor, + (pkg) => + pkg.addVersion("1.0.0", (version) => version.set("unity", "2020")) + ); + const remotePkgInfoUp = buildPackageInfo(packageUp, (pkg) => + pkg.addVersion("1.0.0") + ); + + const defaultManifest = buildPackageManifest(); + const expectedManifestA = buildPackageManifest((manifest) => + manifest.addDependency(packageA, "1.0.0", true, false) + ); + const expectedManifestAB = buildPackageManifest((manifest) => + manifest + .addDependency(packageA, "1.0.0", true, false) + .addDependency(packageB, "1.0.0", true, false) + ); + const expectedManifestC = buildPackageManifest((manifest) => + manifest.addDependency(packageC, "1.0.0", true, false).addScope(packageD) + ); + const expectedManifestUpstream = buildPackageManifest((manifest) => + manifest.addDependency(packageUp, "1.0.0", false, false) + ); + const expectedManifestTestable = buildPackageManifest((manifest) => + manifest.addDependency(packageA, "1.0.0", true, true) + ); + beforeEach(function () { removeWorkDir("test-openupm-cli"); createWorkDir("test-openupm-cli", { @@ -246,7 +145,7 @@ describe("cmd-add.ts", function () { registerRemotePkg(remotePkgInfoWithHigherEditorVersion); registerRemotePkg(remotePkgInfoWithWrongEditorVersion); registerRemoteUpstreamPkg(remotePkgInfoUp); - registerMissingPackage("pkg-not-exist"); + registerMissingPackage(packageMissing); mockConsole = attachMockConsole(); }); @@ -257,7 +156,7 @@ describe("cmd-add.ts", function () { }); it("add pkg", async function () { - const retCode = await add("com.base.package-a", options); + const retCode = await add(packageA, options); retCode.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestA); @@ -265,7 +164,10 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg@1.0.0", async function () { - const retCode = await add("com.base.package-a@1.0.0", options); + const retCode = await add( + packageReference(packageA, semanticVersion("1.0.0")), + options + ); retCode.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestA); @@ -273,7 +175,7 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg@latest", async function () { - const retCode = await add("com.base.package-a@latest", options); + const retCode = await add(packageReference(packageA, "latest"), options); retCode.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestA); @@ -281,9 +183,15 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg@0.1.0 then pkg@1.0.0", async function () { - const retCode1 = await add("com.base.package-a@0.1.0", options); + const retCode1 = await add( + packageReference(packageA, semanticVersion("0.1.0")), + options + ); retCode1.should.equal(0); - const retCode2 = await add("com.base.package-a@1.0.0", options); + const retCode2 = await add( + packageReference(packageA, semanticVersion("1.0.0")), + options + ); retCode2.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestA); @@ -291,9 +199,15 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add exited pkg version", async function () { - const retCode1 = await add("com.base.package-a@1.0.0", options); + const retCode1 = await add( + packageReference(packageA, semanticVersion("1.0.0")), + options + ); retCode1.should.equal(0); - const retCode2 = await add("com.base.package-a@1.0.0", options); + const retCode2 = await add( + packageReference(packageA, semanticVersion("1.0.0")), + options + ); retCode2.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestA); @@ -301,7 +215,10 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg@not-exist-version", async function () { - const retCode = await add("com.base.package-a@2.0.0", options); + const retCode = await add( + packageReference(packageA, semanticVersion("2.0.0")), + options + ); retCode.should.equal(1); const manifest = shouldHaveManifest(); manifest.should.deepEqual(defaultManifest); @@ -311,44 +228,41 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "1.0.0").should.be.ok(); }); it("add pkg@http", async function () { - const gitUrl = "https://github.com/yo/com.base.package-a"; - const retCode = await add("com.base.package-a@" + gitUrl, options); + const gitUrl = "https://github.com/yo/com.base.package-a" as PackageUrl; + const retCode = await add(packageReference(packageA, gitUrl), options); retCode.should.equal(0); const manifest = shouldHaveManifest(); - shouldHaveDependency(manifest, "com.base.package-a", gitUrl); + shouldHaveDependency(manifest, packageA, gitUrl); mockConsole.hasLineIncluding("out", "added").should.be.ok(); mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg@git", async function () { - const gitUrl = "git@github.com:yo/com.base.package-a"; - const retCode = await add("com.base.package-a@" + gitUrl, options); + const gitUrl = "git@github.com:yo/com.base.package-a" as PackageUrl; + const retCode = await add(packageReference(packageA, gitUrl), options); retCode.should.equal(0); const manifest = shouldHaveManifest(); - shouldHaveDependency(manifest, "com.base.package-a", gitUrl); + shouldHaveDependency(manifest, packageA, gitUrl); mockConsole.hasLineIncluding("out", "added").should.be.ok(); mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg@file", async function () { - const fileUrl = "file../yo/com.base.package-a"; - const retCode = await add("com.base.package-a@" + fileUrl, options); + const fileUrl = "file../yo/com.base.package-a" as PackageUrl; + const retCode = await add(packageReference(packageA, fileUrl), options); retCode.should.equal(0); const manifest = shouldHaveManifest(); - shouldHaveDependency(manifest, "com.base.package-a", fileUrl); + shouldHaveDependency(manifest, packageA, fileUrl); mockConsole.hasLineIncluding("out", "added").should.be.ok(); mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg-not-exist", async function () { - const retCode = await add("pkg-not-exist", options); + const retCode = await add(packageMissing, options); retCode.should.equal(1); const manifest = shouldHaveManifest(); manifest.should.deepEqual(defaultManifest); mockConsole.hasLineIncluding("out", "package not found").should.be.ok(); }); it("add more than one pkgs", async function () { - const retCode = await add( - ["com.base.package-a", "com.base.package-b"], - options - ); + const retCode = await add([packageA, packageB], options); retCode.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestAB); @@ -361,7 +275,7 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg from upstream", async function () { - const retCode = await add("com.upstream.package-up", upstreamOptions); + const retCode = await add(packageUp, upstreamOptions); retCode.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestUpstream); @@ -371,14 +285,17 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg-not-exist from upstream", async function () { - const retCode = await add("pkg-not-exist", upstreamOptions); + const retCode = await add(packageMissing, upstreamOptions); retCode.should.equal(1); const manifest = shouldHaveManifest(); manifest.should.deepEqual(defaultManifest); mockConsole.hasLineIncluding("out", "package not found").should.be.ok(); }); it("add pkg with nested dependencies", async function () { - const retCode = await add("com.base.package-c@latest", upstreamOptions); + const retCode = await add( + packageReference(packageC, "latest"), + upstreamOptions + ); retCode.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestC); @@ -386,7 +303,7 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg with tests", async function () { - const retCode = await add("com.base.package-a", testableOptions); + const retCode = await add(packageA, testableOptions); retCode.should.equal(0); const manifest = shouldHaveManifest(); manifest.should.deepEqual(expectedManifestTestable); @@ -394,47 +311,32 @@ describe("cmd-add.ts", function () { mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg with lower editor version", async function () { - const retCode = await add( - "com.base.package-with-lower-editor-version", - testableOptions - ); + const retCode = await add(packageLowerEditor, testableOptions); retCode.should.equal(0); mockConsole.hasLineIncluding("out", "added").should.be.ok(); mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); }); it("add pkg with higher editor version", async function () { - const retCode = await add( - "com.base.package-with-higher-editor-version", - testableOptions - ); + const retCode = await add(packageHigherEditor, testableOptions); retCode.should.equal(1); mockConsole .hasLineIncluding("out", "requires 2020.2 but found 2019.2.13f1") .should.be.ok(); }); it("force add pkg with higher editor version", async function () { - const retCode = await add( - "com.base.package-with-higher-editor-version", - forceOptions - ); + const retCode = await add(packageHigherEditor, forceOptions); retCode.should.equal(0); mockConsole .hasLineIncluding("out", "requires 2020.2 but found 2019.2.13f1") .should.be.ok(); }); it("add pkg with wrong editor version", async function () { - const retCode = await add( - "com.base.package-with-wrong-editor-version", - testableOptions - ); + const retCode = await add(packageWrongEditor, testableOptions); retCode.should.equal(1); mockConsole.hasLineIncluding("out", "2020 is not valid").should.be.ok(); }); it("force add pkg with wrong editor version", async function () { - const retCode = await add( - "com.base.package-with-wrong-editor-version", - forceOptions - ); + const retCode = await add(packageWrongEditor, forceOptions); retCode.should.equal(0); mockConsole.hasLineIncluding("out", "2020 is not valid").should.be.ok(); }); diff --git a/test/test-cmd-deps.ts b/test/test-cmd-deps.ts index 082e01a9..fd91acb5 100644 --- a/test/test-cmd-deps.ts +++ b/test/test-cmd-deps.ts @@ -8,9 +8,11 @@ import { startMockRegistry, stopMockRegistry, } from "./mock-registry"; -import { PkgInfo } from "../src/types/global"; import { createWorkDir, getWorkDir, removeWorkDir } from "./mock-work-dir"; import { attachMockConsole, MockConsole } from "./mock-console"; +import { buildPackageInfo } from "./data-pkg-info"; +import { domainName } from "../src/types/domain-name"; +import { packageReference } from "../src/types/package-reference"; describe("cmd-deps.ts", function () { const options: DepsOptions = { @@ -22,52 +24,20 @@ describe("cmd-deps.ts", function () { describe("deps", function () { let mockConsole: MockConsole = null!; - const remotePkgInfoA: PkgInfo = { - name: "com.example.package-a", - versions: { - "1.0.0": { - name: "com.example.package-a", - version: "1.0.0", - dependencies: { - "com.example.package-b": "1.0.0", - }, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoB: PkgInfo = { - name: "com.example.package-b", - versions: { - "1.0.0": { - name: "com.example.package-b", - version: "1.0.0", - dependencies: { - "com.example.package-up": "1.0.0", - }, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; - const remotePkgInfoUp: PkgInfo = { - name: "com.example.package-up", - versions: { - "1.0.0": { - name: "com.example.package-up", - version: "1.0.0", - dependencies: {}, - }, - }, - "dist-tags": { - latest: "1.0.0", - }, - time: {}, - }; + const remotePkgInfoA = buildPackageInfo("com.example.package-a", (pkg) => + pkg.addVersion("1.0.0", (version) => + version.addDependency("com.example.package-b", "1.0.0") + ) + ); + const remotePkgInfoB = buildPackageInfo("com.example.package-b", (pkg) => + pkg.addVersion("1.0.0", (version) => + version.addDependency("com.example.package-up", "1.0.0") + ) + ); + const remotePkgInfoUp = buildPackageInfo("com.example.package-up", (pkg) => + pkg.addVersion("1.0.0") + ); + beforeEach(function () { removeWorkDir("test-openupm-cli"); createWorkDir("test-openupm-cli", { manifest: true }); @@ -85,53 +55,52 @@ describe("cmd-deps.ts", function () { mockConsole.detach(); }); it("deps pkg", async function () { - const retCode = await deps("com.example.package-a", options); + const retCode = await deps(remotePkgInfoA.name, options); retCode.should.equal(0); - mockConsole - .hasLineIncluding("out", "com.example.package-b") - .should.be.ok(); + mockConsole.hasLineIncluding("out", remotePkgInfoB.name).should.be.ok(); }); it("deps pkg --deep", async function () { - const retCode = await deps("com.example.package-a", { + const retCode = await deps(remotePkgInfoA.name, { ...options, deep: true, }); retCode.should.equal(0); - mockConsole - .hasLineIncluding("out", "com.example.package-b") - .should.be.ok(); - mockConsole - .hasLineIncluding("out", "com.example.package-up") - .should.be.ok(); + mockConsole.hasLineIncluding("out", remotePkgInfoB.name).should.be.ok(); + mockConsole.hasLineIncluding("out", remotePkgInfoUp.name).should.be.ok(); }); it("deps pkg@latest", async function () { - const retCode = await deps("com.example.package-a@latest", options); + const retCode = await deps( + packageReference(remotePkgInfoA.name, "latest"), + options + ); retCode.should.equal(0); - mockConsole - .hasLineIncluding("out", "com.example.package-b") - .should.be.ok(); + mockConsole.hasLineIncluding("out", remotePkgInfoB.name).should.be.ok(); }); it("deps pkg@1.0.0", async function () { - const retCode = await deps("com.example.package-a@1.0.0", options); + const retCode = await deps( + packageReference(remotePkgInfoA.name, "1.0.0"), + options + ); retCode.should.equal(0); - mockConsole - .hasLineIncluding("out", "com.example.package-b") - .should.be.ok(); + mockConsole.hasLineIncluding("out", remotePkgInfoB.name).should.be.ok(); }); it("deps pkg@not-exist-version", async function () { - const retCode = await deps("com.example.package-a@2.0.0", options); + const retCode = await deps( + packageReference(remotePkgInfoA.name, "2.0.0"), + options + ); retCode.should.equal(0); mockConsole .hasLineIncluding("out", "is not a valid choice") .should.be.ok(); }); it("deps pkg-not-exist", async function () { - const retCode = await deps("pkg-not-exist", options); + const retCode = await deps(domainName("pkg-not-exist"), options); retCode.should.equal(0); mockConsole.hasLineIncluding("out", "not found").should.be.ok(); }); it("deps pkg upstream", async function () { - const retCode = await deps("com.example.package-up", options); + const retCode = await deps(remotePkgInfoUp.name, options); retCode.should.equal(0); }); }); diff --git a/test/test-cmd-login.ts b/test/test-cmd-login.ts index 2c42d653..62dc4f85 100644 --- a/test/test-cmd-login.ts +++ b/test/test-cmd-login.ts @@ -1,38 +1,20 @@ import "nock"; -import should from "should"; -import { - generateNpmrcLines, - getNpmrcPath, - validateRegistry, -} from "../src/cmd-login"; +import { generateNpmrcLines, getNpmrcPath } from "../src/cmd-login"; +import { registryUrl } from "../src/types/registry-url"; describe("cmd-login.ts", function () { - describe("validateRegistry", function () { - it("should validate http", async function () { - validateRegistry("http://registry.npmjs.org/").should.be.ok(); - }); - it("should validate https", async function () { - validateRegistry("https://registry.npmjs.org/").should.be.ok(); - }); - it("should reject without http protocal", async function () { - should(function () { - validateRegistry("registry.npmjs.org/"); - }).throw("The registry address should starts with http(s)://"); - }); - }); - describe("generateNpmrcLines", function () { it("should append token to empty content", async function () { generateNpmrcLines( "", - "http://registry.npmjs.org/", + registryUrl("http://registry.npmjs.org/"), "123-456-789" ).should.deepEqual(["//registry.npmjs.org/:_authToken=123-456-789"]); }); it("should append token to exist contents", async function () { generateNpmrcLines( "registry=https://registry.npmjs.org/", - "http://registry.npmjs.org/", + registryUrl(" registry(http://registry.npmjs.org/"), "123-456-789" ).should.deepEqual([ "registry=https://registry.npmjs.org/", @@ -42,7 +24,7 @@ describe("cmd-login.ts", function () { it("should replace token to exist contents", async function () { generateNpmrcLines( "registry=https://registry.npmjs.org/\n//127.0.0.1:4873/:_authToken=blar-blar-blar\n//registry.npmjs.org/:_authToken=blar-blar-blar", - "http://registry.npmjs.org/", + registryUrl("http://registry.npmjs.org/"), "123-456-789" ).should.deepEqual([ "registry=https://registry.npmjs.org/", @@ -53,19 +35,19 @@ describe("cmd-login.ts", function () { it("should handle registry without trailing slash", async function () { generateNpmrcLines( "", - "http://registry.npmjs.org", + registryUrl("http://registry.npmjs.org"), "123-456-789" ).should.deepEqual(["//registry.npmjs.org/:_authToken=123-456-789"]); }); it("should quote token if necessary", async function () { generateNpmrcLines( "", - "http://registry.npmjs.org/", + registryUrl("http://registry.npmjs.org/"), "=123-456-789=" ).should.deepEqual(['//registry.npmjs.org/:_authToken="=123-456-789="']); generateNpmrcLines( "", - "http://registry.npmjs.org/", + registryUrl("http://registry.npmjs.org/"), "?123-456-789?" ).should.deepEqual(['//registry.npmjs.org/:_authToken="?123-456-789?"']); }); diff --git a/test/test-cmd-remove.ts b/test/test-cmd-remove.ts index a288a5bc..6ad76d58 100644 --- a/test/test-cmd-remove.ts +++ b/test/test-cmd-remove.ts @@ -1,7 +1,9 @@ import "should"; import { remove } from "../src/cmd-remove"; -import { PkgManifest } from "../src/types/global"; -import { exampleRegistryUrl } from "./mock-registry"; +import { + exampleRegistryReverseDomain, + exampleRegistryUrl, +} from "./mock-registry"; import { createWorkDir, getWorkDir, removeWorkDir } from "./mock-work-dir"; import { attachMockConsole, MockConsole } from "./mock-console"; import { @@ -9,27 +11,25 @@ import { shouldHaveRegistryWithScopes, shouldNotHaveDependency, } from "./manifest-assertions"; +import { buildPackageManifest } from "./data-pkg-manifest"; +import { domainName } from "../src/types/domain-name"; +import { semanticVersion } from "../src/types/semantic-version"; +import { packageReference } from "../src/types/package-reference"; + +const packageA = domainName("com.example.package-a"); +const packageB = domainName("com.example.package-b"); +const missingPackage = domainName("pkg-not-exist"); describe("cmd-remove.ts", function () { describe("remove", function () { let mockConsole: MockConsole = null!; - const defaultManifest: PkgManifest = { - dependencies: { - "com.example.package-a": "1.0.0", - "com.example.package-b": "1.0.0", - }, - scopedRegistries: [ - { - name: "example.com", - scopes: [ - "com.example", - "com.example.package-a", - "com.example.package-b", - ], - url: exampleRegistryUrl, - }, - ], - }; + + const defaultManifest = buildPackageManifest((manifest) => + manifest + .addDependency(packageA, "1.0.0", true, false) + .addDependency(packageB, "1.0.0", true, false) + ); + beforeEach(function () { removeWorkDir("test-openupm-cli"); createWorkDir("test-openupm-cli", { @@ -48,13 +48,13 @@ describe("cmd-remove.ts", function () { chdir: getWorkDir("test-openupm-cli"), }, }; - const retCode = await remove("com.example.package-a", options); + const retCode = await remove(packageA, options); retCode.should.equal(0); const manifest = shouldHaveManifest(); - shouldNotHaveDependency(manifest, "com.example.package-a"); + shouldNotHaveDependency(manifest, packageA); shouldHaveRegistryWithScopes(manifest, [ - "com.example", - "com.example.package-b", + exampleRegistryReverseDomain, + packageB, ]); mockConsole.hasLineIncluding("out", "removed ").should.be.ok(); mockConsole.hasLineIncluding("out", "open Unity").should.be.ok(); @@ -66,7 +66,10 @@ describe("cmd-remove.ts", function () { chdir: getWorkDir("test-openupm-cli"), }, }; - const retCode = await remove("com.example.package-a@1.0.0", options); + const retCode = await remove( + packageReference(packageA, semanticVersion("1.0.0")), + options + ); retCode.should.equal(1); const manifest = shouldHaveManifest(); manifest.should.deepEqual(defaultManifest); @@ -79,7 +82,7 @@ describe("cmd-remove.ts", function () { chdir: getWorkDir("test-openupm-cli"), }, }; - const retCode = await remove("pkg-not-exist", options); + const retCode = await remove(missingPackage, options); retCode.should.equal(1); const manifest = shouldHaveManifest(); manifest.should.deepEqual(defaultManifest); @@ -92,15 +95,12 @@ describe("cmd-remove.ts", function () { chdir: getWorkDir("test-openupm-cli"), }, }; - const retCode = await remove( - ["com.example.package-a", "com.example.package-b"], - options - ); + const retCode = await remove([packageA, packageB], options); retCode.should.equal(0); const manifest = shouldHaveManifest(); - shouldNotHaveDependency(manifest, "com.example.package-a"); - shouldNotHaveDependency(manifest, "com.example.package-b"); - shouldHaveRegistryWithScopes(manifest, ["com.example"]); + shouldNotHaveDependency(manifest, packageA); + shouldNotHaveDependency(manifest, packageB); + shouldHaveRegistryWithScopes(manifest, [exampleRegistryReverseDomain]); mockConsole .hasLineIncluding("out", "removed com.example.package-a") .should.be.ok(); diff --git a/test/test-cmd-search.ts b/test/test-cmd-search.ts index 4e0d97eb..2c26f1e2 100644 --- a/test/test-cmd-search.ts +++ b/test/test-cmd-search.ts @@ -1,7 +1,6 @@ import nock from "nock"; import "should"; import { search, SearchOptions } from "../src/cmd-search"; - import { exampleRegistryUrl, registerSearchResult, @@ -11,6 +10,8 @@ import { import { SearchEndpointResult } from "./types"; import { createWorkDir, getWorkDir, removeWorkDir } from "./mock-work-dir"; import { attachMockConsole, MockConsole } from "./mock-console"; +import { domainName } from "../src/types/domain-name"; +import { semanticVersion } from "../src/types/semantic-version"; describe("cmd-search.ts", function () { let mockConsole: MockConsole = null!; @@ -37,9 +38,9 @@ describe("cmd-search.ts", function () { objects: [ { package: { - name: "com.example.package-a", + name: domainName("com.example.package-a"), scope: "unscoped", - version: "1.0.0", + version: semanticVersion("1.0.0"), description: "A demo package", date: "2019-10-02T04:02:38.335Z", links: {}, diff --git a/test/test-cmd-view.ts b/test/test-cmd-view.ts index 377a0930..e8a1ae9c 100644 --- a/test/test-cmd-view.ts +++ b/test/test-cmd-view.ts @@ -1,6 +1,5 @@ import "should"; import { view, ViewOptions } from "../src/cmd-view"; -import { PkgInfo } from "../src/types/global"; import { exampleRegistryUrl, registerMissingPackage, @@ -11,6 +10,14 @@ import { } from "./mock-registry"; import { createWorkDir, getWorkDir, removeWorkDir } from "./mock-work-dir"; import { attachMockConsole, MockConsole } from "./mock-console"; +import { buildPackageInfo } from "./data-pkg-info"; +import { domainName } from "../src/types/domain-name"; +import { semanticVersion } from "../src/types/semantic-version"; +import { packageReference } from "../src/types/package-reference"; + +const packageA = domainName("com.example.package-a"); +const packageUp = domainName("com.example.package-up"); +const packageMissing = domainName("pkg-not-exist"); describe("cmd-view.ts", function () { const options: ViewOptions = { @@ -29,104 +36,78 @@ describe("cmd-view.ts", function () { describe("view", function () { let mockConsole: MockConsole = null!; - const remotePkgInfoA: PkgInfo = { - name: "com.example.package-a", - versions: { - "1.0.0": { - name: "com.example.package-a", - displayName: "Package A", - author: { - name: "batman", - }, - version: "1.0.0", - unity: "2018.4", - description: "A demo package", - keywords: [""], - category: "Unity", - dependencies: { - "com.example.package-a": "^1.0.0", - }, - gitHead: "5c141ecfac59c389090a07540f44c8ac5d07a729", - readmeFilename: "README.md", - _id: "com.example.package-a@1.0.0", - _nodeVersion: "12.13.1", - _npmVersion: "6.12.1", - dist: { - integrity: - "sha512-MAh44bur7HGyfbCXH9WKfaUNS67aRMfO0VAbLkr+jwseb1hJue/I1pKsC7PKksuBYh4oqoo9Jov1cBcvjVgjmA==", - shasum: "516957cac4249f95cafab0290335def7d9703db7", - tarball: - "https://cdn.example.com/com.example.package-a/com.example.package-a-1.0.0.tgz", - }, - contributors: [], - }, - }, - time: { - modified: "2019-11-28T18:51:58.123Z", - created: "2019-11-28T18:51:58.123Z", - "1.0.0": "2019-11-28T18:51:58.123Z", - }, - users: {}, - "dist-tags": { - latest: "1.0.0", - }, - _rev: "3-418f950115c32bd0", - _id: "com.example.package-a", - readme: "A demo package", - _attachments: {}, - }; - const remotePkgInfoUp: PkgInfo = { - name: "com.example.package-up", - versions: { - "1.0.0": { - name: "com.example.package-up", - displayName: "Package A", - author: { - name: "batman", - }, - version: "1.0.0", - unity: "2018.4", - description: "A demo package", - keywords: [""], - category: "Unity", - dependencies: { - "com.example.package-up": "^1.0.0", - }, - gitHead: "5c141ecfac59c389090a07540f44c8ac5d07a729", - readmeFilename: "README.md", - _id: "com.example.package-up@1.0.0", - _nodeVersion: "12.13.1", - _npmVersion: "6.12.1", - dist: { - integrity: - "sha512-MAh44bur7HGyfbCXH9WKfaUNS67aRMfO0VAbLkr+jwseb1hJue/I1pKsC7PKksuBYh4oqoo9Jov1cBcvjVgjmA==", - shasum: "516957cac4249f95cafab0290335def7d9703db7", - tarball: - "https://cdn.example.com/com.example.package-up/com.example.package-up-1.0.0.tgz", - }, - contributors: [], - }, - }, - time: { - modified: "2019-11-28T18:51:58.123Z", - created: "2019-11-28T18:51:58.123Z", - "1.0.0": "2019-11-28T18:51:58.123Z", - }, - users: {}, - "dist-tags": { - latest: "1.0.0", - }, - _rev: "3-418f950115c32bd0", - _id: "com.example.package-up", - readme: "A demo package", - _attachments: {}, - }; + const remotePkgInfoA = buildPackageInfo(packageA, (pkg) => + pkg + .set("time", { + modified: "2019-11-28T18:51:58.123Z", + created: "2019-11-28T18:51:58.123Z", + [semanticVersion("1.0.0")]: "2019-11-28T18:51:58.123Z", + }) + .set("_rev", "3-418f950115c32bd0") + .set("readme", "A demo package") + .addVersion("1.0.0", (version) => + version + .set("displayName", "Package A") + .set("author", { name: "batman" }) + .set("unity", "2018.4") + .set("description", "A demo package") + .set("keywords", [""]) + .set("category", "Unity") + .set("gitHead", "5c141ecfac59c389090a07540f44c8ac5d07a729") + .set("readmeFilename", "README.md") + .set("_nodeVersion", "12.13.1") + .set("_npmVersion", "6.12.1") + .set("dist", { + integrity: + "sha512-MAh44bur7HGyfbCXH9WKfaUNS67aRMfO0VAbLkr+jwseb1hJue/I1pKsC7PKksuBYh4oqoo9Jov1cBcvjVgjmA==", + shasum: "516957cac4249f95cafab0290335def7d9703db7", + tarball: + "https://cdn.example.com/com.example.package-a/com.example.package-a-1.0.0.tgz", + }) + .addDependency(packageA, "1.0.0") + ) + ); + + const remotePkgInfoUp = buildPackageInfo(packageUp, (pkg) => + pkg + .set("time", { + modified: "2019-11-28T18:51:58.123Z", + created: "2019-11-28T18:51:58.123Z", + [semanticVersion("1.0.0")]: "2019-11-28T18:51:58.123Z", + }) + .set("_rev", "3-418f950115c32bd0") + .set("readme", "A demo package") + .addVersion("1.0.0", (version) => + version + .set("displayName", "Package A") + .set("author", { + name: "batman", + }) + .set("unity", "2018.4") + .set("description", "A demo package") + .set("keywords", [""]) + .set("category", "Unity") + .addDependency(packageUp, "1.0.0") + .set("gitHead", "5c141ecfac59c389090a07540f44c8ac5d07a729") + .set("readmeFilename", "README.md") + .set("_nodeVersion", "12.13.1") + .set("_npmVersion", "6.12.1") + .set("dist", { + integrity: + "sha512-MAh44bur7HGyfbCXH9WKfaUNS67aRMfO0VAbLkr+jwseb1hJue/I1pKsC7PKksuBYh4oqoo9Jov1cBcvjVgjmA==", + shasum: "516957cac4249f95cafab0290335def7d9703db7", + tarball: + "https://cdn.example.com/com.example.package-up/com.example.package-up-1.0.0.tgz", + }) + ) + ); + beforeEach(function () { removeWorkDir("test-openupm-cli"); createWorkDir("test-openupm-cli", { manifest: true }); startMockRegistry(); registerRemotePkg(remotePkgInfoA); - registerMissingPackage("pkg-not-exist"); + registerMissingPackage(packageMissing); registerRemoteUpstreamPkg(remotePkgInfoUp); mockConsole = attachMockConsole(); }); @@ -136,31 +117,34 @@ describe("cmd-view.ts", function () { mockConsole.detach(); }); it("view pkg", async function () { - const retCode = await view("com.example.package-a", options); + const retCode = await view(packageA, options); retCode.should.equal(0); mockConsole .hasLineIncluding("out", "com.example.package-a@1.0.0") .should.be.ok(); }); it("view pkg@1.0.0", async function () { - const retCode = await view("com.example.package-a@1.0.0", options); + const retCode = await view( + packageReference(packageA, semanticVersion("1.0.0")), + options + ); retCode.should.equal(1); mockConsole.hasLineIncluding("out", "please replace").should.be.ok(); }); it("view pkg-not-exist", async function () { - const retCode = await view("pkg-not-exist", options); + const retCode = await view(packageMissing, options); retCode.should.equal(1); mockConsole.hasLineIncluding("out", "package not found").should.be.ok(); }); it("view pkg from upstream", async function () { - const retCode = await view("com.example.package-up", upstreamOptions); + const retCode = await view(packageUp, upstreamOptions); retCode.should.equal(0); mockConsole .hasLineIncluding("out", "com.example.package-up@1.0.0") .should.be.ok(); }); it("view pkg-not-exist from upstream", async function () { - const retCode = await view("pkg-not-exist", upstreamOptions); + const retCode = await view(packageMissing, upstreamOptions); retCode.should.equal(1); mockConsole.hasLineIncluding("out", "package not found").should.be.ok(); }); diff --git a/test/test-domain-name.ts b/test/test-domain-name.ts new file mode 100644 index 00000000..c9f9c779 --- /dev/null +++ b/test/test-domain-name.ts @@ -0,0 +1,62 @@ +import { describe } from "mocha"; +import { + domainName, + isDomainName, + isInternalPackage, + namespaceFor, +} from "../src/types/domain-name"; +import should from "should"; + +describe("domain-name", function () { + describe("namespace", function () { + [ + ["unity.com", "com.unity"], + ["my-school.ac.at", "at.ac.my-school"], + ["openupm.com", "com.openupm"], + ["registry.npmjs.org", "org.npmjs"], + ].forEach(([hostName, expected]) => + it(`"${hostName}" should become "${expected}"`, function () { + const actual = namespaceFor(hostName); + should(actual).be.equal(expected); + }) + ); + }); + describe("validation", function () { + [ + "com", + "com.unity", + "com.openupm", + "at.ac.my-school", + "dev.comradevanti123", + ].forEach((s) => + it(`"${s}" should be domain-name`, () => + should(isDomainName(s)).be.true()) + ); + [ + "", + " ", + // Invalid characters + "com.x💀x", + // No double hyphens + "com.my--school", + // No leading hyphens + "com.-unity", + // No trailing hyphens + "com.unity-", + ].forEach((s) => + it(`"${s}" should not be domain-name`, () => + should(isDomainName(s)).be.false()) + ); + }); + describe("internal package", function () { + it("test com.otherorg.software", function () { + isInternalPackage(domainName("com.otherorg.software")).should.not.be.ok(); + }); + it("test com.unity.ugui", function () { + isInternalPackage(domainName("com.unity.ugui")).should.be.ok(); + }); + it("test com.unity.modules.tilemap", function () { + isInternalPackage(domainName("com.unity.modules.tilemap")).should.be.ok(); + }); + }); +}); diff --git a/test/test-ip-address.ts b/test/test-ip-address.ts new file mode 100644 index 00000000..49e28d4b --- /dev/null +++ b/test/test-ip-address.ts @@ -0,0 +1,28 @@ +import { describe } from "mocha"; +import { isIpAddress } from "../src/types/ip-address"; +import should from "should"; + +describe("ip-address", function () { + describe("validate", function () { + [ + "10.20.30.40", + "64.233.160.0", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + ].forEach((s) => + it(`"${s}" should be ip-address`, () => should(isIpAddress(s)).be.true()) + ); + + [ + "", + " ", + "hello", + // Missing 4th segment + "64.233.160", + // Deleted some colons + "2001:0db8:85a30000:0000:8a2e0370:7334", + ].forEach((s) => + it(`"${s}" should not be ip-address`, () => + should(isIpAddress(s)).be.false()) + ); + }); +}); diff --git a/test/test-manifest.ts b/test/test-manifest.ts index 075527b0..8ab18eb4 100644 --- a/test/test-manifest.ts +++ b/test/test-manifest.ts @@ -11,6 +11,8 @@ import { shouldHaveNoManifest, shouldNotHaveAnyDependencies, } from "./manifest-assertions"; +import { domainName } from "../src/types/domain-name"; +import { semanticVersion } from "../src/types/semantic-version"; describe("manifest", function () { let mockConsole: MockConsole = null!; @@ -73,7 +75,7 @@ describe("manifest", function () { ).should.be.ok(); const manifest = shouldHaveManifest(); shouldNotHaveAnyDependencies(manifest); - manifest.dependencies["some-pack"] = "1.0.0"; + manifest.dependencies[domainName("some-pack")] = semanticVersion("1.0.0"); saveManifest(manifest).should.be.ok(); const manifest2 = shouldHaveManifest(); manifest2.should.be.deepEqual(manifest); diff --git a/test/test-package-id.ts b/test/test-package-id.ts new file mode 100644 index 00000000..a0c3aa3a --- /dev/null +++ b/test/test-package-id.ts @@ -0,0 +1,24 @@ +import { describe } from "mocha"; +import should from "should"; +import { isPackageId } from "../src/types/package-id"; + +describe("package-id", function () { + describe("validate", function () { + ["com.my-package@1.2.3"].forEach((s) => + it(`"${s}" should be package-id`, () => should(isPackageId(s)).be.true()) + ); + + [ + "", + " ", + // Missing version + "com.my-package", + // Incomplete version + "com.my-package@1", + "com.my-package@1.2", + ].forEach((s) => + it(`"${s}" should not be package-id`, () => + should(isPackageId(s)).be.false()) + ); + }); +}); diff --git a/test/test-package-reference.ts b/test/test-package-reference.ts new file mode 100644 index 00000000..dcf30671 --- /dev/null +++ b/test/test-package-reference.ts @@ -0,0 +1,49 @@ +import { describe } from "mocha"; +import should from "should"; +import { + isPackageReference, + packageReference, + splitPackageReference, +} from "../src/types/package-reference"; + +describe("package-reference", function () { + describe("validation", function () { + [ + "com.abc.my-package", + "com.abc.my-package@1.2.3", + "com.abc.my-package@file://./my-package", + "com.abc.my-package@latest", + ].forEach((input) => + it(`"${input}" should be package-reference`, function () { + should(isPackageReference(input)).be.true(); + }) + ); + [ + // Not valid domain name + "-hello", + ].forEach((input) => + it(`"${input}" should not be package-reference`, function () { + should(isPackageReference(input)).not.be.true(); + }) + ); + }); + + describe("split", function () { + function shouldSplitCorrectly(name: string, version?: string) { + const [actualName, actualVersion] = splitPackageReference( + packageReference(name, version) + ); + should(actualName).be.equal(name); + should(actualVersion).be.equal(version); + } + + it("should split package without version", () => + shouldSplitCorrectly("com.abc.my-package")); + it("should split package with semantic version", () => + shouldSplitCorrectly("com.abc.my-package", "1.0.0")); + it("should split package with file-url", () => + shouldSplitCorrectly("com.abc.my-package", "file://./my-package")); + it("should split package with latest-tag", () => + shouldSplitCorrectly("com.abc.my-package", "latest")); + }); +}); diff --git a/test/test-package-url.ts b/test/test-package-url.ts new file mode 100644 index 00000000..70f0ed38 --- /dev/null +++ b/test/test-package-url.ts @@ -0,0 +1,23 @@ +import { describe } from "mocha"; +import should from "should"; +import { isPackageUrl } from "../src/types/package-url"; + +describe("package-url", function () { + describe("validation", function () { + [ + "https://github.com/yo/com.base.package-a", + "git@github.com:yo/com.base.package-a", + "file../yo/com.base.package-a", + ].forEach((url) => + it(`"${url}" is a package-url`, function () { + should(isPackageUrl(url)).be.true(); + }) + ); + + ["", "com.base.package.a"].forEach((url) => + it(`"${url}" is not a package-url`, function () { + should(isPackageUrl(url)).not.be.true(); + }) + ); + }); +}); diff --git a/test/test-pgk-info.ts b/test/test-pgk-info.ts index b78d3331..1e4c5ac0 100644 --- a/test/test-pgk-info.ts +++ b/test/test-pgk-info.ts @@ -2,11 +2,14 @@ import { tryGetLatestVersion } from "../src/utils/pkg-info"; import "should"; import { describe } from "mocha"; import should from "should"; +import { semanticVersion } from "../src/types/semantic-version"; describe("pkg-info", function () { describe("tryGetLatestVersion", function () { it("from dist-tags", async function () { - const version = tryGetLatestVersion({ "dist-tags": { latest: "1.0.0" } }); + const version = tryGetLatestVersion({ + "dist-tags": { latest: semanticVersion("1.0.0") }, + }); should(version).equal("1.0.0"); }); }); diff --git a/test/test-pkg-name.ts b/test/test-pkg-name.ts deleted file mode 100644 index 592efd26..00000000 --- a/test/test-pkg-name.ts +++ /dev/null @@ -1,55 +0,0 @@ -import "assert"; -import "should"; -import { isInternalPackage, splitPkgName } from "../src/utils/pkg-name"; - -describe("pkg-name.ts", function () { - describe("splitPkgName", function () { - it("pkg@version", function () { - splitPkgName("pkg@1.0.0").should.deepEqual({ - name: "pkg", - version: "1.0.0", - }); - }); - it("pkg@latest", function () { - splitPkgName("pkg@latest").should.deepEqual({ - name: "pkg", - version: "latest", - }); - }); - it("pkg", function () { - splitPkgName("pkg").should.deepEqual({ - name: "pkg", - version: undefined, - }); - }); - it("pkg@file", function () { - splitPkgName("pkg@file:../pkg").should.deepEqual({ - name: "pkg", - version: "file:../pkg", - }); - }); - it("pkg@http", function () { - splitPkgName("pkg@https://github.com/owner/pkg").should.deepEqual({ - name: "pkg", - version: "https://github.com/owner/pkg", - }); - }); - it("pkg@git", function () { - splitPkgName("pkg@git@github.com:owner/pkg.git").should.deepEqual({ - name: "pkg", - version: "git@github.com:owner/pkg.git", - }); - }); - }); - describe("isInternalPackage", function () { - it("test com.otherorg.software", function () { - isInternalPackage("com.otherorg.software").should.not.be.ok(); - }); - it("test com.unity.ugui", function () { - isInternalPackage("com.unity.ugui").should.be.ok(); - }); - it("test com.unity.modules.tilemap", function () { - isInternalPackage("com.unity.modules.tilemap").should.be.ok(); - }); - }); -}); diff --git a/test/test-registry-client.ts b/test/test-registry-client.ts index 668253d4..da03aaaa 100644 --- a/test/test-registry-client.ts +++ b/test/test-registry-client.ts @@ -2,7 +2,6 @@ import "assert"; import "should"; import { parseEnv } from "../src/utils/env"; import { fetchPackageInfo } from "../src/registry-client"; -import { PkgInfo } from "../src/types/global"; import { exampleRegistryUrl, registerMissingPackage, @@ -11,6 +10,10 @@ import { stopMockRegistry, } from "./mock-registry"; import should from "should"; +import { buildPackageInfo } from "./data-pkg-info"; +import { domainName } from "../src/types/domain-name"; + +const packageA = domainName("package-a"); describe("registry-client", function () { describe("fetchPackageInfo", function () { @@ -27,13 +30,9 @@ describe("registry-client", function () { { checkPath: false } ) ).should.be.ok(); - const pkgInfoRemote: PkgInfo = { - name: "package-a", - versions: {}, - time: {}, - }; + const pkgInfoRemote = buildPackageInfo(packageA); registerRemotePkg(pkgInfoRemote); - const info = await fetchPackageInfo("package-a"); + const info = await fetchPackageInfo(packageA); should(info).deepEqual(pkgInfoRemote); }); it("404", async function () { @@ -44,8 +43,8 @@ describe("registry-client", function () { ) ).should.be.ok(); - registerMissingPackage("package-a"); - const info = await fetchPackageInfo("package-a"); + registerMissingPackage(packageA); + const info = await fetchPackageInfo(packageA); (info === undefined).should.be.ok(); }); }); diff --git a/test/test-registry-url.ts b/test/test-registry-url.ts new file mode 100644 index 00000000..498d5e88 --- /dev/null +++ b/test/test-registry-url.ts @@ -0,0 +1,45 @@ +import { describe } from "mocha"; +import { + coerceRegistryUrl, + isRegistryUrl, + registryUrl, + removeTrailingSlash, +} from "../src/types/registry-url"; +import should from "should"; + +describe("registry-url", function () { + describe("validation", function () { + ["http://registry.npmjs.org/", "https://registry.npmjs.org/"].forEach( + (input) => + it(`"${input}" should be registry-url`, function () { + should(isRegistryUrl(input)).be.ok(); + }) + ); + [ + // Missing protocol + "registry.npmjs.org/", + ].forEach((input) => + it(`"${input}" should not be registry-url`, function () { + should(isRegistryUrl(input)).not.be.ok(); + }) + ); + }); + describe("remove trailing slash", function () { + it("should remove trailing slash if it is exists", () => + should(removeTrailingSlash(registryUrl("http://test.com/"))).be.equal( + "http://test.com" + )); + it("should do nothing if there is no trailing slash", () => + should(removeTrailingSlash(registryUrl("http://test.com"))).be.equal( + "http://test.com" + )); + }); + describe("coerce", function () { + it("should coerce urls without protocol", () => + should(coerceRegistryUrl("test.com")).be.equal("http://test.com")); + it("should remove trailing slash", () => + should(coerceRegistryUrl("http://test.com/")).be.equal( + "http://test.com" + )); + }); +}); diff --git a/test/test-semantic-version.ts b/test/test-semantic-version.ts new file mode 100644 index 00000000..7088b111 --- /dev/null +++ b/test/test-semantic-version.ts @@ -0,0 +1,18 @@ +import { describe } from "mocha"; +import should from "should"; +import { isSemanticVersion } from "../src/types/semantic-version"; + +describe("semantic-version", function () { + describe("validate", function () { + ["1.2.3", "1.2.3-alpha"].forEach((input) => + it(`"${input}" is a semantic version`, function () { + should(isSemanticVersion(input)).be.true(); + }) + ); + ["", " ", "wow", "1", "1.2"].forEach((input) => + it(`"${input}" is not a semantic version`, function () { + should(isSemanticVersion(input)).not.be.true(); + }) + ); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 00000000..6392488d --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,111 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + // "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": ["ES2020"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + // "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": false, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": false, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./lib", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + // "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + // "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + //"strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + // "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["./**/*"] +} diff --git a/test/types.ts b/test/types.ts index ee0ec0d5..e2863c7c 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,15 +1,17 @@ -import { Contact, PkgVersion, ReverseDomainName } from "../src/types/global"; +import { Contact } from "../src/types/global"; +import { DomainName } from "../src/types/domain-name"; +import { SemanticVersion } from "../src/types/semantic-version"; type Maintainer = { username: string; email: string }; export type SearchEndpointResult = { objects: Array<{ package: { - name: ReverseDomainName; + name: DomainName; description?: string; date: string; scope: "unscoped"; - version: PkgVersion; + version: SemanticVersion; links: Record; author: Contact; publisher: Maintainer; diff --git a/tsconfig.json b/tsconfig.json index 9293beee..d21ff3b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,15 +25,14 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", - /* Specify what module code is generated. */ + "module": "commonjs", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ "typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */ - "lib/types", "node_modules/@types" + "./src/types", "./node_modules/@types" ], // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -80,15 +79,12 @@ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, - /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, - /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ - "strict": true, - /* Enable all strict type-checking options. */ + "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -110,8 +106,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true - /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": [ "./src/**/*"