diff --git a/src/cmd-add.ts b/src/cmd-add.ts index 1380f34f..24cf4119 100644 --- a/src/cmd-add.ts +++ b/src/cmd-add.ts @@ -1,14 +1,13 @@ import log from "./logger"; import url from "url"; 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 { tryGetLatestVersion } from "./types/pkg-info"; +import { loadManifest, saveManifest } from "./utils/pkg-manifest-io"; import { env, parseEnv } from "./utils/env"; import { compareEditorVersion, tryParseEditorVersion, -} from "./utils/editor-version"; +} from "./types/editor-version"; import { fetchPackageDependencies, fetchPackageInfo } from "./registry-client"; import { DomainName, isDomainName } from "./types/domain-name"; import { SemanticVersion } from "./types/semantic-version"; @@ -17,12 +16,19 @@ import { PackageReference, splitPackageReference, } from "./types/package-reference"; +import { scopedRegistry } from "./types/scoped-registry"; +import { + addDependency, + addScopedRegistry, + addTestable, + tryGetScopedRegistryByUrl, +} from "./types/pkg-manifest"; +import { CmdOptions } from "./types/options"; -export type AddOptions = { +export type AddOptions = CmdOptions<{ test?: boolean; force?: boolean; - _global: GlobalOptions; -}; +}>; type ResultCode = 0 | 1; @@ -76,10 +82,6 @@ const _add = async function ({ // load manifest const manifest = loadManifest(); if (manifest === null) return { code: 1, dirty }; - // ensure manifest.dependencies - if (!manifest.dependencies) { - manifest.dependencies = {}; - } // packages that added to scope registry const pkgsInScope: DomainName[] = []; if (version === undefined || !isPackageUrl(version)) { @@ -196,7 +198,7 @@ const _add = async function ({ } // add to dependencies const oldVersion = manifest.dependencies[name]; - manifest.dependencies[name] = version; + addDependency(manifest, name, version); if (!oldVersion) { // Log the added package log.notice("manifest", `added ${packageReference(name, version)}`); @@ -215,22 +217,14 @@ const _add = async function ({ manifest.scopedRegistries = []; dirty = true; } - const filterEntry = (x: ScopedRegistry): boolean => { - let addr = x.url || ""; - if (addr.endsWith("/")) addr = addr.slice(0, -1); - return addr == env.registry; - }; - if (manifest.scopedRegistries.filter(filterEntry).length <= 0) { + let entry = tryGetScopedRegistryByUrl(manifest, env.registry); + if (entry === null) { const name = url.parse(env.registry).hostname; if (name === null) throw new Error("Could not resolve registry name"); - manifest.scopedRegistries.push({ - name, - url: env.registry, - scopes: [], - }); + entry = scopedRegistry(name, env.registry); + addScopedRegistry(manifest, entry); dirty = true; } - const entry = manifest.scopedRegistries.filter(filterEntry)[0]; // apply pkgsInScope const scopesSet = new Set(entry.scopes || []); if (isDomainName(env.namespace)) pkgsInScope.push(env.namespace); @@ -242,14 +236,7 @@ const _add = async function ({ }); entry.scopes = Array.from(scopesSet).sort(); } - if (testables) { - if (!manifest.testables) { - manifest.testables = []; - } - if (manifest.testables.indexOf(name) === -1) { - manifest.testables.push(name); - } - } + if (testables) addTestable(manifest, name); // save manifest if (dirty) { if (!saveManifest(manifest)) return { code: 1, dirty }; diff --git a/src/cmd-deps.ts b/src/cmd-deps.ts index 0ab8fd37..660031ae 100644 --- a/src/cmd-deps.ts +++ b/src/cmd-deps.ts @@ -1,5 +1,4 @@ import log from "./logger"; -import { GlobalOptions } from "./types/global"; import { parseEnv } from "./utils/env"; import { fetchPackageDependencies } from "./registry-client"; import { DomainName } from "./types/domain-name"; @@ -10,11 +9,11 @@ import { splitPackageReference, VersionReference, } from "./types/package-reference"; +import { CmdOptions } from "./types/options"; -export type DepsOptions = { +export type DepsOptions = CmdOptions<{ deep?: boolean; - _global: GlobalOptions; -}; +}>; export const deps = async function ( pkg: PackageReference, diff --git a/src/cmd-login.ts b/src/cmd-login.ts index fbb010d3..f0ff06fa 100644 --- a/src/cmd-login.ts +++ b/src/cmd-login.ts @@ -1,52 +1,48 @@ import fs from "fs"; import path from "path"; import _ from "lodash"; -import promptly from "promptly"; import { assertIsNpmClientError, getNpmClient } from "./registry-client"; - import log from "./logger"; - -import { GlobalOptions } from "./types/global"; import { getUpmConfigDir, loadUpmConfig, saveUpmConfig, -} from "./utils/upm-config"; +} from "./utils/upm-config-io"; import { parseEnv } from "./utils/env"; +import { encodeBasicAuth } from "./types/upm-config"; +import { Base64 } from "./types/base64"; +import { RegistryUrl, removeTrailingSlash } from "./types/registry-url"; import { - RegistryUrl, - registryUrl, - removeTrailingSlash, -} from "./types/registry-url"; + promptEmail, + promptPassword, + promptRegistryUrl, + promptUsername, +} from "./utils/prompts"; +import { CmdOptions } from "./types/options"; -export type LoginOptions = { +export type LoginOptions = CmdOptions<{ username?: string; password?: string; email?: string; basicAuth?: boolean; alwaysAuth?: boolean; - _global: GlobalOptions; -}; +}>; export const login = async function (options: LoginOptions) { // parse env const envOk = await parseEnv(options, { checkPath: false }); if (!envOk) return 1; // query parameters - if (!options.username) options.username = await promptly.prompt("Username: "); - if (!options.password) - options.password = await promptly.password("Password: "); - if (!options.email) options.email = await promptly.prompt("Email: "); + if (!options.username) options.username = await promptUsername(); + if (!options.password) options.password = await promptPassword(); + if (!options.email) options.email = await promptEmail(); if (!options._global.registry) - options._global.registry = (await promptly.prompt("Registry: ", { - validator: [registryUrl], - })) as RegistryUrl; + options._global.registry = await promptRegistryUrl(); let token: string | null = null; - let _auth: string | null = null; + let _auth: Base64 | null = null; if (options.basicAuth) { // basic auth - const userPass = `${options.username}:${options.password}`; - _auth = Buffer.from(userPass).toString("base64"); + _auth = encodeBasicAuth(options.username, options.password); } else { // npm login const result = await npmLogin({ @@ -205,7 +201,7 @@ const writeUnityToken = async function ({ registry, token, }: { - _auth: string | null; + _auth: Base64 | null; alwaysAuth: boolean; basicAuth: boolean; email: string; diff --git a/src/cmd-remove.ts b/src/cmd-remove.ts index b6c93c93..ac24679e 100644 --- a/src/cmd-remove.ts +++ b/src/cmd-remove.ts @@ -1,6 +1,5 @@ import log from "./logger"; -import { GlobalOptions, ScopedRegistry } from "./types/global"; -import { loadManifest, saveManifest } from "./utils/manifest"; +import { loadManifest, saveManifest } from "./utils/pkg-manifest-io"; import { env, parseEnv } from "./utils/env"; import { isDomainName } from "./types/domain-name"; import { @@ -8,10 +7,14 @@ import { PackageReference, splitPackageReference, } from "./types/package-reference"; +import { hasScope, removeScope } from "./types/scoped-registry"; +import { + removeDependency, + tryGetScopedRegistryByUrl, +} from "./types/pkg-manifest"; +import { CmdOptions } from "./types/options"; -export type RemoveOptions = { - _global: GlobalOptions; -}; +export type RemoveOptions = CmdOptions; export const remove = async function ( pkgs: PackageReference[] | PackageReference, @@ -53,33 +56,21 @@ const _remove = async function (pkg: PackageReference) { if (manifest === null) return { code: 1, dirty }; // not found array const pkgsNotFound = []; - // remove from dependencies - if (manifest.dependencies) { - version = manifest.dependencies[name]; - if (version) { - log.notice("manifest", `removed ${packageReference(name, version)}`); - delete manifest.dependencies[name]; + version = manifest.dependencies[name]; + if (version) { + log.notice("manifest", `removed ${packageReference(name, version)}`); + removeDependency(manifest, name); + dirty = true; + } else pkgsNotFound.push(pkg); + + const entry = tryGetScopedRegistryByUrl(manifest, env.registry); + if (entry !== null) { + if (hasScope(entry, name)) { + removeScope(entry, name); + const scopesSet = new Set(entry.scopes); + if (isDomainName(env.namespace)) scopesSet.add(env.namespace); + entry.scopes = Array.from(scopesSet).sort(); dirty = true; - } else pkgsNotFound.push(pkg); - } - // remove from scopedRegistries - if (manifest.scopedRegistries) { - const filterEntry = (x: ScopedRegistry) => { - let url = x.url || ""; - if (url.endsWith("/")) url = url.slice(0, -1); - return url == env.registry; - }; - const entires = manifest.scopedRegistries.filter(filterEntry); - if (entires.length > 0) { - const entry = entires[0]; - const index = entry.scopes.indexOf(name); - if (index > -1) { - entry.scopes.splice(index, 1); - const scopesSet = new Set(entry.scopes); - if (isDomainName(env.namespace)) scopesSet.add(env.namespace); - entry.scopes = Array.from(scopesSet).sort(); - dirty = true; - } } } // save manifest diff --git a/src/cmd-search.ts b/src/cmd-search.ts index e1d4a4ec..9890cab3 100644 --- a/src/cmd-search.ts +++ b/src/cmd-search.ts @@ -5,20 +5,18 @@ import log from "./logger"; import { is404Error, isHttpError } from "./utils/error-type-guards"; import * as os from "os"; import assert from "assert"; -import { GlobalOptions, PkgInfo } from "./types/global"; -import { tryGetLatestVersion } from "./utils/pkg-info"; +import { PkgInfo, tryGetLatestVersion } from "./types/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"; +import { CmdOptions } from "./types/options"; type DateString = string; type TableRow = [DomainName, SemanticVersion, DateString, ""]; -export type SearchOptions = { - _global: GlobalOptions; -}; +export type SearchOptions = CmdOptions; export type SearchedPkgInfo = Omit & { versions: Record; diff --git a/src/cmd-view.ts b/src/cmd-view.ts index 0208cf66..9c7f7dd8 100644 --- a/src/cmd-view.ts +++ b/src/cmd-view.ts @@ -1,8 +1,7 @@ import chalk from "chalk"; import log from "./logger"; import assert from "assert"; -import { GlobalOptions, PkgInfo } from "./types/global"; -import { tryGetLatestVersion } from "./utils/pkg-info"; +import { PkgInfo, tryGetLatestVersion } from "./types/pkg-info"; import { env, parseEnv } from "./utils/env"; import { fetchPackageInfo } from "./registry-client"; import { DomainName } from "./types/domain-name"; @@ -11,10 +10,9 @@ import { PackageReference, splitPackageReference, } from "./types/package-reference"; +import { CmdOptions } from "./types/options"; -export type ViewOptions = { - _global: GlobalOptions; -}; +export type ViewOptions = CmdOptions; export const view = async function ( pkg: PackageReference, diff --git a/src/registry-client.ts b/src/registry-client.ts index 99d20e32..5cbf9208 100644 --- a/src/registry-client.ts +++ b/src/registry-client.ts @@ -8,10 +8,9 @@ import RegClient, { import log from "./logger"; import request from "request"; import assert, { AssertionError } from "assert"; -import { Dependency, NameVersionPair, PkgInfo } from "./types/global"; import { env } from "./utils/env"; import _ from "lodash"; -import { tryGetLatestVersion } from "./utils/pkg-info"; +import { PkgInfo, tryGetLatestVersion } from "./types/pkg-info"; import { DomainName, isInternalPackage } from "./types/domain-name"; import { SemanticVersion } from "./types/semantic-version"; import { packageReference } from "./types/package-reference"; @@ -44,6 +43,21 @@ export class NpmClientError extends Error { } } +export type Dependency = { + name: DomainName; + version?: SemanticVersion; + upstream: boolean; + self: boolean; + internal: boolean; + reason: string | null; + resolved?: boolean; +}; + +type NameVersionPair = { + name: DomainName; + version: SemanticVersion | "latest" | undefined; +}; + export function assertIsNpmClientError( x: unknown ): asserts x is NpmClientError { @@ -103,6 +117,7 @@ export const fetchPackageInfo = async function ( // eslint-disable-next-line no-empty } catch (err) {} }; + /* Fetch package [valid dependencies, invalid dependencies] with a structure of [ { diff --git a/src/types/another-npm-registry-client.d.ts b/src/types/another-npm-registry-client.d.ts index b74b52ad..6eff650d 100644 --- a/src/types/another-npm-registry-client.d.ts +++ b/src/types/another-npm-registry-client.d.ts @@ -1,5 +1,6 @@ import { Response } from "request"; -import { PkgInfo } from "./global"; + +import { PkgInfo } from "./pkg-info"; declare module "another-npm-registry-client" { export type NpmAuth = diff --git a/src/types/base64.ts b/src/types/base64.ts new file mode 100644 index 00000000..4e9e8721 --- /dev/null +++ b/src/types/base64.ts @@ -0,0 +1,20 @@ +import { Brand } from "ts-brand"; + +export type Base64 = Brand; + +/** + * Encodes a string using base64 + * @param s The string + */ +export function encodeBase64(s: string): Base64 { + return Buffer.from(s).toString("base64") as Base64; +} + +/** + * Decodes a base64 string + * @param base64 The string + */ +export function decodeBase64(base64: Base64): string { + const buffer = Buffer.from(base64, "base64"); + return buffer.toString("utf-8"); +} diff --git a/src/types/contact.ts b/src/types/contact.ts new file mode 100644 index 00000000..4dfc7a09 --- /dev/null +++ b/src/types/contact.ts @@ -0,0 +1,5 @@ +export type Contact = { + name: string; + email?: string; + url?: string; +}; diff --git a/src/utils/editor-version.ts b/src/types/editor-version.ts similarity index 75% rename from src/utils/editor-version.ts rename to src/types/editor-version.ts index aed12f0a..29f2e73d 100644 --- a/src/utils/editor-version.ts +++ b/src/types/editor-version.ts @@ -1,4 +1,40 @@ -import { EditorVersion } from "../types/global"; +/** + * Describes a version of a Unity editor. Mostly this follows calendar-versioning, + * with some extra rules for chinese releases. + * @see https://calver.org/ + */ +export type EditorVersion = { + /** + * The major version. This is the release year. + */ + major: number; + /** + * The minor version. This is usually a number from 1 to 3 + */ + minor: number; + /** + * An optional patch. + */ + patch?: number; + /** + * A flag describing a specific release + */ + flag?: "a" | "b" | "f" | "c"; + flagValue?: 0 | 1 | 2; + /** + * A specific build + */ + build?: number; + /** + * A flag describing a specific locale build + */ + loc?: string; + locValue?: number; + /** + * The specific build for a locale + */ + locBuild?: number; +}; /** * Compares two editor versions for ordering diff --git a/src/types/global.ts b/src/types/global.ts deleted file mode 100644 index 85d2f82c..00000000 --- a/src/types/global.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { NpmAuth } from "another-npm-registry-client"; -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 EditorVersion = { - major: number; - minor: number; - patch?: number; - flag?: "a" | "b" | "f" | "c"; - flagValue?: 0 | 1 | 2; - build?: number; - loc?: string; - locValue?: number; - locBuild?: number; -}; - -export type Env = { - cwd: string; - color: boolean; - systemUser: boolean; - wsl: boolean; - npmAuth?: Record; - auth: Record; - upstream: boolean; - upstreamRegistry: RegistryUrl; - registry: RegistryUrl; - namespace: DomainName | IpAddress; - editorVersion: string | null; - region: Region; - manifestPath: string; -}; - -export type Dist = { - tarball: string; - shasum: string; - integrity: string; -}; - -export type Contact = { - name: string; - email?: string; - url?: string; -}; - -export type PkgVersionInfo = { - _id?: PackageId; - _nodeVersion?: string; - _npmVersion?: string; - _rev?: string; - name: string; - version: SemanticVersion; - unity?: string; - unityRelease?: string; - dependencies?: Record; - license?: string; - displayName?: string; - description?: string; - keywords?: string[]; - homepage?: string; - category?: string; - gitHead?: string; - readmeFilename?: string; - author?: Contact; - contributors?: Contact[]; - dist?: Dist; -}; - -export type PkgInfo = { - name: DomainName; - _id?: DomainName; - _rev?: string; - _attachments?: Record; - readme?: string; - versions: Record; - "dist-tags"?: { latest?: SemanticVersion }; - version?: SemanticVersion; - description?: string; - keywords?: string[]; - time: { - [key: SemanticVersion]: string; - created?: string; - modified?: string; - }; - date?: Date; - users?: Record; -}; - -export type NameVersionPair = { - name: DomainName; - version: SemanticVersion | "latest" | undefined; -}; - -export type Dependency = { - name: DomainName; - version?: SemanticVersion; - upstream: boolean; - self: boolean; - internal: boolean; - reason: string | null; - resolved?: boolean; -}; - -export type ScopedRegistry = { - name: string; - url: RegistryUrl; - scopes: DomainName[]; -}; - -export type PkgManifest = { - dependencies: Record; - scopedRegistries?: ScopedRegistry[]; - testables?: string[]; -}; - -export type GlobalOptions = { - registry?: string; - verbose?: boolean; - color?: boolean; - upstream?: boolean; - cn?: boolean; - systemUser?: boolean; - wsl?: boolean; - chdir?: string; -}; - -export type UpmAuth = { - email: string; - alwaysAuth: boolean; -} & ({ token: string } | { _auth: string }); - -export type UPMConfig = { - npmAuth?: Record; -}; diff --git a/src/types/options.ts b/src/types/options.ts new file mode 100644 index 00000000..52e3d066 --- /dev/null +++ b/src/types/options.ts @@ -0,0 +1,47 @@ +/** + * Options which are shared between commands + */ +type GlobalOptions = { + /** + * Override package registry to use + */ + registry?: string; + /** + * Whether to print logs + */ + verbose?: boolean; + /** + * Whether to use color in the console + */ + color?: boolean; + /** + * Whether to fall back to the Unity registry + */ + upstream?: boolean; + /** + * Whether to run commands for the chinese locale + */ + cn?: boolean; + /** + * Whether to authenticate as a Windows system-user + */ + systemUser?: boolean; + /** + * Whether WSL should be treated as Windows + */ + wsl?: boolean; + /** + * Override working directory + */ + chdir?: string; +}; + +/** + * Command-options. Extends the given record with a _global property + * containing {@link GlobalOptions}. + */ +export type CmdOptions< + TOptions extends Record = Record +> = TOptions & { + _global: GlobalOptions; +}; diff --git a/src/types/pkg-info.ts b/src/types/pkg-info.ts new file mode 100644 index 00000000..57bba4e4 --- /dev/null +++ b/src/types/pkg-info.ts @@ -0,0 +1,70 @@ +import { SemanticVersion } from "./semantic-version"; +import { DomainName } from "./domain-name"; +import { PkgVersionInfo } from "./pkg-version-info"; + +/** + * Describes a package + */ +export type PkgInfo = { + /** + * The packages name + */ + name: DomainName; + /** + * Same as {@link name} + */ + _id?: DomainName; + _rev?: string; + _attachments?: Record; + readme?: string; + /** + * The packages versions, organized by their version + */ + versions: Record; + /** + * Dist-tags. Only includes information about the latest version + */ + "dist-tags"?: { latest?: SemanticVersion }; + /** + * May contain the latest version. Legacy property, use {@link dist-tags} instead + */ + version?: SemanticVersion; + /** + * Short description for the package + */ + description?: string; + /** + * Package keywords + */ + keywords?: string[]; + /** + * Information about package and version creation/modification times + */ + time: { + [key: SemanticVersion]: string; + created?: string; + modified?: string; + }; + date?: Date; + users?: Record; +}; + +const hasLatestDistTag = ( + pkgInfo: Partial +): pkgInfo is Partial & { + "dist-tags": { latest: SemanticVersion }; +} => { + 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: { + "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/types/pkg-manifest.ts b/src/types/pkg-manifest.ts new file mode 100644 index 00000000..54d99295 --- /dev/null +++ b/src/types/pkg-manifest.ts @@ -0,0 +1,98 @@ +import { DomainName } from "./domain-name"; +import { SemanticVersion } from "./semantic-version"; +import { PackageUrl } from "./package-url"; +import { ScopedRegistry } from "./scoped-registry"; +import { RegistryUrl, removeTrailingSlash } from "./registry-url"; + +/** + * The content of the package-manifest (manifest.json) of a Unity project + * @see https://docs.unity3d.com/Manual/upm-manifestPrj.html + */ +export type PkgManifest = { + /** + * Direct dependencies, keyed by their name. Version can be either a + * semantic version or package-url + */ + dependencies: Record; + /** + * Scoped-registries for this project + */ + scopedRegistries?: ScopedRegistry[]; + /** + * Testable package-names + */ + testables?: DomainName[]; +}; + +/** + * Constructs an empty package-manifest + */ +export function emptyPackageManifest(): PkgManifest { + return { dependencies: {} }; +} + +/** + * Adds a dependency to the manifest. If a dependency with that name already + * exists, the version is overwritten + * @param manifest The manifest + * @param name The dependency name + * @param version The dependency version or url + */ +export function addDependency( + manifest: PkgManifest, + name: DomainName, + version: SemanticVersion | PackageUrl +) { + manifest.dependencies[name] = version; +} + +/** + * Removes a dependency from a manifest + * @param manifest The manifest + * @param name The dependency name + */ +export function removeDependency(manifest: PkgManifest, name: DomainName) { + delete manifest.dependencies[name]; +} + +/** + * Attempts to get a scoped-registry with a specific url from the manifest + * @param manifest The manifest + * @param url The url + * @returns The scoped-registry or null if not found + */ +export function tryGetScopedRegistryByUrl( + manifest: PkgManifest, + url: RegistryUrl +): ScopedRegistry | null { + function hasCorrectUrl(registry: ScopedRegistry): boolean { + return removeTrailingSlash(registry.url) === url; + } + + return manifest.scopedRegistries?.find(hasCorrectUrl) ?? null; +} + +/** + * Adds a scoped-registry to the manifest. + * NOTE: Does not check if a scoped-registry with the same name already exists + * @param manifest The manifest + * @param scopedRegistry The scoped-registry + */ +export function addScopedRegistry( + manifest: PkgManifest, + scopedRegistry: ScopedRegistry +) { + if (manifest.scopedRegistries === undefined) manifest.scopedRegistries = []; + manifest.scopedRegistries.push(scopedRegistry); +} + +/** + * Adds a testable to the manifest, if it is not already added + * @param manifest The manifest + * @param name The testable name + */ +export function addTestable(manifest: PkgManifest, name: DomainName) { + if (!manifest.testables) manifest.testables = []; + if (manifest.testables.indexOf(name) === -1) manifest.testables.push(name); + manifest.testables.sort(); +} diff --git a/src/types/pkg-version-info.ts b/src/types/pkg-version-info.ts new file mode 100644 index 00000000..2a806739 --- /dev/null +++ b/src/types/pkg-version-info.ts @@ -0,0 +1,81 @@ +import { PackageId } from "./package-id"; +import { SemanticVersion } from "./semantic-version"; +import { DomainName } from "./domain-name"; +import { Contact } from "./contact"; + +/** + * Distribution information + */ +type Dist = { + tarball: string; + shasum: string; + integrity: string; +}; + +/** + * Contains information about a specific version of a package. This is based on + * the information contained inside a Unity package manifest, with some + * additions. + * @see https://docs.unity3d.com/Manual/upm-manifestPkg.html + */ +export type PkgVersionInfo = { + /** + * Same as {@link name} + */ + _id?: PackageId; + _nodeVersion?: string; + _npmVersion?: string; + _rev?: string; + /** + * The package name + */ + name: string; + /** + * The version + */ + version: SemanticVersion; + /** + * Indicates the lowest Unity version the package is compatible with. + * The expected format is ".". + * @example 2020.2 + */ + unity?: `${number}.${number}`; + /** + * Part of a Unity version indicating the specific release of Unity that the + * package is compatible with. + * The expected format is "". + * @example 0b4 + */ + unityRelease?: string; + /** + * A map of package dependencies. Keys are package names, and values are + * specific versions. + */ + dependencies?: Record; + /** + * Identifier for an OSS license using the SPDX identifier format. + */ + license?: string; + /** + * A user-friendly name to appear in the Unity Editor. + */ + displayName?: string; + /** + * A brief description of the package. + */ + description?: string; + /** + * An array of keywords used by the Package Manager search APIs. + */ + keywords?: string[]; + homepage?: string; + category?: string; + gitHead?: string; + readmeFilename?: string; + /** + * The author of the package. + */ + author?: Contact; + contributors?: Contact[]; + dist?: Dist; +}; diff --git a/src/types/scoped-registry.ts b/src/types/scoped-registry.ts new file mode 100644 index 00000000..bd5ccd81 --- /dev/null +++ b/src/types/scoped-registry.ts @@ -0,0 +1,65 @@ +import { RegistryUrl } from "./registry-url"; +import { DomainName } from "./domain-name"; + +/** + * Contains information about a scoped registry + * @see https://docs.unity3d.com/Manual/upm-scoped.html + */ +export type ScopedRegistry = { + /** + * The scope name as it appears in the user interface + */ + name: string; + /** + * The url to the npm-compatible registry server + */ + url: RegistryUrl; + /** + * Array of scopes that you can map to a package name, either as an exact match + * on the package name, or as a namespace + */ + scopes: DomainName[]; +}; + +/** + * Constructs a scoped registry + * @param name The name + * @param url The url + * @param scopes The scopes. If not specified defaults to empty array + */ +export function scopedRegistry( + name: string, + url: RegistryUrl, + scopes?: DomainName[] +): ScopedRegistry { + return { name, url, scopes: scopes ?? [] }; +} + +/** + * Adds a scope to a registry. The list will be sorted alphabetically + * @param registry The registry + * @param scope The scope + */ +export function addScope(registry: ScopedRegistry, scope: DomainName) { + registry.scopes.push(scope); + registry.scopes.sort(); +} + +/** + * Checks if a registry has a specific scope + * @param registry The registry + * @param scope The scope + */ +export function hasScope(registry: ScopedRegistry, scope: DomainName): boolean { + return registry.scopes.includes(scope); +} + +/** + * Removes a scope from a registry + * @param registry The registry + * @param scope The scope + */ +export function removeScope(registry: ScopedRegistry, scope: DomainName) { + const index = registry.scopes.indexOf(scope); + if (index >= 0) registry.scopes.splice(index, 1); +} diff --git a/src/types/upm-config.ts b/src/types/upm-config.ts new file mode 100644 index 00000000..dd4e0e4c --- /dev/null +++ b/src/types/upm-config.ts @@ -0,0 +1,97 @@ +import { trySplitAtFirstOccurrenceOf } from "../utils/string-utils"; +import { Base64, decodeBase64, encodeBase64 } from "./base64"; +import { RegistryUrl } from "./registry-url"; + +/** + * Authentication information that is shared between different authentication methods + */ +type AuthBase = { + /** + * The email to use + */ + email: string; + /** + * Whether to always authenticate + */ + alwaysAuth?: boolean; +}; + +/** + * Authenticates using encoded username and password + */ +export type BasicAuth = AuthBase & { + /** + * Base64 encoded username and password to authenticate with + */ + _auth: Base64; +}; + +/** + * Authenticates using token + */ +export type TokenAuth = AuthBase & { + /** + * A token to authenticate with + */ + token: string; +}; + +/** + * Authentication information for a registry + */ +export type UpmAuth = BasicAuth | TokenAuth; + +/** + * Content of .upmconfig.toml. Used to authenticate with package registries + * @see https://medium.com/openupm/how-to-authenticate-with-a-upm-scoped-registry-using-cli-afc29c13a2f8 + */ +export type UPMConfig = { + /** + * Authentication information organized by the registry they should be used on + */ + npmAuth?: Record; +}; + +/** + * Checks if an auth-object uses basic authentication + * @param auth The auth-object + */ +export function isBasicAuth(auth: UpmAuth): auth is BasicAuth { + return "_auth" in auth; +} +/** + * Checks if an auth-object uses token authentication + * @param auth The auth-object + */ +export function isTokenAuth(auth: UpmAuth): auth is TokenAuth { + return "token" in auth; +} + +/** + * Encodes a username and password using base64 + * @param username The username + * @param password The password + */ +export function encodeBasicAuth(username: string, password: string): Base64 { + return encodeBase64(`${username}:${password}`); +} + +/** + * Decodes a base64 encoded username and password + * @param base64 The base64 string + * @throws Error if the string cannot be decoded + */ +export function decodeBasicAuth(base64: Base64): [string, string] { + const text = decodeBase64(base64); + const [username, password] = trySplitAtFirstOccurrenceOf(text, ":"); + if (password === undefined) throw new Error("Base64 had invalid format"); + return [username, password]; +} + +/** + * Checks if this auth-object should always authenticate + * @param auth The auth-object + */ +export function shouldAlwaysAuth(auth: UpmAuth): boolean { + return auth.alwaysAuth || false; +} diff --git a/src/utils/env.ts b/src/utils/env.ts index bca13744..46128ae1 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -1,24 +1,55 @@ -import { Env, GlobalOptions } from "../types/global"; import log from "../logger"; import chalk from "chalk"; -import { loadUpmConfig } from "./upm-config"; +import { loadUpmConfig } from "./upm-config-io"; 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 { IpAddress, isIpAddress } from "../types/ip-address"; +import { + DomainName, + namespaceFor, + openUpmReverseDomainName, +} from "../types/domain-name"; import { coerceRegistryUrl, RegistryUrl, registryUrl, } from "../types/registry-url"; import url from "url"; +import { + decodeBasicAuth, + isBasicAuth, + isTokenAuth, + shouldAlwaysAuth, + UpmAuth, +} from "../types/upm-config"; +import { encodeBase64 } from "../types/base64"; +import { NpmAuth } from "another-npm-registry-client"; +import { CmdOptions } from "../types/options"; + +type Region = "us" | "cn"; + +export type Env = { + cwd: string; + color: boolean; + systemUser: boolean; + wsl: boolean; + npmAuth?: Record; + auth: Record; + upstream: boolean; + upstreamRegistry: RegistryUrl; + registry: RegistryUrl; + namespace: DomainName | IpAddress; + editorVersion: string | null; + region: Region; + manifestPath: string; +}; export const env: Env = {}; // Parse env export const parseEnv = async function ( - options: { _global: GlobalOptions } & Record, + options: CmdOptions, { checkPath }: { checkPath: unknown } ) { // set defaults @@ -72,20 +103,18 @@ export const parseEnv = async function ( if (env.npmAuth !== undefined) { (Object.keys(env.npmAuth) as RegistryUrl[]).forEach((reg) => { const regAuth = env.npmAuth![reg]; - if ("token" in regAuth) { + if (isTokenAuth(regAuth)) { env.auth[reg] = { token: regAuth.token, - alwaysAuth: regAuth.alwaysAuth || false, + alwaysAuth: shouldAlwaysAuth(regAuth), }; - } else if ("_auth" in regAuth) { - const buf = Buffer.from(regAuth._auth, "base64"); - const text = buf.toString("utf-8"); - const [username, password] = text.split(":", 2); + } else if (isBasicAuth(regAuth)) { + const [username, password] = decodeBasicAuth(regAuth._auth); env.auth[reg] = { username, - password: Buffer.from(password).toString("base64"), + password: encodeBase64(password), email: regAuth.email, - alwaysAuth: regAuth.alwaysAuth || false, + alwaysAuth: shouldAlwaysAuth(regAuth), }; } else { log.warn( diff --git a/src/utils/pkg-info.ts b/src/utils/pkg-info.ts deleted file mode 100644 index 62616842..00000000 --- a/src/utils/pkg-info.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PkgInfo } from "../types/global"; -import { SemanticVersion } from "../types/semantic-version"; - -const hasLatestDistTag = ( - pkgInfo: Partial -): pkgInfo is Partial & { - "dist-tags": { latest: SemanticVersion }; -} => { - 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: { - "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/manifest.ts b/src/utils/pkg-manifest-io.ts similarity index 95% rename from src/utils/manifest.ts rename to src/utils/pkg-manifest-io.ts index 25fa3ed8..8941ab61 100644 --- a/src/utils/manifest.ts +++ b/src/utils/pkg-manifest-io.ts @@ -1,8 +1,8 @@ -import { PkgManifest } from "../types/global"; import fs from "fs"; import { assertIsError } from "./error-type-guards"; import log from "../logger"; import { env } from "./env"; +import { PkgManifest } from "../types/pkg-manifest"; /** * Attempts to load the manifest from the path specified in env diff --git a/src/utils/prompts.ts b/src/utils/prompts.ts new file mode 100644 index 00000000..b966f2e2 --- /dev/null +++ b/src/utils/prompts.ts @@ -0,0 +1,20 @@ +import promptly from "promptly"; +import { registryUrl, RegistryUrl } from "../types/registry-url"; + +export function promptUsername(): Promise { + return promptly.prompt("Username: "); +} + +export function promptPassword(): Promise { + return promptly.password("Password: "); +} + +export function promptEmail(): Promise { + return promptly.prompt("Email: "); +} + +export function promptRegistryUrl(): Promise { + return promptly.prompt("Registry: ", { + validator: [registryUrl], + }) as Promise; +} diff --git a/src/utils/upm-config.ts b/src/utils/upm-config-io.ts similarity index 90% rename from src/utils/upm-config.ts rename to src/utils/upm-config-io.ts index 2cdfc2fe..49913f9f 100644 --- a/src/utils/upm-config.ts +++ b/src/utils/upm-config-io.ts @@ -1,4 +1,3 @@ -import { UPMConfig } from "../types/global"; import mkdirp from "mkdirp"; import path from "path"; import TOML from "@iarna/toml"; @@ -7,6 +6,9 @@ import log from "../logger"; import isWsl from "is-wsl"; import execute from "./process"; import { env } from "./env"; +import { UPMConfig } from "../types/upm-config"; + +const configFileName = ".upmconfig.toml"; /** * Gets the path to directory in which the upm config is stored @@ -52,7 +54,7 @@ export const loadUpmConfig = async ( configDir?: string ): Promise => { if (configDir === undefined) configDir = await getUpmConfigDir(); - const configPath = path.join(configDir, ".upmconfig.toml"); + const configPath = path.join(configDir, configFileName); if (fs.existsSync(configPath)) { const content = fs.readFileSync(configPath, "utf8"); const config = TOML.parse(content); @@ -68,7 +70,7 @@ export const loadUpmConfig = async ( 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 configPath = path.join(configDir, configFileName); const content = TOML.stringify(config); fs.writeFileSync(configPath, content, "utf8"); log.notice("config", "saved unity config at " + configPath); diff --git a/test/data-pkg-info.ts b/test/data-pkg-info.ts index 278089be..d18ebc1f 100644 --- a/test/data-pkg-info.ts +++ b/test/data-pkg-info.ts @@ -1,4 +1,3 @@ -import { PkgInfo, PkgVersionInfo } from "../src/types/global"; import assert from "assert"; import { DomainName, isDomainName } from "../src/types/domain-name"; import { @@ -6,6 +5,8 @@ import { SemanticVersion, } from "../src/types/semantic-version"; import { packageId } from "../src/types/package-id"; +import { PkgInfo } from "../src/types/pkg-info"; +import { PkgVersionInfo } from "../src/types/pkg-version-info"; /** * Builder class for {@link PkgVersionInfo} diff --git a/test/data-pkg-manifest.ts b/test/data-pkg-manifest.ts index ea6404fe..20151b8f 100644 --- a/test/data-pkg-manifest.ts +++ b/test/data-pkg-manifest.ts @@ -1,8 +1,14 @@ -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"; +import { addScope, scopedRegistry } from "../src/types/scoped-registry"; +import { + addDependency, + addTestable, + emptyPackageManifest, + PkgManifest, +} from "../src/types/pkg-manifest"; /** * Builder class for {@link PkgManifest} @@ -11,9 +17,7 @@ class PkgManifestBuilder { readonly manifest: PkgManifest; constructor() { - this.manifest = { - dependencies: {}, - }; + this.manifest = emptyPackageManifest(); } /** @@ -25,16 +29,13 @@ class PkgManifestBuilder { if (this.manifest.scopedRegistries === undefined) this.manifest.scopedRegistries = [ - { - name: "example.com", - scopes: [domainName("com.example")], - url: exampleRegistryUrl, - }, + scopedRegistry("example.com", exampleRegistryUrl, [ + domainName("com.example"), + ]), ]; const registry = this.manifest.scopedRegistries![0]; - registry.scopes = [name, ...registry.scopes]; - registry.scopes.sort(); + addScope(registry, name); return this; } @@ -45,9 +46,7 @@ class PkgManifestBuilder { */ 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(); + addTestable(this.manifest, name); return this; } @@ -68,7 +67,7 @@ class PkgManifestBuilder { assert(isSemanticVersion(version), `${version} is semantic version`); if (withScope) this.addScope(name); if (testable) this.addTestable(name); - this.manifest.dependencies[name] = version; + addDependency(this.manifest, name, version); return this; } } diff --git a/test/manifest-assertions.ts b/test/manifest-assertions.ts index 9cbbd2c6..b10794ba 100644 --- a/test/manifest-assertions.ts +++ b/test/manifest-assertions.ts @@ -1,9 +1,10 @@ -import { PkgManifest } from "../src/types/global"; -import { loadManifest } from "../src/utils/manifest"; +import { loadManifest } from "../src/utils/pkg-manifest-io"; 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"; +import { hasScope } from "../src/types/scoped-registry"; +import { PkgManifest } from "../src/types/pkg-manifest"; export function shouldHaveManifest(): PkgManifest { const manifest = loadManifest(); @@ -42,7 +43,7 @@ export function shouldHaveRegistryWithScopes( should(manifest.scopedRegistries).not.be.undefined(); manifest .scopedRegistries!.some((registry) => - scopes.every((scope) => registry.scopes.includes(scope)) + scopes.every((scope) => hasScope(registry, scope)) ) .should.be.true("At least one scope was missing"); } diff --git a/test/mock-registry.ts b/test/mock-registry.ts index bcddc0e9..f05e9dc9 100644 --- a/test/mock-registry.ts +++ b/test/mock-registry.ts @@ -1,9 +1,9 @@ -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"; +import { PkgInfo } from "../src/types/pkg-info"; export const unityRegistryUrl = registryUrl("https://packages.unity.com"); export const exampleRegistryUrl = registryUrl("http://example.com"); diff --git a/test/mock-work-dir.ts b/test/mock-work-dir.ts index acc8e39c..fd795ea5 100644 --- a/test/mock-work-dir.ts +++ b/test/mock-work-dir.ts @@ -1,8 +1,8 @@ import path from "path"; import os from "os"; -import { PkgManifest } from "../src/types/global"; import fse from "fs-extra"; import _ from "lodash"; +import { emptyPackageManifest, PkgManifest } from "../src/types/pkg-manifest"; export type ManifestCreationOptions = { manifest: boolean | PkgManifest; @@ -18,7 +18,7 @@ export const createWorkDir = function ( const workDir = getWorkDir(pathToTmp); fse.mkdirpSync(workDir); if (manifest) { - if (!_.isObjectLike(manifest)) manifest = { dependencies: {} }; + if (!_.isObjectLike(manifest)) manifest = emptyPackageManifest(); const manifestDir = path.join(workDir, "Packages"); fse.mkdirpSync(manifestDir); const data = JSON.stringify(manifest); diff --git a/test/test-cmd-add.ts b/test/test-cmd-add.ts index ab5910da..b58c690b 100644 --- a/test/test-cmd-add.ts +++ b/test/test-cmd-add.ts @@ -101,10 +101,14 @@ describe("cmd-add.ts", function () { (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")) + ( + pkg + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore 2020 is not a valid major.minor version, but this is on purpose for this test + ) => pkg.addVersion("1.0.0", (version) => version.set("unity", "2020")) ); const remotePkgInfoUp = buildPackageInfo(packageUp, (pkg) => pkg.addVersion("1.0.0") diff --git a/test/test-editor-version.ts b/test/test-editor-version.ts index 2fca64c2..82307a2f 100644 --- a/test/test-editor-version.ts +++ b/test/test-editor-version.ts @@ -2,7 +2,7 @@ import { describe } from "mocha"; import { compareEditorVersion, tryParseEditorVersion, -} from "../src/utils/editor-version"; +} from "../src/types/editor-version"; import "should"; import assert from "assert"; diff --git a/test/test-env.ts b/test/test-env.ts index 56622839..803d392e 100644 --- a/test/test-env.ts +++ b/test/test-env.ts @@ -97,7 +97,11 @@ describe("env", function () { it("custom registry with extra path", async function () { ( await parseEnv( - { _global: { registry: "https://registry.npmjs.org/some" } }, + { + _global: { + registry: "https://registry.npmjs.org/some", + }, + }, { checkPath: false } ) ).should.be.ok(); @@ -107,7 +111,11 @@ describe("env", function () { it("custom registry with extra path and splash", async function () { ( await parseEnv( - { _global: { registry: "https://registry.npmjs.org/some/" } }, + { + _global: { + registry: "https://registry.npmjs.org/some/", + }, + }, { checkPath: false } ) ).should.be.ok(); @@ -137,7 +145,9 @@ describe("env", function () { it("custom registry with ipv6+port", async function () { ( await parseEnv( - { _global: { registry: "http://[1:2:3:4:5:6:7:8]:4873" } }, + { + _global: { registry: "http://[1:2:3:4:5:6:7:8]:4873" }, + }, { checkPath: false } ) ).should.be.ok(); @@ -170,7 +180,12 @@ describe("env", function () { it("region cn with a custom registry", async function () { ( await parseEnv( - { _global: { cn: true, registry: "https://reg.custom-package.com" } }, + { + _global: { + cn: true, + registry: "https://reg.custom-package.com", + }, + }, { checkPath: false } ) ).should.be.ok(); diff --git a/test/test-pgk-info.ts b/test/test-pgk-info.ts index 1e4c5ac0..c0ab0214 100644 --- a/test/test-pgk-info.ts +++ b/test/test-pgk-info.ts @@ -1,4 +1,4 @@ -import { tryGetLatestVersion } from "../src/utils/pkg-info"; +import { tryGetLatestVersion } from "../src/types/pkg-info"; import "should"; import { describe } from "mocha"; import should from "should"; diff --git a/test/test-manifest.ts b/test/test-pkg-manifest-io.ts similarity index 91% rename from test/test-manifest.ts rename to test/test-pkg-manifest-io.ts index 8ab18eb4..2f020b8c 100644 --- a/test/test-manifest.ts +++ b/test/test-pkg-manifest-io.ts @@ -2,7 +2,7 @@ import { attachMockConsole, MockConsole } from "./mock-console"; import fs from "fs"; import "should"; import path from "path"; -import { saveManifest } from "../src/utils/manifest"; +import { saveManifest } from "../src/utils/pkg-manifest-io"; import { describe } from "mocha"; import { parseEnv } from "../src/utils/env"; import { createWorkDir, getWorkDir, removeWorkDir } from "./mock-work-dir"; @@ -13,8 +13,9 @@ import { } from "./manifest-assertions"; import { domainName } from "../src/types/domain-name"; import { semanticVersion } from "../src/types/semantic-version"; +import { addDependency } from "../src/types/pkg-manifest"; -describe("manifest", function () { +describe("pkg-manifest io", function () { let mockConsole: MockConsole = null!; beforeEach(function () { removeWorkDir("test-openupm-cli"); @@ -75,7 +76,7 @@ describe("manifest", function () { ).should.be.ok(); const manifest = shouldHaveManifest(); shouldNotHaveAnyDependencies(manifest); - manifest.dependencies[domainName("some-pack")] = semanticVersion("1.0.0"); + addDependency(manifest, 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-pkg-manifest.ts b/test/test-pkg-manifest.ts new file mode 100644 index 00000000..0ed6340a --- /dev/null +++ b/test/test-pkg-manifest.ts @@ -0,0 +1,71 @@ +import { describe } from "mocha"; +import { + addDependency, + addScopedRegistry, + addTestable, + emptyPackageManifest, + removeDependency, + tryGetScopedRegistryByUrl, +} from "../src/types/pkg-manifest"; +import { domainName } from "../src/types/domain-name"; +import { semanticVersion } from "../src/types/semantic-version"; +import should from "should"; +import { scopedRegistry } from "../src/types/scoped-registry"; +import { registryUrl } from "../src/types/registry-url"; + +describe("pkg-manifest", function () { + describe("dependency", function () { + it("should add dependency when adding first time", () => { + const manifest = emptyPackageManifest(); + addDependency(manifest, domainName("test"), semanticVersion("1.2.3")); + should(manifest.dependencies).deepEqual({ test: "1.2.3" }); + }); + it("should overwrite dependency when adding second time", () => { + const manifest = emptyPackageManifest(); + addDependency(manifest, domainName("test"), semanticVersion("1.2.3")); + addDependency(manifest, domainName("test"), semanticVersion("2.3.4")); + should(manifest.dependencies).deepEqual({ test: "2.3.4" }); + }); + it("should remove existing dependency", () => { + const manifest = emptyPackageManifest(); + addDependency(manifest, domainName("test"), semanticVersion("1.2.3")); + removeDependency(manifest, domainName("test")); + should(manifest.dependencies).deepEqual({}); + }); + it("should do nothing when dependency does not exist", () => { + const manifest = emptyPackageManifest(); + removeDependency(manifest, domainName("test")); + should(manifest.dependencies).deepEqual({}); + }); + }); + describe("scoped-registry", function () { + it("should should find scoped-registry with url if present", () => { + const manifest = emptyPackageManifest(); + const url = registryUrl("https://test.com"); + const expected = scopedRegistry("test", url); + addScopedRegistry(manifest, expected); + should(tryGetScopedRegistryByUrl(manifest, url)).be.deepEqual(expected); + }); + it("should should not find scoped-registry with incorrect url", () => { + const manifest = emptyPackageManifest(); + const url = registryUrl("https://test.com"); + const expected = scopedRegistry("test", registryUrl("https://test2.com")); + addScopedRegistry(manifest, expected); + should(tryGetScopedRegistryByUrl(manifest, url)).be.null(); + }); + }); + describe("testables", function () { + it("should not add testables which already exist", () => { + const manifest = emptyPackageManifest(); + addTestable(manifest, domainName("a")); + addTestable(manifest, domainName("a")); + should(manifest.testables).deepEqual(["a"]); + }); + it("should add testables in alphabetical order", () => { + const manifest = emptyPackageManifest(); + addTestable(manifest, domainName("b")); + addTestable(manifest, domainName("a")); + should(manifest.testables).deepEqual(["a", "b"]); + }); + }); +}); diff --git a/test/test-scoped-registry.ts b/test/test-scoped-registry.ts new file mode 100644 index 00000000..0a9fc7ac --- /dev/null +++ b/test/test-scoped-registry.ts @@ -0,0 +1,47 @@ +import { describe } from "mocha"; +import { + addScope, + hasScope, + removeScope, + scopedRegistry, +} from "../src/types/scoped-registry"; +import { exampleRegistryUrl } from "./mock-registry"; +import should from "should"; +import { domainName } from "../src/types/domain-name"; + +describe("scoped-registry", function () { + describe("construction", function () { + it("should have empty scopes list if scopes are not specified", () => { + const registry = scopedRegistry("test", exampleRegistryUrl); + should(registry.scopes).be.empty(); + }); + }); + describe("add scope", function () { + it("should keep scope-list alphabetical", () => { + const registry = scopedRegistry("test", exampleRegistryUrl); + addScope(registry, domainName("b")); + addScope(registry, domainName("a")); + should(registry.scopes).be.deepEqual(["a", "b"]); + }); + }); + describe("has scope", function () { + it("should have scope that was added", () => { + const registry = scopedRegistry("test", exampleRegistryUrl); + addScope(registry, domainName("a")); + should(hasScope(registry, domainName("a"))).be.true(); + }); + it("should not have scope that was not added", () => { + const registry = scopedRegistry("test", exampleRegistryUrl); + should(hasScope(registry, domainName("a"))).be.false(); + }); + }); + describe("remove scope", function () { + it("should not have scope after removing it", () => { + const registry = scopedRegistry("test", exampleRegistryUrl, [ + domainName("a"), + ]); + removeScope(registry, domainName("a")); + should(hasScope(registry, domainName("a"))).be.false(); + }); + }); +}); diff --git a/test/test-upm-config.ts b/test/test-upm-config.ts new file mode 100644 index 00000000..48d8b25b --- /dev/null +++ b/test/test-upm-config.ts @@ -0,0 +1,74 @@ +import { describe } from "mocha"; +import { + decodeBasicAuth, + encodeBasicAuth, + isBasicAuth, + isTokenAuth, + shouldAlwaysAuth, + UpmAuth, +} from "../src/types/upm-config"; +import should from "should"; +import { Base64 } from "../src/types/base64"; + +describe("upm-config", function () { + describe("auth", function () { + describe("classification", function () { + it("should be basic auth if it has _auth property", () => { + const auth: UpmAuth = { + email: "real@email.com", + alwaysAuth: true, + // Not a real base64 string, but we don't care in this test + _auth: "h8gz8s9zgseihgisejf" as Base64, + }; + should(isBasicAuth(auth)).be.true(); + }); + it("should be token auth if it has token property", () => { + const auth: UpmAuth = { + email: "real@email.com", + alwaysAuth: true, + // Not a real token, but we don't care in this test + token: "h8gz8s9zgseihgisejf", + }; + should(isTokenAuth(auth)).be.true(); + }); + }); + describe("encode/decode", function () { + it("should decode the same basic auth as was encoded", () => { + const expectedUsername = "my-name"; + const expectedPassword = "123pass"; + const encoded = encodeBasicAuth(expectedUsername, expectedPassword); + const [actualUsername, actualPassword] = decodeBasicAuth(encoded); + should(actualUsername).be.equal(expectedUsername); + should(actualPassword).be.equal(expectedPassword); + }); + }); + describe("always-auth", function () { + it("should always-auth when prop is true", () => { + const auth: UpmAuth = { + email: "real@email.com", + alwaysAuth: true, + // Not a real base64 string, but we don't care in this test + _auth: "h8gz8s9zgseihgisejf" as Base64, + }; + should(shouldAlwaysAuth(auth)).be.true(); + }); + it("should not always-auth when prop is false", () => { + const auth: UpmAuth = { + email: "real@email.com", + alwaysAuth: false, + // Not a real base64 string, but we don't care in this test + _auth: "h8gz8s9zgseihgisejf" as Base64, + }; + should(shouldAlwaysAuth(auth)).be.false(); + }); + it("should not always-auth when prop is missing", () => { + const auth: UpmAuth = { + email: "real@email.com", + // Not a real base64 string, but we don't care in this test + _auth: "h8gz8s9zgseihgisejf" as Base64, + }; + should(shouldAlwaysAuth(auth)).be.false(); + }); + }); + }); +}); diff --git a/test/types.ts b/test/types.ts index e2863c7c..7f61ce04 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,4 +1,4 @@ -import { Contact } from "../src/types/global"; +import { Contact } from "../src/types/contact"; import { DomainName } from "../src/types/domain-name"; import { SemanticVersion } from "../src/types/semantic-version";