From 5d9a0d57bbc98625b5ffeda193cd964e19bf9080 Mon Sep 17 00:00:00 2001 From: Ramon Brullo Date: Wed, 1 Nov 2023 17:41:13 +0100 Subject: [PATCH] Split core module (#58) * refactor: make property required Make version property required in dependency. This makes sense as Unity dependencies always give their dependency versions explicitly. The only reason this was set to optional was because it was not immediately initialized at one point. * refactor: extract utility function There are many points in the code where a package name is combined with a version using `${name}@${version}` or similar. Extracted this logic to a utility function. This process also showed that a potentially undefined version was used in some prints. Changed logic to only print version where it is defined. * refactor: move and refactor function Extract function for getting latest version of pkg-info to own module, so it can be grouped with other pkg-info related functions. Also change name to tryGetLatestVersion to indicate that this function may return undefined. Also refactor the function a bit for readability a * chore: reformat file Just add a line break for consistency * chore: reformat files * refactor: move function Move isInternalPackage function into package-name module to be closer to other related functions * fix: incorrect type This function assumes the name includes no version * refactor: move function Move manifest related functions from core to own module * chore: clean up imports and reformat * refactor: move functions Move upm-config related functions to own module * refactor: extract module Move env related code into own module * refactor: rename type This type does not refer to a generic semantic version but specifically to a unity-editor version * refactor: extract module Extract editor-version related logic to own module * refactor: move function Move function to get search options into cmd-search since that is the only place where it is used * refactor: move functions Move functions for interacting with npm registry into client module. * refactor: rename module --- src/client.ts | 85 ------ src/cmd-add.ts | 37 +-- src/cmd-deps.ts | 11 +- src/cmd-login.ts | 8 +- src/cmd-remove.ts | 9 +- src/cmd-search.ts | 17 +- src/cmd-view.ts | 10 +- src/core.ts | 524 ----------------------------------- src/registry-client.ts | 247 +++++++++++++++++ src/types/global.ts | 4 +- src/utils/editor-version.ts | 90 ++++++ src/utils/env.ts | 149 ++++++++++ src/utils/manifest.ts | 43 +++ src/utils/pkg-info.ts | 18 ++ src/utils/pkg-name.ts | 25 ++ src/utils/pkg-version.ts | 1 + src/utils/upm-config.ts | 75 +++++ test/test-cmd-add.ts | 2 +- test/test-cmd-remove.ts | 2 +- test/test-core.ts | 453 ------------------------------ test/test-editor-version.ts | 125 +++++++++ test/test-env.ts | 189 +++++++++++++ test/test-manifest.ts | 92 ++++++ test/test-pgk-info.ts | 14 + test/test-pkg-name.ts | 13 +- test/test-registry-client.ts | 46 +++ 26 files changed, 1185 insertions(+), 1104 deletions(-) delete mode 100644 src/client.ts delete mode 100644 src/core.ts create mode 100644 src/registry-client.ts create mode 100644 src/utils/editor-version.ts create mode 100644 src/utils/env.ts create mode 100644 src/utils/manifest.ts create mode 100644 src/utils/pkg-info.ts create mode 100644 src/utils/upm-config.ts delete mode 100644 test/test-core.ts create mode 100644 test/test-editor-version.ts create mode 100644 test/test-env.ts create mode 100644 test/test-manifest.ts create mode 100644 test/test-pgk-info.ts create mode 100644 test/test-registry-client.ts diff --git a/src/client.ts b/src/client.ts deleted file mode 100644 index f8395b13..00000000 --- a/src/client.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { promisify } from "util"; -import RegClient, { - AddUserParams, - AddUserResponse, - ClientCallback, - GetParams, -} from "another-npm-registry-client"; -import log from "./logger"; -import request from "request"; -import { AssertionError } from "assert"; -import { PkgInfo } from "./types/global"; - -export type NpmClient = { - rawClient: RegClient; - /** - * @throws {NpmClientError} - */ - get(uri: string, options: GetParams): Promise; - /** - * @throws {NpmClientError} - */ - adduser(uri: string, options: AddUserParams): Promise; -}; - -export class NpmClientError extends Error { - cause: Error; - response: request.Response; - - constructor(cause: Error, response: request.Response) { - super( - cause?.message ?? - "An error occurred while interacting with an Npm registry" - ); - this.name = "NpmClientError"; - this.cause = cause; - this.response = response; - } -} - -export function assertIsNpmClientError( - x: unknown -): asserts x is NpmClientError { - if (!(x instanceof NpmClientError)) - throw new AssertionError({ - message: "Given object was not an NpmClientError", - actual: x, - }); -} - -/** - * Normalizes a RegClient function. Specifically it merges it's multiple - * callback arguments into a single NormalizedError object. This function - * also takes care of binding and promisifying. - */ -function normalizeClientFunction( - client: RegClient, - fn: (uri: string, params: TParam, cb: ClientCallback) => void -): (uri: string, params: TParam) => Promise { - const bound = fn.bind(client); - const withNormalizedError = ( - uri: string, - params: TParam, - cb: (error: NpmClientError | null, data: TData) => void - ) => { - return bound(uri, params, (error, data, raw, res) => { - cb(error !== null ? new NpmClientError(error, res) : null, data); - }); - }; - return promisify(withNormalizedError); -} - -/** - * Return npm client - */ -export const getNpmClient = (): NpmClient => { - // create client - const client = new RegClient({ log }); - return { - // The instance of raw npm client - rawClient: client, - // Promisified methods - get: normalizeClientFunction(client, client.get), - adduser: normalizeClientFunction(client, client.adduser), - }; -}; diff --git a/src/cmd-add.ts b/src/cmd-add.ts index 4b9934f1..01284912 100644 --- a/src/cmd-add.ts +++ b/src/cmd-add.ts @@ -1,19 +1,17 @@ import log from "./logger"; import url from "url"; -import { - compareEditorVersion, - env, - fetchPackageDependencies, - fetchPackageInfo, - getLatestVersion, - loadManifest, - parseEditorVersion, - parseEnv, - saveManifest, -} from "./core"; import { isUrlVersion } from "./utils/pkg-version"; -import { splitPkgName } from "./utils/pkg-name"; +import { atVersion, splitPkgName } from "./utils/pkg-name"; import { GlobalOptions, PkgName, 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"; export type AddOptions = { test?: boolean; @@ -86,7 +84,7 @@ const _add = async function ({ // verify version const versions = Object.keys(pkgInfo.versions); // eslint-disable-next-line require-atomic-updates - if (!version || version == "latest") version = getLatestVersion(pkgInfo); + if (!version || version == "latest") version = tryGetLatestVersion(pkgInfo); if (versions.filter((x) => x == version).length <= 0) { log.warn( "404", @@ -106,8 +104,8 @@ const _add = async function ({ ? versionInfo.unity + "." + versionInfo.unityRelease : versionInfo.unity; if (env.editorVersion) { - const editorVersionResult = parseEditorVersion(env.editorVersion); - const requiredEditorVersionResult = parseEditorVersion( + const editorVersionResult = tryParseEditorVersion(env.editorVersion); + const requiredEditorVersionResult = tryParseEditorVersion( requiredEditorVersion ); if (!editorVersionResult) { @@ -165,7 +163,10 @@ const _add = async function ({ if (!depObj.resolved) log.notice( "suggest", - `to install ${depObj.name}@${depObj.version} or a replaceable version manually` + `to install ${atVersion( + depObj.name, + depObj.version + )} or a replaceable version manually` ); } }); @@ -185,7 +186,7 @@ const _add = async function ({ manifest.dependencies[name] = version; if (!oldVersion) { // Log the added package - log.notice("manifest", `added ${name}@${version}`); + log.notice("manifest", `added ${atVersion(name, version)}`); dirty = true; } else if (oldVersion != version) { // Log the modified package version @@ -193,7 +194,7 @@ const _add = async function ({ dirty = true; } else { // Log the existed package - log.notice("manifest", `existed ${name}@${version}`); + log.notice("manifest", `existed ${atVersion(name, version)}`); } if (!isUpstreamPackage) { // add to scopedRegistries diff --git a/src/cmd-deps.ts b/src/cmd-deps.ts index f5672ac2..ee63424b 100644 --- a/src/cmd-deps.ts +++ b/src/cmd-deps.ts @@ -1,7 +1,8 @@ import log from "./logger"; -import { fetchPackageDependencies, parseEnv } from "./core"; -import { splitPkgName } from "./utils/pkg-name"; +import { atVersion, splitPkgName } from "./utils/pkg-name"; import { GlobalOptions, PkgName, PkgVersion } from "./types/global"; +import { parseEnv } from "./utils/env"; +import { fetchPackageDependencies } from "./registry-client"; export type DepsOptions = { deep?: boolean; @@ -36,13 +37,15 @@ const _deps = async function ({ }); depsValid .filter((x) => !x.self) - .forEach((x) => log.notice("dependency", `${x.name}@${x.version}`)); + .forEach((x) => + log.notice("dependency", `${atVersion(x.name, x.version)}`) + ); depsInvalid .filter((x) => !x.self) .forEach((x) => { let reason = "unknown"; if (x.reason == "package404") reason = "missing dependency"; else if (x.reason == "version404") reason = "missing dependency version"; - log.warn(reason, `${x.name}@${x.version}`); + log.warn(reason, atVersion(x.name, x.version)); }); }; diff --git a/src/cmd-login.ts b/src/cmd-login.ts index 1c34304d..e5198e47 100644 --- a/src/cmd-login.ts +++ b/src/cmd-login.ts @@ -2,17 +2,17 @@ import fs from "fs"; import path from "path"; import _ from "lodash"; import promptly from "promptly"; -import { assertIsNpmClientError, getNpmClient } from "./client"; +import { assertIsNpmClientError, getNpmClient } from "./registry-client"; import log from "./logger"; +import { GlobalOptions, Registry } from "./types/global"; import { getUpmConfigDir, loadUpmConfig, - parseEnv, saveUpmConfig, -} from "./core"; -import { GlobalOptions, Registry } from "./types/global"; +} from "./utils/upm-config"; +import { parseEnv } from "./utils/env"; export type LoginOptions = { username?: string; diff --git a/src/cmd-remove.ts b/src/cmd-remove.ts index 32f7832a..b051367d 100644 --- a/src/cmd-remove.ts +++ b/src/cmd-remove.ts @@ -1,7 +1,8 @@ import log from "./logger"; -import { env, loadManifest, parseEnv, saveManifest } from "./core"; -import { splitPkgName } from "./utils/pkg-name"; +import { atVersion, splitPkgName } from "./utils/pkg-name"; import { GlobalOptions, PkgName, ScopedRegistry } from "./types/global"; +import { loadManifest, saveManifest } from "./utils/manifest"; +import { env, parseEnv } from "./utils/env"; export type RemoveOptions = { _global: GlobalOptions; @@ -36,7 +37,7 @@ const _remove = async function (pkg: PkgName) { const name = split.name; let version = split.version; if (version) { - log.warn("", `please replace '${name}@${version}' with '${name}'`); + log.warn("", `please replace '${atVersion(name, version)}' with '${name}'`); return { code: 1, dirty }; } // load manifest @@ -48,7 +49,7 @@ const _remove = async function (pkg: PkgName) { if (manifest.dependencies) { version = manifest.dependencies[name]; if (version) { - log.notice("manifest", `removed ${name}@${version}`); + log.notice("manifest", `removed ${atVersion(name, version)}`); delete manifest.dependencies[name]; dirty = true; } else pkgsNotFound.push(pkg); diff --git a/src/cmd-search.ts b/src/cmd-search.ts index 01264e21..715fb198 100644 --- a/src/cmd-search.ts +++ b/src/cmd-search.ts @@ -1,8 +1,7 @@ -import npmSearch from "libnpmsearch"; +import npmSearch, { Options } from "libnpmsearch"; import npmFetch from "npm-registry-fetch"; import Table from "cli-table"; import log from "./logger"; -import { env, getLatestVersion, getNpmFetchOptions, parseEnv } from "./core"; import { is404Error, isHttpError } from "./utils/error-type-guards"; import * as os from "os"; import assert from "assert"; @@ -13,6 +12,8 @@ import { PkgVersion, Registry, } from "./types/global"; +import { tryGetLatestVersion } from "./utils/pkg-info"; +import { env, parseEnv } from "./utils/env"; type DateString = string; @@ -21,6 +22,16 @@ type TableRow = [PkgName, PkgVersion, DateString, ""]; export type SearchOptions = { _global: GlobalOptions; }; +// Get npm fetch options +const getNpmFetchOptions = function (): Options { + const opts: Options = { + log, + registry: env.registry, + }; + const auth = env.auth[env.registry]; + if (auth) Object.assign(opts, auth); + return opts; +}; const searchEndpoint = async function ( keyword: string, @@ -86,7 +97,7 @@ const getTable = function () { const getTableRow = function (pkg: PkgInfo): TableRow { const name = pkg.name; - const version = getLatestVersion(pkg); + const version = tryGetLatestVersion(pkg); let date = ""; if (pkg.time && pkg.time.modified) date = pkg.time.modified.split("T")[0]; if (pkg.date) { diff --git a/src/cmd-view.ts b/src/cmd-view.ts index 0b760c39..98664bd2 100644 --- a/src/cmd-view.ts +++ b/src/cmd-view.ts @@ -1,9 +1,11 @@ import chalk from "chalk"; import log from "./logger"; -import { env, fetchPackageInfo, getLatestVersion, parseEnv } from "./core"; import assert from "assert"; -import { splitPkgName } from "./utils/pkg-name"; +import { atVersion, splitPkgName } from "./utils/pkg-name"; import { GlobalOptions, PkgInfo, PkgName } from "./types/global"; +import { tryGetLatestVersion } from "./utils/pkg-info"; +import { env, parseEnv } from "./utils/env"; +import { fetchPackageInfo } from "./registry-client"; export type ViewOptions = { _global: GlobalOptions; @@ -16,7 +18,7 @@ export const view = async function (pkg: PkgName, options: ViewOptions) { // parse name const { name, version } = splitPkgName(pkg); if (version) { - log.warn("", `please replace '${name}@${version}' with '${name}'`); + log.warn("", `please replace '${atVersion(name, version)}' with '${name}'`); return 1; } // verify name @@ -34,7 +36,7 @@ export const view = async function (pkg: PkgName, options: ViewOptions) { const printInfo = function (pkg: PkgInfo) { const versionCount = Object.keys(pkg.versions).length; - const ver = getLatestVersion(pkg); + const ver = tryGetLatestVersion(pkg); assert(ver !== undefined); const verInfo = pkg.versions[ver]; const license = verInfo.license || "proprietary or unlicensed"; diff --git a/src/core.ts b/src/core.ts deleted file mode 100644 index b43ba6b0..00000000 --- a/src/core.ts +++ /dev/null @@ -1,524 +0,0 @@ -import fs from "fs"; -import path from "path"; -import url from "url"; -import _ from "lodash"; -import chalk from "chalk"; -import mkdirp from "mkdirp"; -import net from "node:net"; -import isWsl from "is-wsl"; -import TOML from "@iarna/toml"; -import yaml from "yaml"; -import execute from "./utils/process"; -import { getNpmClient } from "./client"; -import log from "./logger"; -import { assertIsError } from "./utils/error-type-guards"; -import search from "libnpmsearch"; -import assert from "assert"; -import { - Dependency, - Env, - GlobalOptions, - NameVersionPair, - PkgInfo, - PkgManifest, - PkgName, - PkgVersion, - Registry, - SemanticVersion, - UPMConfig, -} from "./types/global"; - -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.cwd = ""; - env.manifestPath = ""; - env.upstream = true; - env.color = true; - env.upstreamRegistry = "https://packages.unity.com"; - env.systemUser = false; - env.wsl = false; - env.editorVersion = null; - env.region = "us"; - // the npmAuth field of .upmconfig.toml - env.npmAuth = {}; - // the dict of auth param for npm registry API - env.auth = {}; - // log level - log.level = options._global.verbose ? "verbose" : "notice"; - // color - if (options._global.color === false) env.color = false; - if (process.env.NODE_ENV == "test") env.color = false; - if (!env.color) { - chalk.level = 0; - log.disableColor(); - } - // upstream - 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.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; - // 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("."); - } - // auth - if (options._global.systemUser) env.systemUser = true; - if (options._global.wsl) env.wsl = true; - 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 ("token" in regAuth) { - env.auth[reg] = { - token: regAuth.token, - alwaysAuth: regAuth.alwaysAuth || false, - }; - } else if ("_auth" in regAuth) { - const buf = Buffer.from(regAuth._auth, "base64"); - const text = buf.toString("utf-8"); - const [username, password] = text.split(":", 2); - env.auth[reg] = { - username, - password: Buffer.from(password).toString("base64"), - email: regAuth.email, - alwaysAuth: regAuth.alwaysAuth || false, - }; - } else { - log.warn( - "env.auth", - `failed to parse auth info for ${reg} in .upmconfig.toml: missing token or _auth fields` - ); - log.warn("env.auth", regAuth); - } - } - } - } - // log.verbose("env.npmAuth", env.npmAuth); - // log.verbose("env.auth", env.auth); - // return if no need to check path - if (!checkPath) return true; - // cwd - if (options._global.chdir) { - const cwd = path.resolve(options._global.chdir); - if (!fs.existsSync(cwd)) { - log.error("env", `can not resolve path ${cwd}`); - return false; - } - env.cwd = cwd; - } else env.cwd = process.cwd(); - // manifest path - const manifestPath = path.join(env.cwd, "Packages/manifest.json"); - if (!fs.existsSync(manifestPath)) { - log.error( - "manifest", - `can not locate manifest.json at path ${manifestPath}` - ); - return false; - } else env.manifestPath = manifestPath; - // editor version - const projectVersionPath = path.join( - env.cwd, - "ProjectSettings/ProjectVersion.txt" - ); - if (!fs.existsSync(projectVersionPath)) { - log.warn( - "ProjectVersion", - `can not locate ProjectVersion.text at path ${projectVersionPath}` - ); - } else { - const projectVersionData = fs.readFileSync(projectVersionPath, "utf8"); - const projectVersionContent = yaml.parse(projectVersionData); - env.editorVersion = projectVersionContent.m_EditorVersion; - } - // return - return true; -}; - -// Get npm fetch options -export const getNpmFetchOptions = function (): search.Options { - const opts: search.Options = { - log, - registry: env.registry, - }; - const auth = env.auth[env.registry]; - if (auth) Object.assign(opts, auth); - return opts; -}; - -// Fetch package info json from registry -export const fetchPackageInfo = async function ( - name: PkgName, - registry?: Registry -): Promise { - if (!registry) registry = env.registry; - const pkgPath = `${registry}/${name}`; - const client = getNpmClient(); - try { - return await client.get(pkgPath, { auth: env.auth[registry] || undefined }); - // eslint-disable-next-line no-empty - } catch (err) {} -}; - -/* Fetch package [valid dependencies, invalid dependencies] with a structure of - [ - { - name, - version, - upstream, // whether belongs to upstream registry - self, // whether is the source package - internal, // whether is an internal package - reason // invalid reason of "version404", "package404" - }, ... - ] - */ -export const fetchPackageDependencies = async function ({ - name, - version, - deep, -}: { - name: PkgName; - version: PkgVersion | undefined; - deep?: boolean; -}): Promise<[Dependency[], Dependency[]]> { - log.verbose("dependency", `fetch: ${name}@${version} deep=${deep}`); - // a list of pending dependency {name, version} - const pendingList: NameVersionPair[] = [{ name, version }]; - // a list of processed dependency {name, version} - const processedList = []; - // a list of dependency entry exists on the registry - const depsValid = []; - // a list of dependency entry doesn't exist on the registry - const depsInvalid = []; - // cached dict: {pkg-name: pkgInfo} - const cachedPacakgeInfoDict: Record< - PkgVersion, - { pkgInfo: PkgInfo; upstream: boolean } - > = {}; - while (pendingList.length > 0) { - // NOTE: Guaranteed defined because of while loop logic - const entry = pendingList.shift() as NameVersionPair; - if (processedList.find((x) => _.isEqual(x, entry)) === undefined) { - // add entry to processed list - processedList.push(entry); - // create valid depedenency structure - const depObj: Dependency = { - ...entry, - internal: isInternalPackage(entry.name), - upstream: false, - self: entry.name == name, - reason: null, - }; - if (!depObj.internal) { - // try fetching package info from cache - const getResult = _.get(cachedPacakgeInfoDict, entry.name, { - pkgInfo: null, - upstream: false, - }); - let pkgInfo = getResult.pkgInfo; - const upstream = getResult.upstream; - if (pkgInfo !== null) { - depObj.upstream = upstream; - } - // try fetching package info from the default registry - if (pkgInfo === null) { - pkgInfo = (await fetchPackageInfo(entry.name)) ?? null; - if (pkgInfo) { - depObj.upstream = false; - cachedPacakgeInfoDict[entry.name] = { pkgInfo, upstream: false }; - } - } - // try fetching package info from the upstream registry - if (!pkgInfo) { - pkgInfo = - (await fetchPackageInfo(entry.name, env.upstreamRegistry)) ?? null; - if (pkgInfo) { - depObj.upstream = true; - cachedPacakgeInfoDict[entry.name] = { pkgInfo, upstream: true }; - } - } - // handle package not exist - if (!pkgInfo) { - log.warn("404", `package not found: ${entry.name}`); - depObj.reason = "package404"; - depsInvalid.push(depObj); - continue; - } - // verify version - const versions = Object.keys(pkgInfo.versions); - if (!entry.version || entry.version == "latest") { - const latestVersion = getLatestVersion(pkgInfo); - assert(latestVersion !== undefined); - // eslint-disable-next-line require-atomic-updates - depObj.version = entry.version = latestVersion; - } - // handle version not exist - if (!versions.find((x) => x == entry.version)) { - log.warn( - "404", - `package ${entry.name}@${ - entry.version - } is not a valid choice of ${versions.reverse().join(", ")}` - ); - depObj.reason = "version404"; - // eslint-disable-next-line require-atomic-updates - // depObj.version = entry.version = getLatestVersion(pkgInfo); - // log.warn("notarget", `fallback to ${entry.name}@${entry.version}`); - depsInvalid.push(depObj); - continue; - } - // 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] }; - }); - deps.forEach((x) => pendingList.push(x)); - } - } - depsValid.push(depObj); - log.verbose( - "dependency", - `${entry.name}@${entry.version} ${ - depObj.internal ? "[internal] " : "" - }${depObj.upstream ? "[upstream]" : ""}` - ); - } - } - return [depsValid, depsInvalid]; -}; - -// Get latest version from package info -export const getLatestVersion = function ( - pkgInfo: Partial -): PkgVersion | undefined { - if (pkgInfo["dist-tags"]?.["latest"] !== undefined) - return pkgInfo["dist-tags"]["latest"]; - else if (pkgInfo.version) return pkgInfo.version; -}; - -// Load manifest json file -export const loadManifest = function (): PkgManifest | null { - try { - const text = fs.readFileSync(env.manifestPath, { encoding: "utf8" }); - return JSON.parse(text); - } catch (err) { - assertIsError(err); - if (err.code == "ENOENT") - log.error("manifest", "file Packages/manifest.json does not exist"); - else { - log.error( - "manifest", - `failed to parse Packages/manifest.json at ${env.manifestPath}` - ); - log.error("manifest", err.message); - } - return null; - } -}; - -// Save manifest json file -export const saveManifest = function (data: PkgManifest) { - const json = JSON.stringify(data, null, 2); - try { - fs.writeFileSync(env.manifestPath, json); - return true; - } catch (err) { - assertIsError(err); - log.error("manifest", "can not write manifest json file"); - log.error("manifest", err.message); - return false; - } -}; - -// Get .upmconfig.toml directory -export const getUpmConfigDir = async function (): Promise { - let dirPath: string | undefined = ""; - const systemUserSubPath = "Unity/config/ServiceAccounts"; - if (env.wsl) { - if (!isWsl) { - throw new Error("no WSL detected"); - } - if (env.systemUser) { - const allUserProfilePath = await execute( - 'wslpath "$(wslvar ALLUSERSPROFILE)"', - { trim: true } - ); - dirPath = path.join(allUserProfilePath, systemUserSubPath); - } else { - dirPath = await execute('wslpath "$(wslvar USERPROFILE)"', { - trim: true, - }); - } - } else { - dirPath = process.env.USERPROFILE - ? process.env.USERPROFILE - : process.env.HOME; - if (env.systemUser) { - if (!process.env.ALLUSERSPROFILE) { - throw new Error("env ALLUSERSPROFILE is empty"); - } - dirPath = path.join(process.env.ALLUSERSPROFILE, systemUserSubPath); - } - } - if (dirPath === undefined) - throw new Error("Could not resolve upm-config dir-path"); - return dirPath; -}; - -// Load .upmconfig.toml -export const loadUpmConfig = async function ( - configDir?: string -): Promise { - if (configDir === undefined) configDir = await getUpmConfigDir(); - const configPath = path.join(configDir, ".upmconfig.toml"); - if (fs.existsSync(configPath)) { - const content = fs.readFileSync(configPath, "utf8"); - const config = TOML.parse(content); - - // NOTE: We assume correct format - return config as UPMConfig; - } -}; - -// Save .upmconfig.toml -export const saveUpmConfig = async function ( - config: UPMConfig, - configDir: string -) { - if (configDir === undefined) configDir = await getUpmConfigDir(); - mkdirp.sync(configDir); - const configPath = path.join(configDir, ".upmconfig.toml"); - const content = TOML.stringify(config); - fs.writeFileSync(configPath, content, "utf8"); - log.notice("config", "saved unity config at " + configPath); -}; - -// Compare unity editor version and return -1, 0, or 1. -export const compareEditorVersion = function (a: string, b: string) { - const verA = parseEditorVersion(a); - const verB = parseEditorVersion(b); - - if (verA === null || verB === null) - throw new Error("An editor version could not be parsed"); - - const editorVersionToArray = (ver: SemanticVersion) => [ - ver.major, - ver.minor, - ver.patch || 0, - ver.flagValue || 0, - ver.build || 0, - ver.locValue || 0, - ver.locBuild || 0, - ]; - const arrA = editorVersionToArray(verA); - const arrB = editorVersionToArray(verB); - for (let i = 0; i < arrA.length; i++) { - const valA = arrA[i]; - const valB = arrB[i]; - if (valA > valB) return 1; - else if (valA < valB) return -1; - } - return 0; -}; - -/** - * Prase editor version string to groups. - * - * E.g. 2020.2.0f2c4 - * major: 2020 - * minor: 2 - * patch: 0 - * flag: 'f' - * flagValue: 2 - * build: 2 - * loc: 'c' - * locValue: 1 - * locBuild: 4 - */ -export const parseEditorVersion = function ( - version: string | null -): SemanticVersion | null { - type RegexMatchGroups = { - major: `${number}`; - minor: `${number}`; - patch?: string; - flag?: "a" | "b" | "f" | "c"; - build?: `${number}`; - loc?: "c"; - locBuild?: `${number}`; - }; - - if (!version) return null; - const regex = - /^(?\d+)\.(?\d+)(\.(?\d+)((?a|b|f|c)(?\d+)((?c)(?\d+))?)?)?/; - const match = regex.exec(version); - if (!match) return null; - const groups = match.groups; - const result: SemanticVersion = { - major: parseInt(groups.major), - minor: parseInt(groups.minor), - }; - if (groups.patch) result.patch = parseInt(groups.patch); - if (groups.flag) { - result.flag = groups.flag; - if (result.flag == "a") result.flagValue = 0; - if (result.flag == "b") result.flagValue = 1; - if (result.flag == "f") result.flagValue = 2; - if (groups.build) result.build = parseInt(groups.build); - } - - if (groups.loc) { - result.loc = groups.loc.toLowerCase(); - if (result.loc == "c") result.locValue = 1; - if (groups.locBuild) result.locBuild = parseInt(groups.locBuild); - } - return result; -}; - -// Detect if the given package name is an internal package -export const isInternalPackage = function (name: PkgName): 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/registry-client.ts b/src/registry-client.ts new file mode 100644 index 00000000..2e0152be --- /dev/null +++ b/src/registry-client.ts @@ -0,0 +1,247 @@ +import { promisify } from "util"; +import RegClient, { + AddUserParams, + AddUserResponse, + ClientCallback, + GetParams, +} from "another-npm-registry-client"; +import log from "./logger"; +import request from "request"; +import assert, { AssertionError } from "assert"; +import { + Dependency, + NameVersionPair, + PkgInfo, + PkgName, + PkgVersion, + Registry, +} from "./types/global"; +import { env } from "./utils/env"; +import { atVersion, isInternalPackage } from "./utils/pkg-name"; +import _ from "lodash"; +import { tryGetLatestVersion } from "./utils/pkg-info"; + +export type NpmClient = { + rawClient: RegClient; + /** + * @throws {NpmClientError} + */ + get(uri: string, options: GetParams): Promise; + /** + * @throws {NpmClientError} + */ + adduser(uri: string, options: AddUserParams): Promise; +}; + +export class NpmClientError extends Error { + cause: Error; + response: request.Response; + + constructor(cause: Error, response: request.Response) { + super( + cause?.message ?? + "An error occurred while interacting with an Npm registry" + ); + this.name = "NpmClientError"; + this.cause = cause; + this.response = response; + } +} + +export function assertIsNpmClientError( + x: unknown +): asserts x is NpmClientError { + if (!(x instanceof NpmClientError)) + throw new AssertionError({ + message: "Given object was not an NpmClientError", + actual: x, + }); +} + +/** + * Normalizes a RegClient function. Specifically it merges it's multiple + * callback arguments into a single NormalizedError object. This function + * also takes care of binding and promisifying. + */ +function normalizeClientFunction( + client: RegClient, + fn: (uri: string, params: TParam, cb: ClientCallback) => void +): (uri: string, params: TParam) => Promise { + const bound = fn.bind(client); + const withNormalizedError = ( + uri: string, + params: TParam, + cb: (error: NpmClientError | null, data: TData) => void + ) => { + return bound(uri, params, (error, data, raw, res) => { + cb(error !== null ? new NpmClientError(error, res) : null, data); + }); + }; + return promisify(withNormalizedError); +} + +/** + * Return npm client + */ +export const getNpmClient = (): NpmClient => { + // create client + const client = new RegClient({ log }); + return { + // The instance of raw npm client + rawClient: client, + // Promisified methods + get: normalizeClientFunction(client, client.get), + adduser: normalizeClientFunction(client, client.adduser), + }; +}; +// Fetch package info json from registry +export const fetchPackageInfo = async function ( + name: PkgName, + registry?: Registry +): Promise { + if (!registry) registry = env.registry; + const pkgPath = `${registry}/${name}`; + const client = getNpmClient(); + try { + return await client.get(pkgPath, { auth: env.auth[registry] || undefined }); + // eslint-disable-next-line no-empty + } catch (err) {} +}; +/* Fetch package [valid dependencies, invalid dependencies] with a structure of + [ + { + name, + version, + upstream, // whether belongs to upstream registry + self, // whether is the source package + internal, // whether is an internal package + reason // invalid reason of "version404", "package404" + }, ... + ] + */ +export const fetchPackageDependencies = async function ({ + name, + version, + deep, +}: { + name: PkgName; + version: PkgVersion | undefined; + deep?: boolean; +}): Promise<[Dependency[], Dependency[]]> { + log.verbose( + "dependency", + `fetch: ${ + version !== undefined ? atVersion(name, version) : name + } deep=${deep}` + ); + // a list of pending dependency {name, version} + const pendingList: NameVersionPair[] = [{ name, version }]; + // a list of processed dependency {name, version} + const processedList = []; + // a list of dependency entry exists on the registry + const depsValid = []; + // a list of dependency entry doesn't exist on the registry + const depsInvalid = []; + // cached dict: {pkg-name: pkgInfo} + const cachedPacakgeInfoDict: Record< + PkgVersion, + { pkgInfo: PkgInfo; upstream: boolean } + > = {}; + while (pendingList.length > 0) { + // NOTE: Guaranteed defined because of while loop logic + const entry = pendingList.shift() as NameVersionPair; + if (processedList.find((x) => _.isEqual(x, entry)) === undefined) { + // add entry to processed list + processedList.push(entry); + // create valid depedenency structure + const depObj: Dependency = { + ...entry, + internal: isInternalPackage(entry.name), + upstream: false, + self: entry.name == name, + version: "", + reason: null, + }; + if (!depObj.internal) { + // try fetching package info from cache + const getResult = _.get(cachedPacakgeInfoDict, entry.name, { + pkgInfo: null, + upstream: false, + }); + let pkgInfo = getResult.pkgInfo; + const upstream = getResult.upstream; + if (pkgInfo !== null) { + depObj.upstream = upstream; + } + // try fetching package info from the default registry + if (pkgInfo === null) { + pkgInfo = (await fetchPackageInfo(entry.name)) ?? null; + if (pkgInfo) { + depObj.upstream = false; + cachedPacakgeInfoDict[entry.name] = { pkgInfo, upstream: false }; + } + } + // try fetching package info from the upstream registry + if (!pkgInfo) { + pkgInfo = + (await fetchPackageInfo(entry.name, env.upstreamRegistry)) ?? null; + if (pkgInfo) { + depObj.upstream = true; + cachedPacakgeInfoDict[entry.name] = { pkgInfo, upstream: true }; + } + } + // handle package not exist + if (!pkgInfo) { + log.warn("404", `package not found: ${entry.name}`); + depObj.reason = "package404"; + depsInvalid.push(depObj); + continue; + } + // verify version + const versions = Object.keys(pkgInfo.versions); + if (!entry.version || entry.version == "latest") { + const latestVersion = tryGetLatestVersion(pkgInfo); + assert(latestVersion !== undefined); + // eslint-disable-next-line require-atomic-updates + depObj.version = entry.version = latestVersion; + } + // handle version not exist + 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(", ")}` + ); + depObj.reason = "version404"; + // eslint-disable-next-line require-atomic-updates + // depObj.version = entry.version = getLatestVersion(pkgInfo); + // log.warn("notarget", `fallback to ${entry.name}@${entry.version}`); + depsInvalid.push(depObj); + continue; + } + // 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] }; + }); + deps.forEach((x) => pendingList.push(x)); + } + } + depsValid.push(depObj); + log.verbose( + "dependency", + `${ + entry.version !== undefined + ? atVersion(entry.name, entry.version) + : entry.name + } ${depObj.internal ? "[internal] " : ""}${ + depObj.upstream ? "[upstream]" : "" + }` + ); + } + } + return [depsValid, depsInvalid]; +}; diff --git a/src/types/global.ts b/src/types/global.ts index 3af06329..7f2cd06b 100644 --- a/src/types/global.ts +++ b/src/types/global.ts @@ -10,7 +10,7 @@ export type Region = "us" | "cn"; export type Registry = string; -export type SemanticVersion = { +export type EditorVersion = { major: number; minor: number; patch?: number; @@ -74,7 +74,7 @@ export type NameVersionPair = { export type Dependency = { name: PkgName; - version: PkgVersion | undefined; + version: PkgVersion; upstream: boolean; self: boolean; internal: boolean; diff --git a/src/utils/editor-version.ts b/src/utils/editor-version.ts new file mode 100644 index 00000000..aed12f0a --- /dev/null +++ b/src/utils/editor-version.ts @@ -0,0 +1,90 @@ +import { EditorVersion } from "../types/global"; + +/** + * Compares two editor versions for ordering + * @param a The first version + * @param b The second version + */ +export const compareEditorVersion = function ( + a: string, + b: string +): -1 | 0 | 1 { + const verA = tryParseEditorVersion(a); + const verB = tryParseEditorVersion(b); + + if (verA === null || verB === null) + throw new Error("An editor version could not be parsed"); + + const editorVersionToArray = (ver: EditorVersion) => [ + ver.major, + ver.minor, + ver.patch || 0, + ver.flagValue || 0, + ver.build || 0, + ver.locValue || 0, + ver.locBuild || 0, + ]; + const arrA = editorVersionToArray(verA); + const arrB = editorVersionToArray(verB); + for (let i = 0; i < arrA.length; i++) { + const valA = arrA[i]; + const valB = arrB[i]; + if (valA > valB) return 1; + else if (valA < valB) return -1; + } + return 0; +}; + +/** + * Attempts to parse editor version string to groups + * + * E.g. 2020.2.0f2c4 + * major: 2020 + * minor: 2 + * patch: 0 + * flag: 'f' + * flagValue: 2 + * build: 2 + * loc: 'c' + * locValue: 1 + * locBuild: 4 + */ +export const tryParseEditorVersion = function ( + version: string | null +): EditorVersion | null { + type RegexMatchGroups = { + major: `${number}`; + minor: `${number}`; + patch?: string; + flag?: "a" | "b" | "f" | "c"; + build?: `${number}`; + loc?: "c"; + locBuild?: `${number}`; + }; + + if (!version) return null; + const regex = + /^(?\d+)\.(?\d+)(\.(?\d+)((?a|b|f|c)(?\d+)((?c)(?\d+))?)?)?/; + const match = regex.exec(version); + if (!match) return null; + const groups = match.groups; + const result: EditorVersion = { + major: parseInt(groups.major), + minor: parseInt(groups.minor), + }; + if (groups.patch) result.patch = parseInt(groups.patch); + if (groups.flag) { + result.flag = groups.flag; + if (result.flag == "a") result.flagValue = 0; + if (result.flag == "b") result.flagValue = 1; + if (result.flag == "f") result.flagValue = 2; + if (groups.build) result.build = parseInt(groups.build); + } + + if (groups.loc) { + result.loc = groups.loc.toLowerCase(); + if (result.loc == "c") result.locValue = 1; + if (groups.locBuild) result.locBuild = parseInt(groups.locBuild); + } + return result; +}; diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 00000000..c49b6f1e --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,149 @@ +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"; + +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.cwd = ""; + env.manifestPath = ""; + env.upstream = true; + env.color = true; + env.upstreamRegistry = "https://packages.unity.com"; + env.systemUser = false; + env.wsl = false; + env.editorVersion = null; + env.region = "us"; + // the npmAuth field of .upmconfig.toml + env.npmAuth = {}; + // the dict of auth param for npm registry API + env.auth = {}; + // log level + log.level = options._global.verbose ? "verbose" : "notice"; + // color + if (options._global.color === false) env.color = false; + if (process.env.NODE_ENV == "test") env.color = false; + if (!env.color) { + chalk.level = 0; + log.disableColor(); + } + // upstream + 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.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; + // 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("."); + } + // auth + if (options._global.systemUser) env.systemUser = true; + if (options._global.wsl) env.wsl = true; + 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 ("token" in regAuth) { + env.auth[reg] = { + token: regAuth.token, + alwaysAuth: regAuth.alwaysAuth || false, + }; + } else if ("_auth" in regAuth) { + const buf = Buffer.from(regAuth._auth, "base64"); + const text = buf.toString("utf-8"); + const [username, password] = text.split(":", 2); + env.auth[reg] = { + username, + password: Buffer.from(password).toString("base64"), + email: regAuth.email, + alwaysAuth: regAuth.alwaysAuth || false, + }; + } else { + log.warn( + "env.auth", + `failed to parse auth info for ${reg} in .upmconfig.toml: missing token or _auth fields` + ); + log.warn("env.auth", regAuth); + } + } + } + } + // log.verbose("env.npmAuth", env.npmAuth); + // log.verbose("env.auth", env.auth); + // return if no need to check path + if (!checkPath) return true; + // cwd + if (options._global.chdir) { + const cwd = path.resolve(options._global.chdir); + if (!fs.existsSync(cwd)) { + log.error("env", `can not resolve path ${cwd}`); + return false; + } + env.cwd = cwd; + } else env.cwd = process.cwd(); + // manifest path + const manifestPath = path.join(env.cwd, "Packages/manifest.json"); + if (!fs.existsSync(manifestPath)) { + log.error( + "manifest", + `can not locate manifest.json at path ${manifestPath}` + ); + return false; + } else env.manifestPath = manifestPath; + // editor version + const projectVersionPath = path.join( + env.cwd, + "ProjectSettings/ProjectVersion.txt" + ); + if (!fs.existsSync(projectVersionPath)) { + log.warn( + "ProjectVersion", + `can not locate ProjectVersion.text at path ${projectVersionPath}` + ); + } else { + const projectVersionData = fs.readFileSync(projectVersionPath, "utf8"); + const projectVersionContent = yaml.parse(projectVersionData); + env.editorVersion = projectVersionContent.m_EditorVersion; + } + // return + return true; +}; diff --git a/src/utils/manifest.ts b/src/utils/manifest.ts new file mode 100644 index 00000000..25fa3ed8 --- /dev/null +++ b/src/utils/manifest.ts @@ -0,0 +1,43 @@ +import { PkgManifest } from "../types/global"; +import fs from "fs"; +import { assertIsError } from "./error-type-guards"; +import log from "../logger"; +import { env } from "./env"; + +/** + * Attempts to load the manifest from the path specified in env + */ +export const loadManifest = function (): PkgManifest | null { + try { + const text = fs.readFileSync(env.manifestPath, { encoding: "utf8" }); + return JSON.parse(text); + } catch (err) { + assertIsError(err); + if (err.code == "ENOENT") + log.error("manifest", "file Packages/manifest.json does not exist"); + else { + log.error( + "manifest", + `failed to parse Packages/manifest.json at ${env.manifestPath}` + ); + log.error("manifest", err.message); + } + return null; + } +}; + +/** + * Save manifest json file to the path specified in env + */ +export const saveManifest = function (data: PkgManifest) { + const json = JSON.stringify(data, null, 2); + try { + fs.writeFileSync(env.manifestPath, json); + return true; + } catch (err) { + assertIsError(err); + log.error("manifest", "can not write manifest json file"); + log.error("manifest", err.message); + return false; + } +}; diff --git a/src/utils/pkg-info.ts b/src/utils/pkg-info.ts new file mode 100644 index 00000000..bb2bc466 --- /dev/null +++ b/src/utils/pkg-info.ts @@ -0,0 +1,18 @@ +import { PkgInfo, PkgVersion } from "../types/global"; + +const hasLatestDistTag = ( + pkgInfo: Partial +): pkgInfo is Partial & { "dist-tags": { latest: PkgVersion } } => { + return pkgInfo["dist-tags"]?.["latest"] !== undefined; +}; + +/** + * Attempt to get the latest version from a package + * @param pkgInfo The package. All properties are assumed to be potentially missing + */ +export const tryGetLatestVersion = function ( + pkgInfo: Partial +): PkgVersion | 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 index 146ea161..e663f4b1 100644 --- a/src/utils/pkg-name.ts +++ b/src/utils/pkg-name.ts @@ -16,3 +16,28 @@ export const splitPkgName = function (pkgName: PkgName): { : 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 index fac76da6..171f37cd 100644 --- a/src/utils/pkg-version.ts +++ b/src/utils/pkg-version.ts @@ -1,6 +1,7 @@ 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"); diff --git a/src/utils/upm-config.ts b/src/utils/upm-config.ts new file mode 100644 index 00000000..2cdfc2fe --- /dev/null +++ b/src/utils/upm-config.ts @@ -0,0 +1,75 @@ +import { UPMConfig } from "../types/global"; +import mkdirp from "mkdirp"; +import path from "path"; +import TOML from "@iarna/toml"; +import fs from "fs"; +import log from "../logger"; +import isWsl from "is-wsl"; +import execute from "./process"; +import { env } from "./env"; + +/** + * Gets the path to directory in which the upm config is stored + */ +export const getUpmConfigDir = async (): Promise => { + let dirPath: string | undefined = ""; + const systemUserSubPath = "Unity/config/ServiceAccounts"; + if (env.wsl) { + if (!isWsl) { + throw new Error("no WSL detected"); + } + if (env.systemUser) { + const allUserProfilePath = await execute( + 'wslpath "$(wslvar ALLUSERSPROFILE)"', + { trim: true } + ); + dirPath = path.join(allUserProfilePath, systemUserSubPath); + } else { + dirPath = await execute('wslpath "$(wslvar USERPROFILE)"', { + trim: true, + }); + } + } else { + dirPath = process.env.USERPROFILE + ? process.env.USERPROFILE + : process.env.HOME; + if (env.systemUser) { + if (!process.env.ALLUSERSPROFILE) { + throw new Error("env ALLUSERSPROFILE is empty"); + } + dirPath = path.join(process.env.ALLUSERSPROFILE, systemUserSubPath); + } + } + if (dirPath === undefined) + throw new Error("Could not resolve upm-config dir-path"); + return dirPath; +}; + +/** + * Attempts to load the upm config + */ +export const loadUpmConfig = async ( + configDir?: string +): Promise => { + if (configDir === undefined) configDir = await getUpmConfigDir(); + const configPath = path.join(configDir, ".upmconfig.toml"); + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, "utf8"); + const config = TOML.parse(content); + + // NOTE: We assume correct format + return config as UPMConfig; + } +}; + +/** + * Save the upm config + */ +export const saveUpmConfig = async (config: UPMConfig, configDir: string) => { + if (configDir === undefined) configDir = await getUpmConfigDir(); + mkdirp.sync(configDir); + const configPath = path.join(configDir, ".upmconfig.toml"); + const content = TOML.stringify(config); + fs.writeFileSync(configPath, content, "utf8"); + log.notice("config", "saved unity config at " + configPath); +}; diff --git a/test/test-cmd-add.ts b/test/test-cmd-add.ts index 86b1c14d..3dff8b5a 100644 --- a/test/test-cmd-add.ts +++ b/test/test-cmd-add.ts @@ -1,7 +1,6 @@ import "assert"; import nock from "nock"; import "should"; -import { loadManifest } from "../src/core"; import { add } from "../src/cmd-add"; @@ -16,6 +15,7 @@ import { } from "./utils"; import testConsole from "test-console"; import assert from "assert"; +import { loadManifest } from "../src/utils/manifest"; describe("cmd-add.ts", function () { const options = { diff --git a/test/test-cmd-remove.ts b/test/test-cmd-remove.ts index abad6df7..11ac5244 100644 --- a/test/test-cmd-remove.ts +++ b/test/test-cmd-remove.ts @@ -1,6 +1,5 @@ import "assert"; import "should"; -import { loadManifest } from "../src/core"; import { remove } from "../src/cmd-remove"; @@ -13,6 +12,7 @@ import { } from "./utils"; import testConsole from "test-console"; import assert from "assert"; +import { loadManifest } from "../src/utils/manifest"; describe("cmd-remove.ts", function () { describe("remove", function () { diff --git a/test/test-core.ts b/test/test-core.ts deleted file mode 100644 index 04f3682c..00000000 --- a/test/test-core.ts +++ /dev/null @@ -1,453 +0,0 @@ -import "assert"; -import fs from "fs"; -import nock from "nock"; -import path from "path"; -import "should"; -import { - compareEditorVersion, - env, - fetchPackageInfo, - getLatestVersion, - isInternalPackage, - loadManifest, - parseEditorVersion, - parseEnv, - saveManifest, -} from "../src/core"; - -import { - createWorkDir, - getInspects, - getOutputs, - getWorkDir, - nockDown, - nockUp, - removeWorkDir, -} from "./utils"; -import testConsole from "test-console"; -import assert from "assert"; - -describe("cmd-core.ts", function () { - describe("parseEnv", function () { - let stdoutInspect: testConsole.Inspector = null!; - let stderrInspect: testConsole.Inspector = null!; - before(function () { - removeWorkDir("test-openupm-cli"); - removeWorkDir("test-openupm-cli-no-manifest"); - createWorkDir("test-openupm-cli", { - manifest: true, - editorVersion: " 2019.2.13f1", - }); - createWorkDir("test-openupm-cli-no-manifest", { - manifest: false, - editorVersion: " 2019.2.13f1", - }); - }); - after(function () { - removeWorkDir("test-openupm-cli"); - removeWorkDir("test-openupm-cli-no-manifest"); - }); - beforeEach(function () { - [stdoutInspect, stderrInspect] = getInspects(); - }); - afterEach(function () { - stdoutInspect.restore(); - stderrInspect.restore(); - }); - it("defaults", async function () { - (await parseEnv({ _global: {} }, { checkPath: false })).should.be.ok(); - env.registry.should.equal("https://package.openupm.com"); - env.upstream.should.be.ok(); - env.upstreamRegistry.should.equal("https://packages.unity.com"); - env.namespace.should.equal("com.openupm"); - env.cwd.should.equal(""); - env.manifestPath.should.equal(""); - (env.editorVersion === null).should.be.ok(); - }); - it("check path", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("test-openupm-cli") } }, - { checkPath: true } - ) - ).should.be.ok(); - env.cwd.should.be.equal(getWorkDir("test-openupm-cli")); - env.manifestPath.should.be.equal( - path.join(getWorkDir("test-openupm-cli"), "Packages/manifest.json") - ); - }); - it("can not resolve path", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("path-not-exist") } }, - { checkPath: true } - ) - ).should.not.be.ok(); - const [stdout] = getOutputs(stdoutInspect, stderrInspect); - stdout.includes("can not resolve path").should.be.ok(); - }); - it("can not locate manifest.json", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("test-openupm-cli-no-manifest") } }, - { checkPath: true } - ) - ).should.not.be.ok(); - const [stdout] = getOutputs(stdoutInspect, stderrInspect); - stdout.includes("can not locate manifest.json").should.be.ok(); - }); - it("custom registry", async function () { - ( - await parseEnv( - { _global: { registry: "https://registry.npmjs.org" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("https://registry.npmjs.org"); - env.namespace.should.be.equal("org.npmjs"); - }); - it("custom registry with splash", async function () { - ( - await parseEnv( - { _global: { registry: "https://registry.npmjs.org/" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("https://registry.npmjs.org"); - env.namespace.should.be.equal("org.npmjs"); - }); - it("custom registry with extra path", async function () { - ( - await parseEnv( - { _global: { registry: "https://registry.npmjs.org/some" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("https://registry.npmjs.org/some"); - env.namespace.should.be.equal("org.npmjs"); - }); - it("custom registry with extra path and splash", async function () { - ( - await parseEnv( - { _global: { registry: "https://registry.npmjs.org/some/" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("https://registry.npmjs.org/some"); - env.namespace.should.be.equal("org.npmjs"); - }); - it("custom registry without http", async function () { - ( - await parseEnv( - { _global: { registry: "registry.npmjs.org" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("http://registry.npmjs.org"); - env.namespace.should.be.equal("org.npmjs"); - }); - it("custom registry with ipv4+port", async function () { - ( - await parseEnv( - { _global: { registry: "http://127.0.0.1:4873" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("http://127.0.0.1:4873"); - env.namespace.should.be.equal("127.0.0.1"); - }); - it("custom registry with ipv6+port", async function () { - ( - await parseEnv( - { _global: { registry: "http://[1:2:3:4:5:6:7:8]:4873" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("http://[1:2:3:4:5:6:7:8]:4873"); - env.namespace.should.be.equal("1:2:3:4:5:6:7:8"); - }); - it("upstream", async function () { - ( - await parseEnv({ _global: { upstream: false } }, { checkPath: false }) - ).should.be.ok(); - env.upstream.should.not.be.ok(); - }); - it("editorVersion", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("test-openupm-cli") } }, - { checkPath: true } - ) - ).should.be.ok(); - assert(env.editorVersion !== null); - env.editorVersion.should.be.equal("2019.2.13f1"); - }); - it("region cn", async function () { - ( - await parseEnv({ _global: { cn: true } }, { checkPath: false }) - ).should.be.ok(); - env.registry.should.be.equal("https://package.openupm.cn"); - env.upstreamRegistry.should.be.equal("https://packages.unity.cn"); - env.region.should.be.equal("cn"); - }); - it("region cn with a custom registry", async function () { - ( - await parseEnv( - { _global: { cn: true, registry: "https://reg.custom-package.com" } }, - { checkPath: false } - ) - ).should.be.ok(); - env.registry.should.be.equal("https://reg.custom-package.com"); - env.upstreamRegistry.should.be.equal("https://packages.unity.cn"); - env.region.should.be.equal("cn"); - }); - }); - - describe("loadManifest/SaveManifest", function () { - let stdoutInspect: testConsole.Inspector = null!; - let stderrInspect: testConsole.Inspector = null!; - beforeEach(function () { - removeWorkDir("test-openupm-cli"); - createWorkDir("test-openupm-cli", { manifest: true }); - createWorkDir("test-openupm-cli-wrong-json", { - manifest: true, - }); - fs.writeFileSync( - path.join( - getWorkDir("test-openupm-cli-wrong-json"), - "Packages/manifest.json" - ), - "wrong-json" - ); - [stdoutInspect, stderrInspect] = getInspects(); - }); - afterEach(function () { - removeWorkDir("test-openupm-cli"); - removeWorkDir("test-openupm-cli-wrong-json"); - stdoutInspect.restore(); - stderrInspect.restore(); - }); - it("loadManifest", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("test-openupm-cli") } }, - { checkPath: true } - ) - ).should.be.ok(); - const manifest = loadManifest(); - assert(manifest !== null); - manifest.should.be.deepEqual({ dependencies: {} }); - }); - it("no manifest file", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("path-not-exist") } }, - { checkPath: false } - ) - ).should.be.ok(); - const manifest = loadManifest(); - (manifest === null).should.be.ok(); - const [stdout] = getOutputs(stdoutInspect, stderrInspect); - stdout.includes("does not exist").should.be.ok(); - }); - it("wrong json content", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("test-openupm-cli-wrong-json") } }, - { checkPath: true } - ) - ).should.be.ok(); - const manifest = loadManifest(); - (manifest === null).should.be.ok(); - const [stdout] = getOutputs(stdoutInspect, stderrInspect); - stdout.includes("failed to parse").should.be.ok(); - }); - it("saveManifest", async function () { - ( - await parseEnv( - { _global: { chdir: getWorkDir("test-openupm-cli") } }, - { checkPath: true } - ) - ).should.be.ok(); - const manifest = loadManifest(); - assert(manifest !== null); - manifest.should.be.deepEqual({ dependencies: {} }); - manifest.dependencies["some-pack"] = "1.0.0"; - saveManifest(manifest).should.be.ok(); - const manifest2 = loadManifest(); - assert(manifest2 !== null); - manifest2.should.be.deepEqual(manifest); - }); - }); - - describe("fetchPackageInfo", function () { - beforeEach(function () { - nockUp(); - }); - afterEach(function () { - nockDown(); - }); - it("simple", async function () { - ( - await parseEnv( - { _global: { registry: "http://example.com" } }, - { checkPath: false } - ) - ).should.be.ok(); - const pkgInfoRemote = { name: "com.littlebigfun.addressable-importer" }; - nock("http://example.com") - .get("/package-a") - .reply(200, pkgInfoRemote, { "Content-Type": "application/json" }); - const info = await fetchPackageInfo("package-a"); - assert(info !== undefined); - info.should.deepEqual(pkgInfoRemote); - }); - it("404", async function () { - ( - await parseEnv( - { _global: { registry: "http://example.com" } }, - { checkPath: false } - ) - ).should.be.ok(); - - nock("http://example.com").get("/package-a").reply(404); - const info = await fetchPackageInfo("package-a"); - (info === undefined).should.be.ok(); - }); - }); - - describe("getLatestVersion", function () { - it("from dist-tags", async function () { - const version = getLatestVersion({ "dist-tags": { latest: "1.0.0" } }); - assert(version !== undefined); - version.should.equal("1.0.0"); - }); - }); - describe("parseEditorVersion", function () { - it("test null", function () { - (parseEditorVersion(null) === null).should.be.ok(); - }); - it("test x.y", function () { - const version = parseEditorVersion("2019.2"); - assert(version !== null); - version.should.deepEqual({ major: 2019, minor: 2 }); - }); - it("test x.y.z", function () { - const version = parseEditorVersion("2019.2.1"); - assert(version !== null); - version.should.deepEqual({ - major: 2019, - minor: 2, - patch: 1, - }); - }); - it("test x.y.zan", function () { - const version = parseEditorVersion("2019.2.1a5"); - assert(version !== null); - version.should.deepEqual({ - major: 2019, - minor: 2, - patch: 1, - flag: "a", - flagValue: 0, - build: 5, - }); - }); - it("test x.y.zbn", function () { - const version = parseEditorVersion("2019.2.1b5"); - assert(version !== null); - version.should.deepEqual({ - major: 2019, - minor: 2, - patch: 1, - flag: "b", - flagValue: 1, - build: 5, - }); - }); - it("test x.y.zfn", function () { - const version = parseEditorVersion("2019.2.1f5"); - assert(version !== null); - version.should.deepEqual({ - major: 2019, - minor: 2, - patch: 1, - flag: "f", - flagValue: 2, - build: 5, - }); - }); - it("test x.y.zcn", function () { - const version = parseEditorVersion("2019.2.1f1c5"); - assert(version !== null); - version.should.deepEqual({ - major: 2019, - minor: 2, - patch: 1, - flag: "f", - flagValue: 2, - build: 1, - loc: "c", - locValue: 1, - locBuild: 5, - }); - }); - it("test invalid version", function () { - (parseEditorVersion("2019") === null).should.be.ok(); - }); - }); - - describe("compareEditorVersion", function () { - it("test 2019.1 == 2019.1", function () { - compareEditorVersion("2019.1", "2019.1").should.equal(0); - }); - it("test 2019.1.1 == 2019.1.1", function () { - compareEditorVersion("2019.1.1", "2019.1.1").should.equal(0); - }); - it("test 2019.1.1f1 == 2019.1.1f1", function () { - compareEditorVersion("2019.1.1f1", "2019.1.1f1").should.equal(0); - }); - it("test 2019.1.1f1c1 == 2019.1.1f1c1", function () { - compareEditorVersion("2019.1.1f1c1", "2019.1.1f1c1").should.equal(0); - }); - it("test 2019.2 > 2019.1", function () { - compareEditorVersion("2019.2", "2019.1").should.equal(1); - }); - it("test 2020.2 > 2019.1", function () { - compareEditorVersion("2020.1", "2019.1").should.equal(1); - }); - it("test 2019.1 < 2019.2", function () { - compareEditorVersion("2019.1", "2019.2").should.equal(-1); - }); - it("test 2019.1 < 2020.1", function () { - compareEditorVersion("2019.1", "2020.1").should.equal(-1); - }); - it("test 2019.1 < 2019.1.1", function () { - compareEditorVersion("2019.1", "2019.1.1").should.equal(-1); - }); - it("test 2019.1.1 < 2019.1.1f1", function () { - compareEditorVersion("2019.1.1", "2019.1.1f1").should.equal(-1); - }); - it("test 2019.1.1a1 < 2020.1.1b1", function () { - compareEditorVersion("2019.1.1a1", "2020.1.1b1").should.equal(-1); - }); - it("test 2019.1.1b1 < 2020.1.1f1", function () { - compareEditorVersion("2019.1.1b1", "2020.1.1f1").should.equal(-1); - }); - it("test 2019.1.1f1 < 2020.1.1f1c1", function () { - compareEditorVersion("2019.1.1f1", "2020.1.1f1c1").should.equal(-1); - }); - }); - - 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-editor-version.ts b/test/test-editor-version.ts new file mode 100644 index 00000000..2fca64c2 --- /dev/null +++ b/test/test-editor-version.ts @@ -0,0 +1,125 @@ +import { describe } from "mocha"; +import { + compareEditorVersion, + tryParseEditorVersion, +} from "../src/utils/editor-version"; +import "should"; +import assert from "assert"; + +describe("editor-version", function () { + describe("parseEditorVersion", function () { + it("test null", function () { + (tryParseEditorVersion(null) === null).should.be.ok(); + }); + it("test x.y", function () { + const version = tryParseEditorVersion("2019.2"); + assert(version !== null); + version.should.deepEqual({ major: 2019, minor: 2 }); + }); + it("test x.y.z", function () { + const version = tryParseEditorVersion("2019.2.1"); + assert(version !== null); + version.should.deepEqual({ + major: 2019, + minor: 2, + patch: 1, + }); + }); + it("test x.y.zan", function () { + const version = tryParseEditorVersion("2019.2.1a5"); + assert(version !== null); + version.should.deepEqual({ + major: 2019, + minor: 2, + patch: 1, + flag: "a", + flagValue: 0, + build: 5, + }); + }); + it("test x.y.zbn", function () { + const version = tryParseEditorVersion("2019.2.1b5"); + assert(version !== null); + version.should.deepEqual({ + major: 2019, + minor: 2, + patch: 1, + flag: "b", + flagValue: 1, + build: 5, + }); + }); + it("test x.y.zfn", function () { + const version = tryParseEditorVersion("2019.2.1f5"); + assert(version !== null); + version.should.deepEqual({ + major: 2019, + minor: 2, + patch: 1, + flag: "f", + flagValue: 2, + build: 5, + }); + }); + it("test x.y.zcn", function () { + const version = tryParseEditorVersion("2019.2.1f1c5"); + assert(version !== null); + version.should.deepEqual({ + major: 2019, + minor: 2, + patch: 1, + flag: "f", + flagValue: 2, + build: 1, + loc: "c", + locValue: 1, + locBuild: 5, + }); + }); + it("test invalid version", function () { + (tryParseEditorVersion("2019") === null).should.be.ok(); + }); + }); + + describe("compareEditorVersion", function () { + it("test 2019.1 == 2019.1", function () { + compareEditorVersion("2019.1", "2019.1").should.equal(0); + }); + it("test 2019.1.1 == 2019.1.1", function () { + compareEditorVersion("2019.1.1", "2019.1.1").should.equal(0); + }); + it("test 2019.1.1f1 == 2019.1.1f1", function () { + compareEditorVersion("2019.1.1f1", "2019.1.1f1").should.equal(0); + }); + it("test 2019.1.1f1c1 == 2019.1.1f1c1", function () { + compareEditorVersion("2019.1.1f1c1", "2019.1.1f1c1").should.equal(0); + }); + it("test 2019.2 > 2019.1", function () { + compareEditorVersion("2019.2", "2019.1").should.equal(1); + }); + it("test 2020.2 > 2019.1", function () { + compareEditorVersion("2020.1", "2019.1").should.equal(1); + }); + it("test 2019.1 < 2019.2", function () { + compareEditorVersion("2019.1", "2019.2").should.equal(-1); + }); + it("test 2019.1 < 2020.1", function () { + compareEditorVersion("2019.1", "2020.1").should.equal(-1); + }); + it("test 2019.1 < 2019.1.1", function () { + compareEditorVersion("2019.1", "2019.1.1").should.equal(-1); + }); + it("test 2019.1.1 < 2019.1.1f1", function () { + compareEditorVersion("2019.1.1", "2019.1.1f1").should.equal(-1); + }); + it("test 2019.1.1a1 < 2020.1.1b1", function () { + compareEditorVersion("2019.1.1a1", "2020.1.1b1").should.equal(-1); + }); + it("test 2019.1.1b1 < 2020.1.1f1", function () { + compareEditorVersion("2019.1.1b1", "2020.1.1f1").should.equal(-1); + }); + it("test 2019.1.1f1 < 2020.1.1f1c1", function () { + compareEditorVersion("2019.1.1f1", "2020.1.1f1c1").should.equal(-1); + }); + }); +}); diff --git a/test/test-env.ts b/test/test-env.ts new file mode 100644 index 00000000..8c21d288 --- /dev/null +++ b/test/test-env.ts @@ -0,0 +1,189 @@ +import testConsole from "test-console"; +import { + createWorkDir, + getInspects, + getOutputs, + getWorkDir, + removeWorkDir, +} from "./utils"; +import "should"; +import { env, parseEnv } from "../src/utils/env"; +import path from "path"; +import assert from "assert"; + +describe("env", function () { + describe("parseEnv", function () { + let stdoutInspect: testConsole.Inspector = null!; + let stderrInspect: testConsole.Inspector = null!; + before(function () { + removeWorkDir("test-openupm-cli"); + removeWorkDir("test-openupm-cli-no-manifest"); + createWorkDir("test-openupm-cli", { + manifest: true, + editorVersion: " 2019.2.13f1", + }); + createWorkDir("test-openupm-cli-no-manifest", { + manifest: false, + editorVersion: " 2019.2.13f1", + }); + }); + after(function () { + removeWorkDir("test-openupm-cli"); + removeWorkDir("test-openupm-cli-no-manifest"); + }); + beforeEach(function () { + [stdoutInspect, stderrInspect] = getInspects(); + }); + afterEach(function () { + stdoutInspect.restore(); + stderrInspect.restore(); + }); + it("defaults", async function () { + (await parseEnv({ _global: {} }, { checkPath: false })).should.be.ok(); + env.registry.should.equal("https://package.openupm.com"); + env.upstream.should.be.ok(); + env.upstreamRegistry.should.equal("https://packages.unity.com"); + env.namespace.should.equal("com.openupm"); + env.cwd.should.equal(""); + env.manifestPath.should.equal(""); + (env.editorVersion === null).should.be.ok(); + }); + it("check path", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("test-openupm-cli") } }, + { checkPath: true } + ) + ).should.be.ok(); + env.cwd.should.be.equal(getWorkDir("test-openupm-cli")); + env.manifestPath.should.be.equal( + path.join(getWorkDir("test-openupm-cli"), "Packages/manifest.json") + ); + }); + it("can not resolve path", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("path-not-exist") } }, + { checkPath: true } + ) + ).should.not.be.ok(); + const [stdout] = getOutputs(stdoutInspect, stderrInspect); + stdout.includes("can not resolve path").should.be.ok(); + }); + it("can not locate manifest.json", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("test-openupm-cli-no-manifest") } }, + { checkPath: true } + ) + ).should.not.be.ok(); + const [stdout] = getOutputs(stdoutInspect, stderrInspect); + stdout.includes("can not locate manifest.json").should.be.ok(); + }); + it("custom registry", async function () { + ( + await parseEnv( + { _global: { registry: "https://registry.npmjs.org" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("https://registry.npmjs.org"); + env.namespace.should.be.equal("org.npmjs"); + }); + it("custom registry with splash", async function () { + ( + await parseEnv( + { _global: { registry: "https://registry.npmjs.org/" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("https://registry.npmjs.org"); + env.namespace.should.be.equal("org.npmjs"); + }); + it("custom registry with extra path", async function () { + ( + await parseEnv( + { _global: { registry: "https://registry.npmjs.org/some" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("https://registry.npmjs.org/some"); + env.namespace.should.be.equal("org.npmjs"); + }); + it("custom registry with extra path and splash", async function () { + ( + await parseEnv( + { _global: { registry: "https://registry.npmjs.org/some/" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("https://registry.npmjs.org/some"); + env.namespace.should.be.equal("org.npmjs"); + }); + it("custom registry without http", async function () { + ( + await parseEnv( + { _global: { registry: "registry.npmjs.org" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("http://registry.npmjs.org"); + env.namespace.should.be.equal("org.npmjs"); + }); + it("custom registry with ipv4+port", async function () { + ( + await parseEnv( + { _global: { registry: "http://127.0.0.1:4873" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("http://127.0.0.1:4873"); + env.namespace.should.be.equal("127.0.0.1"); + }); + it("custom registry with ipv6+port", async function () { + ( + await parseEnv( + { _global: { registry: "http://[1:2:3:4:5:6:7:8]:4873" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("http://[1:2:3:4:5:6:7:8]:4873"); + env.namespace.should.be.equal("1:2:3:4:5:6:7:8"); + }); + it("upstream", async function () { + ( + await parseEnv({ _global: { upstream: false } }, { checkPath: false }) + ).should.be.ok(); + env.upstream.should.not.be.ok(); + }); + it("editorVersion", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("test-openupm-cli") } }, + { checkPath: true } + ) + ).should.be.ok(); + assert(env.editorVersion !== null); + env.editorVersion.should.be.equal("2019.2.13f1"); + }); + it("region cn", async function () { + ( + await parseEnv({ _global: { cn: true } }, { checkPath: false }) + ).should.be.ok(); + env.registry.should.be.equal("https://package.openupm.cn"); + env.upstreamRegistry.should.be.equal("https://packages.unity.cn"); + env.region.should.be.equal("cn"); + }); + it("region cn with a custom registry", async function () { + ( + await parseEnv( + { _global: { cn: true, registry: "https://reg.custom-package.com" } }, + { checkPath: false } + ) + ).should.be.ok(); + env.registry.should.be.equal("https://reg.custom-package.com"); + env.upstreamRegistry.should.be.equal("https://packages.unity.cn"); + env.region.should.be.equal("cn"); + }); + }); +}); diff --git a/test/test-manifest.ts b/test/test-manifest.ts new file mode 100644 index 00000000..d168784e --- /dev/null +++ b/test/test-manifest.ts @@ -0,0 +1,92 @@ +import testConsole from "test-console"; +import { + createWorkDir, + getInspects, + getOutputs, + getWorkDir, + removeWorkDir, +} from "./utils"; +import fs from "fs"; +import "should"; +import path from "path"; +import { loadManifest, saveManifest } from "../src/utils/manifest"; +import assert from "assert"; +import { describe } from "mocha"; +import { parseEnv } from "../src/utils/env"; + +describe("manifest", function () { + let stdoutInspect: testConsole.Inspector = null!; + let stderrInspect: testConsole.Inspector = null!; + beforeEach(function () { + removeWorkDir("test-openupm-cli"); + createWorkDir("test-openupm-cli", { manifest: true }); + createWorkDir("test-openupm-cli-wrong-json", { + manifest: true, + }); + fs.writeFileSync( + path.join( + getWorkDir("test-openupm-cli-wrong-json"), + "Packages/manifest.json" + ), + "wrong-json" + ); + [stdoutInspect, stderrInspect] = getInspects(); + }); + afterEach(function () { + removeWorkDir("test-openupm-cli"); + removeWorkDir("test-openupm-cli-wrong-json"); + stdoutInspect.restore(); + stderrInspect.restore(); + }); + it("loadManifest", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("test-openupm-cli") } }, + { checkPath: true } + ) + ).should.be.ok(); + const manifest = loadManifest(); + assert(manifest !== null); + manifest.should.be.deepEqual({ dependencies: {} }); + }); + it("no manifest file", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("path-not-exist") } }, + { checkPath: false } + ) + ).should.be.ok(); + const manifest = loadManifest(); + (manifest === null).should.be.ok(); + const [stdout] = getOutputs(stdoutInspect, stderrInspect); + stdout.includes("does not exist").should.be.ok(); + }); + it("wrong json content", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("test-openupm-cli-wrong-json") } }, + { checkPath: true } + ) + ).should.be.ok(); + const manifest = loadManifest(); + (manifest === null).should.be.ok(); + const [stdout] = getOutputs(stdoutInspect, stderrInspect); + stdout.includes("failed to parse").should.be.ok(); + }); + it("saveManifest", async function () { + ( + await parseEnv( + { _global: { chdir: getWorkDir("test-openupm-cli") } }, + { checkPath: true } + ) + ).should.be.ok(); + const manifest = loadManifest(); + assert(manifest !== null); + manifest.should.be.deepEqual({ dependencies: {} }); + manifest.dependencies["some-pack"] = "1.0.0"; + saveManifest(manifest).should.be.ok(); + const manifest2 = loadManifest(); + assert(manifest2 !== null); + manifest2.should.be.deepEqual(manifest); + }); +}); diff --git a/test/test-pgk-info.ts b/test/test-pgk-info.ts new file mode 100644 index 00000000..92e0bd75 --- /dev/null +++ b/test/test-pgk-info.ts @@ -0,0 +1,14 @@ +import { tryGetLatestVersion } from "../src/utils/pkg-info"; +import assert from "assert"; +import "should"; +import { describe } from "mocha"; + +describe("pkg-info", function () { + describe("tryGetLatestVersion", function () { + it("from dist-tags", async function () { + const version = tryGetLatestVersion({ "dist-tags": { latest: "1.0.0" } }); + assert(version !== undefined); + version.should.equal("1.0.0"); + }); + }); +}); diff --git a/test/test-pkg-name.ts b/test/test-pkg-name.ts index 3adb7006..592efd26 100644 --- a/test/test-pkg-name.ts +++ b/test/test-pkg-name.ts @@ -1,6 +1,6 @@ import "assert"; import "should"; -import { splitPkgName } from "../src/utils/pkg-name"; +import { isInternalPackage, splitPkgName } from "../src/utils/pkg-name"; describe("pkg-name.ts", function () { describe("splitPkgName", function () { @@ -41,4 +41,15 @@ describe("pkg-name.ts", function () { }); }); }); + 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 new file mode 100644 index 00000000..280ac205 --- /dev/null +++ b/test/test-registry-client.ts @@ -0,0 +1,46 @@ +import "assert"; +import nock from "nock"; +import "should"; + +import { nockDown, nockUp } from "./utils"; +import assert from "assert"; +import { parseEnv } from "../src/utils/env"; +import { fetchPackageInfo } from "../src/registry-client"; + +describe("registry-client", function () { + describe("fetchPackageInfo", function () { + beforeEach(function () { + nockUp(); + }); + afterEach(function () { + nockDown(); + }); + it("simple", async function () { + ( + await parseEnv( + { _global: { registry: "http://example.com" } }, + { checkPath: false } + ) + ).should.be.ok(); + const pkgInfoRemote = { name: "com.littlebigfun.addressable-importer" }; + nock("http://example.com") + .get("/package-a") + .reply(200, pkgInfoRemote, { "Content-Type": "application/json" }); + const info = await fetchPackageInfo("package-a"); + assert(info !== undefined); + info.should.deepEqual(pkgInfoRemote); + }); + it("404", async function () { + ( + await parseEnv( + { _global: { registry: "http://example.com" } }, + { checkPath: false } + ) + ).should.be.ok(); + + nock("http://example.com").get("/package-a").reply(404); + const info = await fetchPackageInfo("package-a"); + (info === undefined).should.be.ok(); + }); + }); +});