diff --git a/index.js b/index.js index a80314ee5..8779e5963 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,15 @@ module.exports = { normalizer: require("./lib/normalizer"), projectPreprocessor: require("./lib/projectPreprocessor"), + /** + * @public + * @see module:@ui5/project.ui5Framework + * @namespace + */ + ui5Framework: { + Openui5Resolver: require("./lib/ui5Framework/Openui5Resolver"), + Sapui5Resolver: require("./lib/ui5Framework/Sapui5Resolver") + }, /** * @private * @see module:@ui5/project.translators diff --git a/lib/normalizer.js b/lib/normalizer.js index 0a0b69f69..2d3d2d461 100644 --- a/lib/normalizer.js +++ b/lib/normalizer.js @@ -20,15 +20,28 @@ const Normalizer = { * @param {string} [options.configPath] Path to configuration file * @param {string} [options.translatorName] Translator to use * @param {Object} [options.translatorOptions] Options to pass to translator + * @param {Object} [options.frameworkOptions] Options to pass to the framework installer * @returns {Promise} Promise resolving to tree object */ generateProjectTree: async function(options = {}) { - const tree = await Normalizer.generateDependencyTree(options); + let tree = await Normalizer.generateDependencyTree(options); if (options.configPath) { tree.configPath = options.configPath; } - return projectPreprocessor.processTree(tree); + tree = await projectPreprocessor.processTree(tree); + + if (tree.framework) { + const ui5Framework = require("./translators/ui5Framework"); + log.verbose(`Root project ${tree.metadata.name} defines framework ` + + `configuration. Installing UI5 dependencies...`); + let frameworkTree = await ui5Framework.generateDependencyTree(tree, options.frameworkOptions); + if (frameworkTree) { + frameworkTree = await projectPreprocessor.processTree(frameworkTree); + ui5Framework.mergeTrees(tree, frameworkTree); + } + } + return tree; }, /** diff --git a/lib/translators/npm.js b/lib/translators/npm.js index 6645f3c78..11797924f 100644 --- a/lib/translators/npm.js +++ b/lib/translators/npm.js @@ -3,7 +3,7 @@ const path = require("path"); const readPkgUp = require("read-pkg-up"); const readPkg = require("read-pkg"); const {promisify} = require("util"); -const fs = require("fs"); +const fs = require("graceful-fs"); const realpath = promisify(fs.realpath); const resolveModulePath = promisify(require("resolve")); const parentNameRegExp = new RegExp(/:([^:]+):$/i); diff --git a/lib/translators/ui5Framework.js b/lib/translators/ui5Framework.js new file mode 100644 index 000000000..f395b3c7d --- /dev/null +++ b/lib/translators/ui5Framework.js @@ -0,0 +1,221 @@ +const log = require("@ui5/logger").getLogger("normalizer:translators:ui5Framework"); + +class ProjectProcessor { + constructor({libraryMetadata}) { + this._libraryMetadata = libraryMetadata; + this._projectCache = {}; + } + getProject(libName) { + log.verbose(`Creating project for library ${libName}...`); + + if (this._projectCache[libName]) { + log.verbose(`Returning cached project for library ${libName}`); + return this._projectCache[libName]; + } + + if (!this._libraryMetadata[libName]) { + throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); + } + + const depMetadata = this._libraryMetadata[libName]; + + const dependencies = []; + dependencies.push(...depMetadata.dependencies.map((depName) => { + return this.getProject(depName); + })); + + if (depMetadata.optionalDependencies) { + const resolvedOptionals = depMetadata.optionalDependencies.map((depName) => { + if (this._libraryMetadata[depName]) { + log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); + return this.getProject(depName); + } + }).filter(($)=>$); + + dependencies.push(...resolvedOptionals); + } + + this._projectCache[libName] = { + id: depMetadata.id, + version: depMetadata.version, + path: depMetadata.path, + dependencies + }; + return this._projectCache[libName]; + } +} + +const utils = { + getAllNodesOfTree(tree) { + const nodes = {}; + const queue = [...tree]; + while (queue.length) { + const project = queue.shift(); + if (!nodes[project.metadata.name]) { + nodes[project.metadata.name] = project; + queue.push(...project.dependencies); + } + } + return nodes; + }, + isFrameworkProject(project) { + return project.id.startsWith("@openui5/") || project.id.startsWith("@sapui5/"); + }, + shouldIncludeDependency({optional, development}, root) { + // Root project should include all dependencies + // Otherwise only non-optional and non-development dependencies should be included + return root || (optional !== true && development !== true); + }, + getFrameworkLibrariesFromTree(project, ui5Dependencies = [], root = true) { + if (utils.isFrameworkProject(project)) { + // Ignoring UI5 Framework libraries in dependencies + return ui5Dependencies; + } + if (project.framework && project.framework.libraries) { + project.framework.libraries.forEach((dependency) => { + if (!ui5Dependencies.includes(dependency.name) && utils.shouldIncludeDependency(dependency, root)) { + ui5Dependencies.push(dependency.name); + } + }); + } else { + log.verbose(`Project ${project.metadata.name} defines no framework.libraries configuration`); + // Possible future enhancement: Fallback to detect OpenUI5 framework dependencies in package.json + } + project.dependencies.map((depProject) => { + utils.getFrameworkLibrariesFromTree(depProject, ui5Dependencies, false); + }); + return ui5Dependencies; + }, + ProjectProcessor +}; + +/** + * + * + * @private + * @namespace + * @alias module:@ui5/project.translators.ui5Framework + */ +module.exports = { + /** + * + * + * @public + * @param {Object} tree + * @param {Object} [options] + * @param {Object} [options.versionOverride] Framework version to use instead of the root projects framework + * version from the provided tree + * @returns {Promise} Promise + */ + generateDependencyTree: async function(tree, options = {}) { + // Don't create a tree when root project doesn't have a framework configuration + if (!tree.framework) { + return null; + } + + const frameworkName = tree.framework.name; + if (frameworkName !== "SAPUI5" && frameworkName !== "OpenUI5") { + throw new Error( + `Unknown framework.name "${frameworkName}" for project ${tree.id}. Must be "OpenUI5" or "SAPUI5"` + ); + } + + let version; + if (!tree.framework.version) { + throw new Error( + `framework.version is not defined for project ${tree.id}` + ); + } else if (options.versionOverride) { + log.info( + `Overriding configured ${frameworkName} version ` + + `${tree.framework.version} with supplied version ${options.versionOverride}` + ); + version = options.versionOverride; + } else { + version = tree.framework.version; + } + + log.info(`Using ${frameworkName} version: ${version}`); + + const referencedLibraries = utils.getFrameworkLibrariesFromTree(tree); + + let resolver; + if (frameworkName === "OpenUI5") { + const Openui5Resolver = require("../ui5Framework/Openui5Resolver"); + resolver = new Openui5Resolver({cwd: tree.path, version}); + } else if (frameworkName === "SAPUI5") { + const Sapui5Resolver = require("../ui5Framework/Sapui5Resolver"); + resolver = new Sapui5Resolver({cwd: tree.path, version}); + } + + let startTime; + if (log.isLevelEnabled("verbose")) { + startTime = process.hrtime(); + } + + const {libraryMetadata} = await resolver.install(referencedLibraries); + + if (log.isLevelEnabled("verbose")) { + const timeDiff = process.hrtime(startTime); + const prettyHrtime = require("pretty-hrtime"); + log.verbose(`${frameworkName} dependencies ${referencedLibraries.join(", ")} resolved in ${prettyHrtime(timeDiff)}`); + } + + const projectProcessor = new utils.ProjectProcessor({ + libraryMetadata + }); + + const libraries = referencedLibraries.map((libName) => { + return projectProcessor.getProject(libName); + }); + + // Use root project (=requesting project) as root of framework tree + const frameworkTree = { + id: tree.id, + version: tree.version, + path: tree.path, + dependencies: libraries + }; + return frameworkTree; + }, + + mergeTrees: function(projectTree, frameworkTree) { + const frameworkLibs = utils.getAllNodesOfTree(frameworkTree.dependencies); + + log.verbose(`Merging framework tree into project tree "${projectTree.metadata.name}"`); + + const queue = [projectTree]; + while (queue.length) { + const project = queue.shift(); + + project.dependencies = project.dependencies.filter((depProject) => { + if (utils.isFrameworkProject(depProject)) { + log.verbose( + `A translator has already added the UI5 framework library ${depProject.metadata.name} ` + + `(id: ${depProject.id}) to the dependencies of project ${project.metadata.name}. ` + + `This dependency will be ignored.`); + log.info(`If project ${project.metadata.name} contains a package.json in which it defines a ` + + `dependency to the UI5 framework library ${depProject.id}, this dependency should be removed.`); + return false; + } + return true; + }); + queue.push(...project.dependencies); + + if (project.framework && project.framework.libraries) { + const frameworkDeps = project.framework.libraries.map((dependency) => { + if (!frameworkLibs[dependency.name]) { + throw new Error(`Missing framework library ${dependency.name} ` + + `required by project ${project.metadata.name}`); + } + return frameworkLibs[dependency.name]; + }); + project.dependencies.push(...frameworkDeps); + } + } + return projectTree; + }, + + // Export for testing only + _utils: process.env.NODE_ENV === "test" ? utils : undefined +}; diff --git a/lib/ui5Framework/AbstractResolver.js b/lib/ui5Framework/AbstractResolver.js new file mode 100644 index 000000000..05cf66710 --- /dev/null +++ b/lib/ui5Framework/AbstractResolver.js @@ -0,0 +1,157 @@ +const path = require("path"); +const log = require("@ui5/logger").getLogger("ui5Framework:AbstractResolver"); + +/** + * Abstract Resolver + * + * @public + * @abstract + * @memberof module:@ui5/project.ui5Framework + */ +class AbstractResolver { + /** + * @param {*} options options + * @param {string} options.version Framework version to use + * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc + * @param {string} [options.ui5HomeDir="~/.ui5"] UI5 home directory location. This will be used to store packages, metadata and configuration used by the resolvers. Relative to `process.cwd()` + */ + constructor({cwd, version, ui5HomeDir}) { + if (new.target === AbstractResolver) { + throw new TypeError("Class 'AbstractResolver' is abstract"); + } + + if (!version) { + throw new Error(`AbstractResolver: Missing parameter "version"`); + } + + this._ui5HomeDir = ui5HomeDir || path.join(require("os").homedir(), ".ui5"); + + this._cwd = cwd || process.cwd(); + this._version = version; + } + + async _processLibrary(libraryName, libraryMetadata, errors) { + // Check if library is already processed + if (libraryMetadata[libraryName]) { + return; + } + // Mark library as handled + libraryMetadata[libraryName] = {}; + + log.verbose("Processing " + libraryName); + + const promises = await this.handleLibrary(libraryName); + + const [metadata, {pkgPath}] = await Promise.all([ + promises.metadata.then((metadata) => + this._processDependencies(libraryName, metadata, libraryMetadata, errors)), + promises.install + ]); + + // Add path to installed package to metadata + metadata.path = pkgPath; + + // Add metadata entry + libraryMetadata[libraryName] = metadata; + } + + async _processDependencies(libraryName, metadata, libraryMetadata, errors) { + if (metadata.dependencies.length > 0) { + log.verbose("Processing dependencies of " + libraryName); + await this._processLibraries(metadata.dependencies, libraryMetadata, errors); + log.verbose("Done processing dependencies of " + libraryName); + } + return metadata; + } + + async _processLibraries(libraryNames, libraryMetadata, errors) { + const results = await Promise.all(libraryNames.map(async (libraryName) => { + try { + await this._processLibrary(libraryName, libraryMetadata, errors); + } catch (err) { + return `Failed to resolve library ${libraryName}: ${err.message}`; + } + })); + // Don't add empty results (success) + errors.push(...results.filter(($) => $)); + } + + /** + * Library metadata entry + * + * @example + * { + * "id": "@openui5/sap.ui.core", + * "version": "1.75.0", + * "path": "~/.ui5/framework/packages/@openui5/sap.ui.core/1.75.0", + * "dependencies": [], + * "optionalDependencies": [] + * } + * + * @public + * @typedef {Object} LibraryMetadataEntry + * @property {string} id Identifier + * @property {string} version Version + * @property {string} path Path + * @property {string[]} dependencies List of dependency ids + * @property {string[]} optionalDependencies List of optional dependency ids + * @memberof module:@ui5/project.ui5Framework + */ + /** + * Install result + * + * @example + * { + * "libraryMetadata": { + * "sap.ui.core": { + * // ... + * }, + * "sap.m": { + * // ... + * } + * } + * } + * + * @public + * @typedef {Object} ResolverInstallResult + * @property {Object.} libraryMetadata + * Object containing all installed libraries with library name as key + * @memberof module:@ui5/project.ui5Framework + */ + /** + * Installs the provided libraries and their dependencies + * + * @example + * resolver.install(["sap.ui.core", "sap.m"]).then(({libraryMetadata}) => { + * // Installation done + * }).catch((err) => { + * // Handle installation errors + * }); + * + * @public + * @param {string[]} libraryNames List of library names to be installed + * @returns {module:@ui5/project.ui5Framework.ResolverInstallResult} + * Resolves with an object containing the libraryMetadata + */ + async install(libraryNames) { + const libraryMetadata = {}; + const errors = []; + + await this._processLibraries(libraryNames, libraryMetadata, errors); + + if (errors.length > 0) { + throw new Error("Resolution of framework libraries failed with errors:\n" + errors.join("\n")); + } + + return { + libraryMetadata + }; + } + + // To be implemented by resolver + async handleLibrary(libraryName) { + throw new Error("AbstractResolver: handleLibrary must be implemented!"); + } +} + +module.exports = AbstractResolver; diff --git a/lib/ui5Framework/Openui5Resolver.js b/lib/ui5Framework/Openui5Resolver.js new file mode 100644 index 000000000..d14c5e7d5 --- /dev/null +++ b/lib/ui5Framework/Openui5Resolver.js @@ -0,0 +1,77 @@ +const AbstractResolver = require("./AbstractResolver"); +const Installer = require("./npm/Installer"); + +/** + * Resolver for the OpenUI5 framework + * + * @public + * @memberof module:@ui5/project.ui5Framework + * @extends module:@ui5/project.ui5Framework.AbstractResolver + */ +class Openui5Resolver extends AbstractResolver { + /** + * @param {*} options options + * @param {string} options.version OpenUI5 version to use + * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc + * @param {string} [options.ui5HomeDir="~/.ui5"] UI5 home directory location. This will be used to store packages, metadata and configuration used by the resolvers. Relative to `process.cwd()` + */ + constructor(options) { + super(options); + + this._installer = new Installer({ + cwd: this._cwd, + ui5HomeDir: this._ui5HomeDir + }); + this._loadLibraryMetadata = {}; + } + static _getNpmPackageName(libraryName) { + return "@openui5/" + libraryName; + } + static _getLibaryName(pkgName) { + return pkgName.replace(/^@openui5\//, ""); + } + _getLibraryMetadata(libraryName) { + if (!this._loadLibraryMetadata[libraryName]) { + this._loadLibraryMetadata[libraryName] = Promise.resolve().then(async () => { + // Trigger manifest request to gather transitive dependencies + const pkgName = Openui5Resolver._getNpmPackageName(libraryName); + const libraryManifest = await this._installer.fetchPackageManifest({pkgName, version: this._version}); + let dependencies = []; + if (libraryManifest.dependencies) { + const depNames = Object.keys(libraryManifest.dependencies); + dependencies = depNames.map(Openui5Resolver._getLibaryName); + } + + // npm devDependencies are handled as "optionalDependencies" + // in terms of the UI5 framework metadata structure + let optionalDependencies = []; + if (libraryManifest.devDependencies) { + const devDepNames = Object.keys(libraryManifest.devDependencies); + optionalDependencies = devDepNames.map(Openui5Resolver._getLibaryName); + } + + return { + id: pkgName, + version: this._version, + dependencies, + optionalDependencies + }; + }); + } + return this._loadLibraryMetadata[libraryName]; + } + async handleLibrary(libraryName) { + const pkgName = Openui5Resolver._getNpmPackageName(libraryName); + return { + // Trigger metadata request + metadata: this._getLibraryMetadata(libraryName), + // Also trigger installation of package + install: this._installer.installPackage({ + pkgName, + version: this._version + }) + }; + } +} + +module.exports = Openui5Resolver; diff --git a/lib/ui5Framework/Sapui5Resolver.js b/lib/ui5Framework/Sapui5Resolver.js new file mode 100644 index 000000000..4b3d171a5 --- /dev/null +++ b/lib/ui5Framework/Sapui5Resolver.js @@ -0,0 +1,71 @@ +const path = require("path"); +const AbstractResolver = require("./AbstractResolver"); +const Installer = require("./npm/Installer"); +const log = require("@ui5/logger").getLogger("normalizer:ui5Framework:Sapui5Resolver"); + +const DIST_PKG_NAME = "@sapui5/distribution-metadata"; + +/** + * Resolver for the SAPUI5 framework + * + * @public + * @memberof module:@ui5/project.ui5Framework + * @extends module:@ui5/project.ui5Framework.AbstractResolver + */ +class Sapui5Resolver extends AbstractResolver { + /** + * @param {*} options options + * @param {string} options.version SAPUI5 version to use + * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc + * @param {string} [options.ui5HomeDir="~/.ui5"] UI5 home directory location. This will be used to store packages, metadata and configuration used by the resolvers. Relative to `process.cwd()` + */ + constructor(options) { + super(options); + + this._installer = new Installer({ + cwd: this._cwd, + ui5HomeDir: this._ui5HomeDir + }); + this._loadDistMetadata = null; + } + loadDistMetadata() { + if (!this._loadDistMetadata) { + this._loadDistMetadata = Promise.resolve().then(async () => { + const version = this._version; + log.verbose(`Installing ${DIST_PKG_NAME} in version ${version}...`); + const pkgName = DIST_PKG_NAME; + const {pkgPath} = await this._installer.installPackage({ + pkgName, + version + }); + + const metadata = await this._installer.readJson(path.join(pkgPath, "metadata.json")); + return metadata; + }); + } + return this._loadDistMetadata; + } + async handleLibrary(libraryName) { + const distMetadata = await this.loadDistMetadata(); + const metadata = distMetadata.libraries[libraryName]; + if (!metadata) { + throw new Error(`Could not find library "${libraryName}"`); + } + + return { + metadata: Promise.resolve({ + id: metadata.npmPackageName, + version: metadata.version, + dependencies: metadata.dependencies, + optionalDependencies: metadata.optionalDependencies + }), + // Trigger installation of package + install: this._installer.installPackage({ + pkgName: metadata.npmPackageName, + version: metadata.version + }) + }; + } +} + +module.exports = Sapui5Resolver; diff --git a/lib/ui5Framework/npm/Installer.js b/lib/ui5Framework/npm/Installer.js new file mode 100644 index 000000000..ff56acb44 --- /dev/null +++ b/lib/ui5Framework/npm/Installer.js @@ -0,0 +1,120 @@ +const path = require("path"); +const fs = require("graceful-fs"); +const {promisify} = require("util"); +const stat = promisify(fs.stat); +const readFile = promisify(fs.readFile); +const lockfile = require("lockfile"); +const lock = promisify(lockfile.lock); +const unlock = promisify(lockfile.unlock); +const mkdirp = require("mkdirp"); +const Registry = require("./Registry"); +const log = require("@ui5/logger").getLogger("normalizer:ui5Framework:npm:Installer"); + +class Installer { + constructor({cwd, ui5HomeDir}) { + if (!cwd) { + throw new Error(`Installer: Missing parameter "cwd"`); + } + if (!ui5HomeDir) { + throw new Error(`Installer: Missing parameter "ui5HomeDir"`); + } + this._baseDir = path.join(ui5HomeDir, "framework", "packages"); + log.verbose(`Installing to: ${this._baseDir}`); + this._registry = new Registry({ + cwd, + cacheDir: path.join(ui5HomeDir, "framework", "cacache") + }); + this._lockDir = path.join(ui5HomeDir, "framework", "locks"); + } + + async readJson(jsonPath) { + return JSON.parse(await readFile(jsonPath, {encoding: "utf8"})); + } + + async fetchPackageManifest({pkgName, version}) { + const targetDir = this._getTargetDirForPackage({pkgName, version}); + try { + const pkg = await this.readJson(path.join(targetDir, "package.json")); + return { + name: pkg.name, + dependencies: pkg.dependencies, + devDependencies: pkg.devDependencies + }; + } catch (err) { + if (err.code === "ENOENT") { // "File or directory does not exist" + const manifest = await this._registry.requestPackageManifest(pkgName, version); + return { + name: manifest.name, + dependencies: manifest.dependencies, + devDependencies: manifest.devDependencies + }; + } else { + throw err; + } + } + } + + async installPackage({pkgName, version}) { + const targetDir = this._getTargetDirForPackage({pkgName, version}); + const installed = await this._packageJsonExists(targetDir); + if (!installed) { + await this._synchronize({pkgName, version}, async () => { + // check again whether package is now installed + const installed = await this._packageJsonExists(targetDir); + if (!installed) { + log.info(`Installing missing package ${pkgName}...`); + log.verbose(`Installing ${pkgName} in version ${version} to ${targetDir}...`); + await this._registry.extractPackage(pkgName, version, targetDir); + } else { + log.verbose(`Alrady installed: ${pkgName} in version ${version}`); + } + }); + } else { + log.verbose(`Alrady installed: ${pkgName} in version ${version}`); + } + return { + pkgPath: targetDir + }; + } + + async _packageJsonExists(targetDir) { + try { + await stat(path.join(targetDir, "package.json")); + return true; + } catch (err) { + if (err.code === "ENOENT") { // "File or directory does not exist" + return false; + } else { + throw err; + } + } + } + + async _synchronize({pkgName, version}, callback) { + const lockPath = this._getLockPath({pkgName, version}); + await mkdirp(this._lockDir); + log.verbose("Locking " + lockPath); + await lock(lockPath, { + wait: 10000, + stale: 60000, + retries: 10 + }); + try { + await callback(); + } finally { + log.verbose("Unlocking " + lockPath); + await unlock(lockPath); + } + } + + _getLockPath({pkgName, version}) { + const lockName = pkgName.replace(/\//g, "-"); + return path.join(this._lockDir, `package-${lockName}@${version}.lock`); + } + + _getTargetDirForPackage({pkgName, version}) { + return path.join(this._baseDir, ...pkgName.split("/"), version); + } +} + +module.exports = Installer; diff --git a/lib/ui5Framework/npm/Registry.js b/lib/ui5Framework/npm/Registry.js new file mode 100644 index 000000000..0d4716f41 --- /dev/null +++ b/lib/ui5Framework/npm/Registry.js @@ -0,0 +1,54 @@ +const log = require("@ui5/logger").getLogger("normalizer:ui5Framework:npm:Registry"); + +function logConfig(config, configName) { + const configValue = config[configName]; + if (configValue) { + log.verbose(` ${configName}: ${configValue}`); + } +} + +class Registry { + constructor({cwd, cacheDir}) { + this._pacote = require("pacote"); + this._cwd = cwd; + this._cacheDir = cacheDir; + } + requestPackageManifest(pkgName, version) { + return this._pacote.manifest(`${pkgName}@${version}`, this._getPacoteOptions()); + } + extractPackage(pkgName, version, targetDir) { + return this._pacote.extract(`${pkgName}@${version}`, targetDir, this._getPacoteOptions()); + } + _getPacoteOptions() { + if (!this._npmConfig) { + const libnpmconfig = require("libnpmconfig"); + const opts = { + cache: this._cacheDir + }; + if (log.isLevelEnabled("verbose")) { + opts.log = log._getLogger(); + } + const config = libnpmconfig.read(opts, { + cwd: this._cwd + }).toJSON(); + + log.verbose(`Using npm configuration (extract):`); + // Do not log full configuration as it may contain authentication tokens + logConfig(config, "registry"); + logConfig(config, "@sapui5:registry"); + logConfig(config, "@openui5:registry"); + logConfig(config, "proxy"); + logConfig(config, "globalconfig"); + logConfig(config, "userconfig"); + logConfig(config, "cache"); + logConfig(config, "cwd"); + + this._npmConfig = config; + } + + // Use cached config + return this._npmConfig; + } +} + +module.exports = Registry; diff --git a/package-lock.json b/package-lock.json index 185c6468c..917ba19b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -530,6 +530,22 @@ "fastq": "^1.6.0" } }, + "@npmcli/ci-detect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.2.0.tgz", + "integrity": "sha512-JtktVH7ASBVIWsQTFlFpeOzhBJskvoBCTfeeRhhZy7ybATcUvwiwotZ8j5rkqUUyB69lIy/AvboiiiGBjYBKBA==" + }, + "@npmcli/installed-package-contents": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.5.tgz", + "integrity": "sha512-aKIwguaaqb6ViwSOFytniGvLPb9SMCUm39TgM3SfUo7n0TxUMbwoXfpwyvQ4blm10lzbAwTsvjr7QZ85LvTi4A==", + "requires": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1", + "read-package-json-fast": "^1.1.1", + "readdir-scoped-modules": "^1.1.0" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -725,11 +741,40 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" + }, + "agentkeepalive": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.0.tgz", + "integrity": "sha512-CW/n1wxF8RpEuuiq6Vbn9S8m0VSYDMnZESqaJ6F2cWN9fY8rei2qaxweIaRgq+ek8TqfoFIsUjaGNKGGEHElSg==", + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "aggregate-error": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", - "dev": true, "requires": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -925,6 +970,11 @@ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -1305,11 +1355,76 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, + "cacache": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.0.tgz", + "integrity": "sha512-L0JpXHhplbJSiDGzyJJnJCTL7er7NzbBgxzVqLswEb4bO91Zbv17OUMuUeu/q0ZwKn3V+1HM4wb9tO4eVE/K8g==", + "requires": { + "chownr": "^1.1.2", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "move-concurrently": "^1.0.1", + "p-map": "^3.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^2.7.1", + "ssri": "^8.0.0", + "tar": "^6.0.1", + "unique-filename": "^1.1.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "mkdirp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -1498,6 +1613,11 @@ } } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "chunkd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-1.0.0.tgz", @@ -1519,8 +1639,7 @@ "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" }, "clean-yaml-object": { "version": "0.1.0", @@ -1892,6 +2011,37 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", @@ -2072,6 +2222,11 @@ "ms": "2.0.0" } }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -2289,6 +2444,14 @@ "rimraf": "^2.6.2" }, "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -2299,6 +2462,15 @@ } } }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2430,6 +2602,15 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "optional": true, + "requires": { + "iconv-lite": "~0.4.13" + } + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2450,6 +2631,11 @@ "integrity": "sha1-IcoRLUirJLTh5//A5TOdMf38J0w=", "dev": true }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2527,6 +2713,19 @@ "event-emitter": "~0.3.5" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, "es6-set": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", @@ -2686,6 +2885,15 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3005,6 +3213,11 @@ "reusify": "^1.0.4" } }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -3213,6 +3426,25 @@ "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", "dev": true }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3502,8 +3734,7 @@ "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" }, "http-deceiver": { "version": "1.2.7", @@ -3539,6 +3770,30 @@ "requires-port": "^1.0.0" } }, + "http-proxy-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-3.0.0.tgz", + "integrity": "sha512-uGuJaBWQWDQCJI5ip0d/VTYZW0nRrlLWXA4A7P1jrsa+f77rW2yXz315oBt6zGCF6l8C2tlMxY7ffULCj+5FhA==", + "requires": { + "agent-base": "5", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -3549,6 +3804,38 @@ "sshpk": "^1.7.0" } }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "requires": { + "agent-base": "5", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "requires": { + "ms": "^2.0.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3557,6 +3844,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, "ignore": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", @@ -3568,6 +3860,14 @@ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "requires": { + "minimatch": "^3.0.4" + } + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -3605,14 +3905,17 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" }, "inflight": { "version": "1.0.6", @@ -3631,8 +3934,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { "version": "6.5.2", @@ -3762,6 +4064,11 @@ } } }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3862,6 +4169,11 @@ } } }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=" + }, "is-npm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-3.0.0.tgz", @@ -4005,8 +4317,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "4.0.0", @@ -4218,6 +4529,16 @@ "strip-json-comments": "^3.0.1", "taffydb": "2.6.2", "underscore": "~1.9.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + } } }, "jsdoctypeparser": { @@ -4243,6 +4564,11 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, + "json-parse-even-better-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.0.1.tgz", + "integrity": "sha512-XFY2Mbnmg+8r7MRsxfArVkZcfjxGlF/NjM3LsPXVeCX/GBF/1FTCv+idHBYC4qLPtK7q8HC8bapLoWqnhP/bXw==" + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -4281,6 +4607,11 @@ } } }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -4351,6 +4682,16 @@ "type-check": "~0.3.2" } }, + "libnpmconfig": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/libnpmconfig/-/libnpmconfig-1.2.1.tgz", + "integrity": "sha512-9esX8rTQAHqarx6qeZqmGQKBNZR5OIbl/Ayr0qQDy3oXja2iFVQQI81R6GZ2a02bSNZ9p3YOGX1O6HHCb1X7kA==", + "requires": { + "figgy-pudding": "^3.5.1", + "find-up": "^3.0.0", + "ini": "^1.3.5" + } + }, "linkify-it": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", @@ -4379,6 +4720,14 @@ "path-exists": "^3.0.0" } }, + "lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "requires": { + "signal-exit": "^3.0.2" + } + }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", @@ -4522,6 +4871,43 @@ "semver": "^6.0.0" } }, + "make-fetch-happen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.1.tgz", + "integrity": "sha512-oiK8xz6+IxaPqmOCW+rmlH922RTZ+fi4TAULGRih8ryqIju0x6WriDR3smm7Z+8NZRxDIK/iDLM096F/gLfiWg==", + "requires": { + "agentkeepalive": "^4.1.0", + "cacache": "^15.0.0", + "http-cache-semantics": "^4.0.4", + "http-proxy-agent": "^3.0.0", + "https-proxy-agent": "^4.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^5.1.1", + "minipass": "^3.0.0", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.1.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^8.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, "map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -4756,14 +5142,95 @@ } } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "minipass": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", + "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", "requires": { - "minimist": "0.0.8" + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.2.1.tgz", + "integrity": "sha512-ssHt0dkljEDaKmTgQ04DQgx2ag6G2gMPxA5hpcsoeTbfDgRf2fC2gNSRc6kISjD7ckCpHwwQvXxuTBK8402fXg==", + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-pipeline": "^1.2.2", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz", + "integrity": "sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "mkdirp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" + }, "mock-require": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz", @@ -4773,6 +5240,37 @@ "normalize-path": "^2.1.1" } }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4878,6 +5376,131 @@ "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", "dev": true }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "requires": { + "semver": "^7.1.1" + }, + "dependencies": { + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==" + } + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + }, + "npm-package-arg": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.0.0.tgz", + "integrity": "sha512-JgqZHCEUKvhX7EehLNdySiuB227a0QYra9wpZOkW+jvwsRYKkce7y5Rv2axkxScJU1EP+L32jT2PLhQz7IWHlw==", + "requires": { + "hosted-git-info": "^3.0.2", + "osenv": "^0.1.5", + "semver": "^7.0.0", + "validate-npm-package-name": "^3.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.2.tgz", + "integrity": "sha512-ezZMWtHXm7Eb7Rq4Mwnx2vs79WUx2QmRg3+ZqeGroKzfDO+EprOcgRPYghsOP9JuYBfK18VojmRTGCg8Ma+ktw==", + "requires": { + "lru-cache": "^5.1.1" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "npm-packlist": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.1.0.tgz", + "integrity": "sha512-XXqrT4WXVc8M1cdL7LCOUflEdyvCu9lKmM5j5mFwXAK8hUMRxzClNml8ox2d8YIDhS7p51AP6zYWNsgNiWuSLQ==", + "requires": { + "glob": "^7.1.6", + "ignore-walk": "^3.0.3", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.0.0.tgz", + "integrity": "sha512-PdJpXMvjqt4nftNEDpCgjBUF8yI3Q3MyuAmVB9nemnnCg32F4BPL/JFBfdj8DubgHCYUFQhtLWmBPvdsFtjWMg==", + "requires": { + "npm-install-checks": "^4.0.0", + "npm-package-arg": "^8.0.0", + "semver": "^7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==" + } + } + }, + "npm-registry-fetch": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-7.0.0.tgz", + "integrity": "sha512-XABSG02i/03EfnXM8azGksxICQO8g5MSiaxIUBsNTLXnQLBcdQNS67aOZsF5Yn97RV9o9g+fBUUN2Sg8u7iCBw==", + "requires": { + "@npmcli/ci-detect": "^1.0.0", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^8.0.1", + "minipass": "^3.0.0", + "minipass-fetch": "^1.1.2", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.0.0", + "npm-package-arg": "^8.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -5177,11 +5800,24 @@ } } }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } }, "p-cancelable": { "version": "1.1.0", @@ -5246,6 +5882,67 @@ "semver": "^6.2.0" } }, + "pacote": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-11.0.0.tgz", + "integrity": "sha512-Q8H9lfzlZTZyWWtYNGETtFp2qmzd9XHfuF3gas5xd8T6vf/6E7BOmhybWU2PxSVeSYjIw4hLpaf7hCqY+T2TbQ==", + "requires": { + "@npmcli/installed-package-contents": "^1.0.5", + "cacache": "^15.0.0", + "chownr": "^1.1.3", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.0.1", + "minipass-fetch": "^1.2.1", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.0", + "npm-packlist": "^2.0.3", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^7.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "read-package-json-fast": "^1.1.3", + "semver": "^7.1.1", + "ssri": "^8.0.0", + "tar": "^6.0.0", + "which": "^2.0.2" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "mkdirp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" + }, + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5502,6 +6199,20 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -5602,6 +6313,15 @@ } } }, + "read-package-json-fast": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-1.1.3.tgz", + "integrity": "sha512-MmFqiyfCXV2Dmm4jH24DEGhxdkUDFivJQj4oPZQPOKywxR7HWBE6WnMWDAapfFHi3wm1b+mhR+XHlUH0CL8axg==", + "requires": { + "json-parse-even-better-errors": "^2.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -5645,6 +6365,17 @@ "util-deprecate": "~1.0.1" } }, + "readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, "readdirp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", @@ -5894,6 +6625,11 @@ "signal-exit": "^3.0.2" } }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5921,6 +6657,14 @@ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "^1.1.1" + } + }, "rxjs": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", @@ -6085,6 +6829,39 @@ } } }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6130,6 +6907,23 @@ "which": "^2.0.1" }, "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6255,6 +7049,14 @@ "tweetnacl": "~0.14.0" } }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "requires": { + "minipass": "^3.1.1" + } + }, "stack-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", @@ -6607,6 +7409,31 @@ } } }, + "tar": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.1.tgz", + "integrity": "sha512-bKhKrrz2FJJj5s7wynxy/fyxpE0CmCjmOQ1KV4KkgXFWOgoIT/NbTMnB1n+LFNrNk0SSBVGGxcK5AGsyC+pW5Q==", + "requires": { + "chownr": "^1.1.3", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", @@ -6856,6 +7683,22 @@ "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", "dev": true }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "requires": { + "imurmurhash": "^0.1.4" + } + }, "unique-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", @@ -6874,6 +7717,17 @@ "mkdirp": "^0.5.1", "os-tmpdir": "^1.0.1", "uid2": "0.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } } }, "unpipe": { @@ -6947,6 +7801,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "requires": { + "builtins": "^1.0.3" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7112,6 +7974,17 @@ "dev": true, "requires": { "mkdirp": "^0.5.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } } }, "write-file-atomic": { diff --git a/package.json b/package.json index b88aa85cf..9e486f241 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,10 @@ "@ui5/server": "^1.6.0", "graceful-fs": "^4.2.3", "js-yaml": "^3.13.1", - "mock-require": "^3.0.3", + "libnpmconfig": "^1.2.1", + "lockfile": "^1.0.4", + "mkdirp": "^1.0.3", + "pacote": "^11.0.0", "pretty-hrtime": "^1.0.3", "read-pkg": "^3.0.0", "read-pkg-up": "^4.0.0", @@ -116,6 +119,7 @@ "eslint-config-google": "^0.14.0", "eslint-plugin-jsdoc": "^4.8.4", "jsdoc": "^3.6.3", + "mock-require": "^3.0.3", "nyc": "^15.0.0", "open-cli": "^5.0.0", "rimraf": "^3.0.2", diff --git a/test/lib/normalizer.js b/test/lib/normalizer.js index 43a48a366..e5d0d4d10 100644 --- a/test/lib/normalizer.js +++ b/test/lib/normalizer.js @@ -1,20 +1,28 @@ const test = require("ava"); const sinon = require("sinon"); const normalizer = require("../..").normalizer; -const npmTranslatorStub = sinon.stub(require("../..").translators.npm); -const staticTranslatorStub = sinon.stub(require("../..").translators.static); const projectPreprocessor = require("../../lib/projectPreprocessor"); +const ui5Framework = require("../../lib/translators/ui5Framework"); + +test.beforeEach((t) => { + t.context.npmTranslatorStub = sinon.stub(require("../..").translators.npm); + t.context.staticTranslatorStub = sinon.stub(require("../..").translators.static); +}); + +test.afterEach.always(() => { + sinon.restore(); +}); test.serial("Uses npm translator as default strategy", (t) => { normalizer.generateDependencyTree(); - t.truthy(npmTranslatorStub.generateDependencyTree.called); + t.truthy(t.context.npmTranslatorStub.generateDependencyTree.called); }); test.serial("Uses static translator as strategy", (t) => { normalizer.generateDependencyTree({ translatorName: "static" }); - t.truthy(staticTranslatorStub.generateDependencyTree.called); + t.truthy(t.context.staticTranslatorStub.generateDependencyTree.called); }); test.serial("Generate project tree using with overwritten config path", async (t) => { @@ -24,11 +32,35 @@ test.serial("Generate project tree using with overwritten config path", async (t t.deepEqual(projectPreprocessorStub.getCall(0).args[0], { configPath: "newPath/config.json" }, "Process tree with config loaded from custom path"); - projectPreprocessorStub.restore(); - normalizer.generateDependencyTree.restore(); }); -test("Error: Throws if unknown translator should be used as strategy", async (t) => { +test.serial("Pass frameworkOptions to ui5Framework translator", async (t) => { + const options = { + frameworkOptions: { + versionOverride: "1.2.3" + } + }; + const tree = { + metadata: { + name: "test" + }, + framework: {} + }; + + sinon.stub(normalizer, "generateDependencyTree").resolves({configPath: "defaultPath/config.json"}); + sinon.stub(projectPreprocessor, "processTree").resolves(tree); + + const ui5FrameworkGenerateDependencyTreeStub = sinon.stub(ui5Framework, "generateDependencyTree").resolves(null); + + await normalizer.generateProjectTree(options); + + t.is(ui5FrameworkGenerateDependencyTreeStub.callCount, 1, + "ui5Framework.generateDependencyTree should be called once"); + t.deepEqual(ui5FrameworkGenerateDependencyTreeStub.getCall(0).args, [tree, {versionOverride: "1.2.3"}], + "ui5Framework.generateDependencyTree should be called with expected args"); +}); + +test.serial("Error: Throws if unknown translator should be used as strategy", async (t) => { const translatorName = "notExistingTranslator"; return normalizer.generateDependencyTree({ translatorName diff --git a/test/lib/translators/ui5Framework.integration.js b/test/lib/translators/ui5Framework.integration.js new file mode 100644 index 000000000..4b7f507b8 --- /dev/null +++ b/test/lib/translators/ui5Framework.integration.js @@ -0,0 +1,801 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const path = require("path"); +const os = require("os"); + +const pacote = require("pacote"); +const libnpmconfig = require("libnpmconfig"); +const lockfile = require("lockfile"); +const logger = require("@ui5/logger"); +const normalizer = require("../../../lib/normalizer"); +const projectPreprocessor = require("../../../lib/projectPreprocessor"); +let ui5Framework; +let Installer; + +// Use path within project as mocking base directory to reduce chance of side effects +// in case mocks/stubs do not work and real fs is used +const fakeBaseDir = path.join(__dirname, "fake-tmp"); +const ui5FrameworkBaseDir = path.join(fakeBaseDir, "homedir", ".ui5", "framework"); +const ui5PackagesBaseDir = path.join(ui5FrameworkBaseDir, "packages"); + +test.beforeEach((t) => { + sinon.stub(libnpmconfig, "read").returns({ + toJSON: () => { + return { + registry: "https://registry.fake", + cache: path.join(ui5FrameworkBaseDir, "cacache"), + proxy: "" + }; + } + }); + sinon.stub(os, "homedir").returns(path.join(fakeBaseDir, "homedir")); + + sinon.stub(lockfile, "lock").yieldsAsync(); + sinon.stub(lockfile, "unlock").yieldsAsync(); + + mock("mkdirp", sinon.stub().resolves()); + + // Re-require to ensure that mocked modules are used + ui5Framework = mock.reRequire("../../../lib/translators/ui5Framework"); + Installer = require("../../../lib/ui5Framework/npm/Installer"); +}); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); + logger.setLevel("info"); // default log level +}); + +function defineTest(testName, { + frameworkName, + verbose = false +}) { + const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5"; + + const distributionMetadata = { + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib2": { + npmPackageName: "@sapui5/sap.ui.lib2", + version: "1.75.2", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [] + }, + "sap.ui.lib3": { + npmPackageName: "@sapui5/sap.ui.lib3", + version: "1.75.3", + dependencies: [], + optionalDependencies: [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + } + } + }; + + function project({name, version, type, framework, _level, dependencies = []}) { + const proj = { + _level, + id: name + "-id", + version, + path: path.join(fakeBaseDir, "project-" + name), + specVersion: "1.1", + kind: "project", + type, + metadata: { + name + }, + dependencies + }; + if (framework) { + proj.framework = framework; + } + return proj; + } + function frameworkProject({name, _level, dependencies = []}) { + const metadata = frameworkName === "SAPUI5" ? distributionMetadata.libraries[name] : null; + const id = frameworkName === "SAPUI5" ? metadata.npmPackageName : npmScope + "/" + name; + const version = frameworkName === "SAPUI5" ? metadata.version : "1.75.0"; + return { + _level, + id, + version, + path: path.join( + ui5PackagesBaseDir, + // sap.ui.lib4 is in @openui5 scope in SAPUI5 and OpenUI5 + name === "sap.ui.lib4" ? "@openui5" : npmScope, + name, version + ), + specVersion: "1.0", + kind: "project", + type: "library", + metadata: { + name + }, + framework: { + libraries: [] + }, + dependencies + }; + } + + test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => { + // Enable verbose logging + if (verbose) { + logger.setLevel("verbose"); + } + + const translatorTree = { + id: "test-application-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "project-test-application"), + dependencies: [ + { + id: "test-dependency-id", + version: "4.5.6", + path: path.join(fakeBaseDir, "project-test-dependency"), + dependencies: [] + }, + { + id: "test-dependency-no-framework-id", + version: "7.8.9", + path: path.join(fakeBaseDir, "project-test-dependency-no-framework"), + dependencies: [] + } + ] + }; + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + + sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "readConfigFile") + .callsFake(async (configPath) => { + throw new Error("ProjectPreprocessor#readConfigFile stub called with unknown configPath: " + configPath); + }) + .withArgs(path.join(fakeBaseDir, "project-test-application", "ui5.yaml")) + .resolves([{ + specVersion: "1.1", + type: "application", + metadata: { + name: "test-application" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + } + ] + } + }]) + .withArgs(path.join(fakeBaseDir, "project-test-dependency", "ui5.yaml")) + .resolves([{ + specVersion: "1.1", + type: "library", + metadata: { + name: "test-dependency" + }, + framework: { + version: "1.99.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib2" + } + ] + } + }]) + .withArgs(path.join(fakeBaseDir, "project-test-dependency-no-framework", "ui5.yaml")) + .resolves([{ + specVersion: "1.1", + type: "library", + metadata: { + name: "test-dependency-no-framework" + } + }]) + .withArgs(path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", + frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0", "ui5.yaml" + )) + .resolves([{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib1" + }, + framework: {libraries: []} + }]) + .withArgs(path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", + frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0", "ui5.yaml" + )) + .resolves([{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib2" + }, + framework: {libraries: []} + }]) + .withArgs(path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", + frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0", "ui5.yaml" + )) + .resolves([{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib3" + }, + framework: {libraries: []} + }]) + .withArgs(path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", + frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0", "ui5.yaml" + )) + .resolves([{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib4" + }, + framework: {libraries: []} + }]); + + // Prevent applying types as this would require a lot of mocking + sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "applyType"); + + sinon.stub(pacote, "extract").resolves(); + + if (frameworkName === "OpenUI5") { + sinon.stub(pacote, "manifest") + .callsFake(async (spec) => { + throw new Error("pacote.manifest stub called with unknown spec: " + spec); + }) + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: {} + }) + .withArgs("@openui5/sap.ui.lib2@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib2", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib3": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib3@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib3", + version: "1.75.0", + devDependencies: { + "@openui5/sap.ui.lib4": "1.75.0" + } + }) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib4", + version: "1.75.0", + dependencies: { + "@openui5/sap.ui.lib1": "1.75.0" + } + }); + } else if (frameworkName === "SAPUI5") { + sinon.stub(Installer.prototype, "readJson") + .callsFake(async (path) => { + throw new Error("Installer#readJson stub called with unknown path: " + path); + }) + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves(distributionMetadata); + } + + const expectedTree = project({ + _level: 0, + name: "test-application", + version: "1.2.3", + type: "application", + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + } + ] + }, + dependencies: [ + project({ + _level: 1, + name: "test-dependency", + version: "4.5.6", + type: "library", + framework: { + version: "1.99.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib2" + } + ] + }, + dependencies: [ + frameworkProject({ + _level: 1, + name: "sap.ui.lib1", + }), + frameworkProject({ + _level: 1, + name: "sap.ui.lib2", + dependencies: [ + frameworkProject({ + _level: 2, + name: "sap.ui.lib3", + dependencies: [ + frameworkProject({ + name: "sap.ui.lib4", + _level: 1, + dependencies: [ + frameworkProject({ + _level: 1, + name: "sap.ui.lib1" + }) + ] + }) + ] + }) + ] + }) + ] + }), + project({ + _level: 1, + name: "test-dependency-no-framework", + version: "7.8.9", + type: "library" + }), + frameworkProject({ + _level: 1, + name: "sap.ui.lib1", + }), + frameworkProject({ + name: "sap.ui.lib4", + _level: 1, + dependencies: [ + frameworkProject({ + _level: 1, + name: "sap.ui.lib1" + }) + ] + }) + ] + }); + + const tree = await normalizer.generateProjectTree(); + + t.deepEqual(tree, expectedTree, "Returned tree should be correct"); + }); +} + +defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { + frameworkName: "SAPUI5" +}); +defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { + frameworkName: "SAPUI5", + verbose: true +}); +defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { + frameworkName: "OpenUI5" +}); +defineTest("ui5Framework translator should enhance tree with UI5 framework libraries", { + frameworkName: "OpenUI5", + verbose: true +}); + +function defineErrorTest(testName, { + frameworkName, + failExtract = false, + failMetadata = false, + expectedErrorMessage +}) { + test.serial(testName, async (t) => { + const translatorTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [] + }; + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + + sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "readConfigFile") + .callsFake(async (configPath) => { + throw new Error("ProjectPreprocessor#readConfigFile stub called with unknown configPath: " + configPath); + }) + .withArgs(path.join(fakeBaseDir, "application-project", "ui5.yaml")) + .resolves([{ + specVersion: "1.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + } + ] + } + }]); + + // Prevent applying types as this would require a lot of mocking + sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "applyType"); + + const extractStub = sinon.stub(pacote, "extract"); + extractStub.callsFake(async (spec) => { + throw new Error("pacote.extract stub called with unknown spec: " + spec); + }); + + const manifestStub = sinon.stub(pacote, "manifest"); + manifestStub.callsFake(async (spec) => { + throw new Error("pacote.manifest stub called with unknown spec: " + spec); + }); + + if (frameworkName === "SAPUI5") { + if (failExtract) { + extractStub + .withArgs("@sapui5/sap.ui.lib1@1.75.1") + .rejects(new Error("Failed extracting package @sapui5/sap.ui.lib1@1.75.1")) + .withArgs("@openui5/sap.ui.lib4@1.75.4") + .rejects(new Error("Failed extracting package @openui5/sap.ui.lib4@1.75.4")); + } else { + extractStub + .withArgs("@sapui5/sap.ui.lib1@1.75.1").resolves() + .withArgs("@openui5/sap.ui.lib4@1.75.4").resolves(); + } + if (failMetadata) { + extractStub + .withArgs("@sapui5/distribution-metadata@1.75.0") + .rejects(new Error("Failed extracting package @sapui5/distribution-metadata@1.75.0")); + } else { + extractStub + .withArgs("@sapui5/distribution-metadata@1.75.0") + .resolves(); + sinon.stub(Installer.prototype, "readJson") + .callThrough() + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves({ + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib2": { + npmPackageName: "@sapui5/sap.ui.lib2", + version: "1.75.2", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [] + }, + "sap.ui.lib3": { + npmPackageName: "@sapui5/sap.ui.lib3", + version: "1.75.3", + dependencies: [], + optionalDependencies: [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + } + } + }); + } + } else if (frameworkName === "OpenUI5") { + if (failExtract) { + extractStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .rejects(new Error("Failed extracting package @openui5/sap.ui.lib1@1.75.0")) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .rejects(new Error("Failed extracting package @openui5/sap.ui.lib4@1.75.0")); + } else { + extractStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves() + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves(); + } + if (failMetadata) { + manifestStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib1@1.75.0")) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib4@1.75.0")); + } else { + manifestStub + .withArgs("@openui5/sap.ui.lib1@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: {} + }) + .withArgs("@openui5/sap.ui.lib4@1.75.0") + .resolves({ + name: "@openui5/sap.ui.lib4", + version: "1.75.0" + }); + } + } + + await t.throwsAsync(async () => { + await normalizer.generateProjectTree(); + }, expectedErrorMessage); + }); +} + +defineErrorTest("SAPUI5: ui5Framework translator should throw a proper error when metadata request fails", { + frameworkName: "SAPUI5", + failMetadata: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed extracting package @sapui5/distribution-metadata@1.75.0 +Failed to resolve library sap.ui.lib4: Failed extracting package @sapui5/distribution-metadata@1.75.0` // TODO: should only be returned once? +}); +defineErrorTest("SAPUI5: ui5Framework translator should throw a proper error when package extraction fails", { + frameworkName: "SAPUI5", + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed extracting package @sapui5/sap.ui.lib1@1.75.1 +Failed to resolve library sap.ui.lib4: Failed extracting package @openui5/sap.ui.lib4@1.75.4` +}); +defineErrorTest("SAPUI5: ui5Framework translator should throw a proper error when metadata request and package extraction fails", { + frameworkName: "SAPUI5", + failMetadata: true, + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed extracting package @sapui5/distribution-metadata@1.75.0 +Failed to resolve library sap.ui.lib4: Failed extracting package @sapui5/distribution-metadata@1.75.0` +}); + + +defineErrorTest("OpenUI5: ui5Framework translator should throw a proper error when metadata request fails", { + frameworkName: "OpenUI5", + failMetadata: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 +Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` +}); +defineErrorTest("OpenUI5: ui5Framework translator should throw a proper error when package extraction fails", { + frameworkName: "OpenUI5", + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed extracting package @openui5/sap.ui.lib1@1.75.0 +Failed to resolve library sap.ui.lib4: Failed extracting package @openui5/sap.ui.lib4@1.75.0` +}); +defineErrorTest("OpenUI5: ui5Framework translator should throw a proper error when metadata request and package extraction fails", { + frameworkName: "OpenUI5", + failMetadata: true, + failExtract: true, + expectedErrorMessage: `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0 +Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0` +}); + +test.serial("ui5Framework translator should not be called when no framework configuration is given", async (t) => { + const translatorTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [] + }; + const projectPreprocessorTree = Object.assign({}, translatorTree, { + specVersion: "1.1", + type: "application", + metadata: { + name: "test-project" + } + }); + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); + + const ui5FrameworkMock = sinon.mock(ui5Framework); + ui5FrameworkMock.expects("generateDependencyTree").never(); + + const expectedTree = projectPreprocessorTree; + + const tree = await normalizer.generateProjectTree(); + + t.deepEqual(tree, expectedTree, "Returned tree should be correct"); + ui5FrameworkMock.verify(); +}); + +test.serial("ui5Framework translator should not try to install anything when no library is referenced", async (t) => { + const translatorTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [] + }; + const projectPreprocessorTree = Object.assign({}, translatorTree, { + specVersion: "1.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5", + version: "1.75.0" + } + }); + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); + + const extractStub = sinon.stub(pacote, "extract"); + const manifestStub = sinon.stub(pacote, "manifest"); + + await normalizer.generateProjectTree(); + + t.is(extractStub.callCount, 0, "No package should be extracted"); + t.is(manifestStub.callCount, 0, "No manifest should be requested"); +}); + +test.serial("ui5Framework translator should throw an error when framework version is not defined", async (t) => { + const translatorTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [] + }; + const projectPreprocessorTree = Object.assign({}, translatorTree, { + specVersion: "1.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5" + } + }); + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); + + await t.throwsAsync(async () => { + await normalizer.generateProjectTree(); + }, `framework.version is not defined for project test-id`); +}); + +test.serial("ui5Framework translator should throw an error when framework name is not supported", async (t) => { + const translatorTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [] + }; + const projectPreprocessorTree = Object.assign({}, translatorTree, { + specVersion: "1.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "UI5" + } + }); + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); + + await t.throwsAsync(async () => { + await normalizer.generateProjectTree(); + }, `Unknown framework.name "UI5" for project test-id. Must be "OpenUI5" or "SAPUI5"`); +}); + +test.serial("SAPUI5: ui5Framework translator should throw error when using a library that is not part of the dist metadata", async (t) => { + const translatorTree = { + id: "test-id", + version: "1.2.3", + path: path.join(fakeBaseDir, "application-project"), + dependencies: [] + }; + const projectPreprocessorTree = Object.assign({}, translatorTree, { + specVersion: "1.1", + type: "application", + metadata: { + name: "test-project" + }, + framework: { + name: "SAPUI5", + version: "1.75.0", + libraries: [ + {name: "sap.ui.lib1"}, + {name: "does.not.exist"}, + {name: "sap.ui.lib4"}, + ] + } + }); + + sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); + sinon.stub(projectPreprocessor, "processTree").withArgs(translatorTree).resolves(projectPreprocessorTree); + + sinon.stub(pacote, "extract").resolves(); + + sinon.stub(Installer.prototype, "readJson") + .callThrough() + .withArgs(path.join(fakeBaseDir, + "homedir", ".ui5", "framework", "packages", + "@sapui5", "distribution-metadata", "1.75.0", + "metadata.json")) + .resolves({ + libraries: { + "sap.ui.lib1": { + npmPackageName: "@sapui5/sap.ui.lib1", + version: "1.75.1", + dependencies: [], + optionalDependencies: [] + }, + "sap.ui.lib4": { + npmPackageName: "@openui5/sap.ui.lib4", + version: "1.75.4", + dependencies: [ + "sap.ui.lib1" + ], + optionalDependencies: [] + } + } + }); + + await t.throwsAsync(async () => { + await normalizer.generateProjectTree(); + }, `Resolution of framework libraries failed with errors: +Failed to resolve library does.not.exist: Could not find library "does.not.exist"`); +}); + +// TODO test: Should not download packages again in case they are already installed + +// TODO test: Should ignore framework libraries in dependencies diff --git a/test/lib/translators/ui5Framework.js b/test/lib/translators/ui5Framework.js new file mode 100644 index 000000000..919084499 --- /dev/null +++ b/test/lib/translators/ui5Framework.js @@ -0,0 +1,316 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +let ui5Framework; +let utils; + +test.beforeEach((t) => { + t.context.Sapui5ResolverStub = sinon.stub(); + t.context.Sapui5ResolverInstallStub = sinon.stub(); + t.context.Sapui5ResolverStub.callsFake(() => { + return { + install: t.context.Sapui5ResolverInstallStub + }; + }); + mock("../../../lib/ui5Framework/Sapui5Resolver", t.context.Sapui5ResolverStub); + + t.context.Openui5ResolverStub = sinon.stub(); + mock("../../../lib/ui5Framework/Openui5Resolver", t.context.Openui5ResolverStub); + + ui5Framework = mock.reRequire("../../../lib/translators/ui5Framework"); + utils = ui5Framework._utils; +}); + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test.serial("generateDependencyTree", async (t) => { + const tree = { + id: "test1", + version: "1.0.0", + path: "/test-project/", + framework: { + name: "SAPUI5", + version: "1.75.0" + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + const getFrameworkLibrariesFromTreeStub = sinon.stub(utils, "getFrameworkLibrariesFromTree") + .returns(referencedLibraries); + + t.context.Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + const getProjectStub = sinon.stub(); + getProjectStub.onFirstCall().returns({fake: "metadata-project-1"}); + getProjectStub.onSecondCall().returns({fake: "metadata-project-2"}); + getProjectStub.onThirdCall().returns({fake: "metadata-project-3"}); + const ProjectProcessorStub = sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + getProject: getProjectStub + }; + }); + + const ui5FrameworkTree = await ui5Framework.generateDependencyTree(tree); + + t.is(getFrameworkLibrariesFromTreeStub.callCount, 1, "getFrameworkLibrariesFromTree should be called once"); + t.deepEqual(getFrameworkLibrariesFromTreeStub.getCall(0).args, [tree], + "getFrameworkLibrariesFromTree should be called with expected args"); + + t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{cwd: tree.path, version: tree.framework.version}], + "Sapui5Resolver#constructor should be called with expected args"); + + t.is(t.context.Sapui5ResolverInstallStub.callCount, 1, "Sapui5Resolver#install should be called once"); + t.deepEqual(t.context.Sapui5ResolverInstallStub.getCall(0).args, [referencedLibraries], + "Sapui5Resolver#install should be called with expected args"); + + t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once"); + t.deepEqual(ProjectProcessorStub.getCall(0).args, [{libraryMetadata}], + "ProjectProcessor#constructor should be called with expected args"); + + t.is(getProjectStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times"); + t.deepEqual(getProjectStub.getCall(0).args, [referencedLibraries[0]], + "Sapui5Resolver#getProject should be called with expected args (call 1)"); + t.deepEqual(getProjectStub.getCall(1).args, [referencedLibraries[1]], + "Sapui5Resolver#getProject should be called with expected args (call 2)"); + t.deepEqual(getProjectStub.getCall(2).args, [referencedLibraries[2]], + "Sapui5Resolver#getProject should be called with expected args (call 3)"); + + t.deepEqual(ui5FrameworkTree, { + id: "test1", + version: "1.0.0", + path: "/test-project/", + dependencies: [ + {fake: "metadata-project-1"}, + {fake: "metadata-project-2"}, + {fake: "metadata-project-3"} + ] + }); +}); + +test.serial("generateDependencyTree (with versionOverride)", async (t) => { + const tree = { + id: "test1", + version: "1.0.0", + path: "/test-project/", + framework: { + name: "SAPUI5", + version: "1.75.0" + } + }; + + const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]; + const libraryMetadata = {fake: "metadata"}; + + sinon.stub(utils, "getFrameworkLibrariesFromTree").returns(referencedLibraries); + + t.context.Sapui5ResolverInstallStub.resolves({libraryMetadata}); + + const getProjectStub = sinon.stub(); + getProjectStub.onFirstCall().returns({fake: "metadata-project-1"}); + getProjectStub.onSecondCall().returns({fake: "metadata-project-2"}); + getProjectStub.onThirdCall().returns({fake: "metadata-project-3"}); + sinon.stub(utils, "ProjectProcessor") + .callsFake(() => { + return { + getProject: getProjectStub + }; + }); + + await ui5Framework.generateDependencyTree(tree, {versionOverride: "1.99.0"}); + + t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once"); + t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{cwd: tree.path, version: "1.99.0"}], + "Sapui5Resolver#constructor should be called with expected args"); +}); + +test.serial("generateDependencyTree should throw error when no framework version is provided in tree", async (t) => { + const tree = { + id: "test-id", + version: "1.2.3", + path: "/test-project/", + metadata: { + name: "test-name" + }, + framework: { + name: "SAPUI5" + } + }; + + await t.throwsAsync(async () => { + await ui5Framework.generateDependencyTree(tree); + }, "framework.version is not defined for project test-id"); + + await t.throwsAsync(async () => { + await ui5Framework.generateDependencyTree(tree, { + versionOverride: "1.75.0" + }); + }, "framework.version is not defined for project test-id"); +}); + +test.serial("generateDependencyTree should ignore root project without framework configuration", async (t) => { + const tree = { + id: "test-id", + version: "1.2.3", + path: "/test-project/", + metadata: { + name: "test-name" + }, + dependencies: [] + }; + const ui5FrameworkTree = await ui5Framework.generateDependencyTree(tree); + + t.is(ui5FrameworkTree, null, "No framework tree should be returned"); +}); +test.serial("utils.isFrameworkProject", (t) => { + t.true(utils.isFrameworkProject({id: "@sapui5/foo"}), "@sapui5/foo"); + t.true(utils.isFrameworkProject({id: "@openui5/foo"}), "@openui5/foo"); + t.false(utils.isFrameworkProject({id: "sapui5"}), "sapui5"); + t.false(utils.isFrameworkProject({id: "openui5"}), "openui5"); +}); +test.serial("utils.shouldIncludeDependency", (t) => { + // root project dependency should always be included + t.true(utils.shouldIncludeDependency({}, true)); + t.true(utils.shouldIncludeDependency({optional: true}, true)); + t.true(utils.shouldIncludeDependency({optional: false}, true)); + t.true(utils.shouldIncludeDependency({optional: null}, true)); + t.true(utils.shouldIncludeDependency({optional: "abc"}, true)); + t.true(utils.shouldIncludeDependency({development: true}, true)); + t.true(utils.shouldIncludeDependency({development: false}, true)); + t.true(utils.shouldIncludeDependency({development: null}, true)); + t.true(utils.shouldIncludeDependency({development: "abc"}, true)); + t.true(utils.shouldIncludeDependency({foo: true}, true)); + + t.true(utils.shouldIncludeDependency({}, false)); + t.false(utils.shouldIncludeDependency({optional: true}, false)); + t.true(utils.shouldIncludeDependency({optional: false}, false)); + t.true(utils.shouldIncludeDependency({optional: null}, false)); + t.true(utils.shouldIncludeDependency({optional: "abc"}, false)); + t.false(utils.shouldIncludeDependency({development: true}, false)); + t.true(utils.shouldIncludeDependency({development: false}, false)); + t.true(utils.shouldIncludeDependency({development: null}, false)); + t.true(utils.shouldIncludeDependency({development: "abc"}, false)); + t.true(utils.shouldIncludeDependency({foo: true}, false)); + + // Having both optional and development should not be the case, but that should be validated beforehand + t.true(utils.shouldIncludeDependency({optional: true, development: true}, true)); + t.false(utils.shouldIncludeDependency({optional: true, development: true}, false)); +}); +test.serial("utils.getFrameworkLibrariesFromTree: Project without dependencies", (t) => { + const tree = { + id: "test", + metadata: { + name: "test" + }, + framework: { + libraries: [] + }, + dependencies: [] + }; + const ui5Dependencies = utils.getFrameworkLibrariesFromTree(tree); + t.deepEqual(ui5Dependencies, []); +}); + +test.serial("utils.getFrameworkLibrariesFromTree: Project with libraries and dependency with libraries", (t) => { + const tree = { + id: "test1", + metadata: { + name: "test1" + }, + framework: { + libraries: [ + { + name: "lib1" + }, + { + name: "lib2", + optional: true + }, + { + name: "lib6", + development: true + } + ] + }, + dependencies: [ + { + id: "test2", + metadata: { + name: "test2" + }, + framework: { + libraries: [ + { + name: "lib3" + }, + { + name: "lib4", + optional: true + } + ] + }, + dependencies: [ + { + id: "test3", + metadata: { + name: "test3" + }, + framework: { + libraries: [ + { + name: "lib5" + }, + { + name: "lib7", + development: true + } + ] + }, + dependencies: [] + } + ] + }, + { + id: "@sapui5/lib8", + metadata: { + name: "lib8" + }, + framework: { + libraries: [ + { + name: "should.be.ignored" + } + ] + }, + dependencies: [] + }, + { + id: "@openui5/lib9", + metadata: { + name: "lib9" + }, + dependencies: [] + }, + { + id: "@foo/library", + metadata: { + name: "foo.library" + }, + dependencies: [] + } + ] + }; + const ui5Dependencies = utils.getFrameworkLibrariesFromTree(tree); + t.deepEqual(ui5Dependencies, ["lib1", "lib2", "lib6", "lib3", "lib5"]); +}); + +// TODO test: utils.getAllNodesOfTree + +// TODO test: ProjectProcessor diff --git a/test/lib/ui5framework/AbstractResolver.js b/test/lib/ui5framework/AbstractResolver.js new file mode 100644 index 000000000..6117e4ae6 --- /dev/null +++ b/test/lib/ui5framework/AbstractResolver.js @@ -0,0 +1,263 @@ +const test = require("ava"); +const sinon = require("sinon"); +const path = require("path"); + +const AbstractResolver = require("../../../lib/ui5Framework/AbstractResolver"); + +class MyResolver extends AbstractResolver {} + +test("AbstractResolver: abstract constructor should throw", async (t) => { + await t.throwsAsync(async () => { + new AbstractResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + }, `Class 'AbstractResolver' is abstract`); +}); + +test("AbstractResolver: constructor", (t) => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + t.true(resolver instanceof MyResolver, "Constructor returns instance of sub-class"); + t.true(resolver instanceof AbstractResolver, "Constructor returns instance of abstract class"); +}); + +test("AbstractResolver: constructor requires 'version'", (t) => { + t.throws(() => { + new MyResolver({}); + }, `AbstractResolver: Missing parameter "version"`); +}); + +test("AbstractResolver: Set 'cwd'", (t) => { + const resolver = new MyResolver({ + version: "1.75.0", + cwd: "/my-cwd/" + }); + t.is(resolver._cwd, "/my-cwd/", "Should be given 'cwd'"); +}); + +test("AbstractResolver: Defaults 'cwd' to process.cwd()", (t) => { + const resolver = new MyResolver({ + version: "1.75.0", + ui5HomeDir: "/ui5home/" + }); + t.is(resolver._cwd, process.cwd(), "Should default to process.cwd()"); +}); + +test("AbstractResolver: Set 'ui5HomeDir'", (t) => { + const resolver = new MyResolver({ + version: "1.75.0", + ui5HomeDir: "/my-ui5HomeDir/" + }); + t.is(resolver._ui5HomeDir, "/my-ui5HomeDir/", "Should be given 'ui5HomeDir'"); +}); + +test("AbstractResolver: Defaults 'ui5HomeDir' to ~/.ui5", (t) => { + const resolver = new MyResolver({ + version: "1.75.0", + cwd: "/test-project/" + }); + t.is(resolver._ui5HomeDir, path.join(require("os").homedir(), ".ui5"), "Should default to ~/.ui5"); +}); + +test("AbstractResolver: handleLibrary should throw an Error when not implemented", async (t) => { + await t.throwsAsync(async () => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + await resolver.handleLibrary(); + }, `AbstractResolver: handleLibrary must be implemented!`); +}); + +test("AbstractResolver: install", async (t) => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const metadata = { + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }, + "sap.ui.lib2": { + "npmPackageName": "@openui5/sap.ui.lib2", + "version": "1.75.0", + "dependencies": [ + "sap.ui.lib3" + ], + "optionalDependencies": [] + }, + "sap.ui.lib3": { + "npmPackageName": "@openui5/sap.ui.lib3", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [ + "sap.ui.lib4" + ] + }, + "sap.ui.lib4": { + "npmPackageName": "@openui5/sap.ui.lib4", + "version": "1.75.0", + "dependencies": [ + "sap.ui.lib1" + ], + "optionalDependencies": [] + } + } + }; + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib1"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }) + .withArgs("sap.ui.lib2").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib2"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib2"}) + }) + .withArgs("sap.ui.lib3").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib3"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib3"}) + }) + .withArgs("sap.ui.lib4").resolves({ + metadata: Promise.resolve(metadata.libraries["sap.ui.lib4"]), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib4"}) + }); + + await resolver.install(["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib4"]); + + t.is(handleLibraryStub.callCount, 4, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling (rejection of metadata/install)", async (t) => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib1")), + install: Promise.reject(new Error("Error installing sap.ui.lib1")) + }) + .withArgs("sap.ui.lib2").resolves({ + metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib2")), + install: Promise.reject(new Error("Error installing sap.ui.lib2")) + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]); + }, `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Error installing sap.ui.lib1 +Failed to resolve library sap.ui.lib2: Error installing sap.ui.lib2`); + + t.is(handleLibraryStub.callCount, 2, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling (rejection of dependency metadata/install)", async (t) => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.resolve({ + dependencies: ["sap.ui.lib2"] + }), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }) + .withArgs("sap.ui.lib2").resolves({ + metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib2")), + install: Promise.reject(new Error("Error installing sap.ui.lib2")) + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1"]); + }, `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib2: Error installing sap.ui.lib2`); + + t.is(handleLibraryStub.callCount, 2, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling (rejection of dependency install)", async (t) => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Unknown handleLibrary call: ${libraryName}`); + }) + .withArgs("sap.ui.lib1").resolves({ + metadata: Promise.resolve({ + dependencies: ["sap.ui.lib2"] + }), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }) + .withArgs("sap.ui.lib2").callsFake(() => { + return { + metadata: Promise.resolve({ + dependencies: ["sap.ui.lib3"] + }), + install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"}) + }; + }) + .withArgs("sap.ui.lib3").callsFake(() => { + return { + metadata: Promise.resolve({ + dependencies: [] + }), + install: Promise.reject(new Error("Error installing sap.ui.lib3")) + }; + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1"]); + }, `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib3: Error installing sap.ui.lib3`); + + t.is(handleLibraryStub.callCount, 3, "Each library should be handled once"); +}); + +test("AbstractResolver: install error handling (handleLibrary throws error)", async (t) => { + const resolver = new MyResolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const handleLibraryStub = sinon.stub(resolver, "handleLibrary"); + handleLibraryStub + .callsFake(async (libraryName) => { + throw new Error(`Error within handleLibrary: ${libraryName}`); + }); + + await t.throwsAsync(async () => { + await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]); + }, `Resolution of framework libraries failed with errors: +Failed to resolve library sap.ui.lib1: Error within handleLibrary: sap.ui.lib1 +Failed to resolve library sap.ui.lib2: Error within handleLibrary: sap.ui.lib2`); + + t.is(handleLibraryStub.callCount, 2, "Each library should be handled once"); +}); diff --git a/test/lib/ui5framework/Openui5Resolver.js b/test/lib/ui5framework/Openui5Resolver.js new file mode 100644 index 000000000..1f8c0e0d1 --- /dev/null +++ b/test/lib/ui5framework/Openui5Resolver.js @@ -0,0 +1,113 @@ +const test = require("ava"); +const sinon = require("sinon"); + +const Openui5Resolver = require("../../../lib/ui5Framework/Openui5Resolver"); + +test("Openui5Resolver: _getNpmPackageName", (t) => { + t.is(Openui5Resolver._getNpmPackageName("foo"), "@openui5/foo"); +}); + +test("Openui5Resolver: _getLibaryName", (t) => { + t.is(Openui5Resolver._getLibaryName("@openui5/foo"), "foo"); + t.is(Openui5Resolver._getLibaryName("@something/else"), "@something/else"); +}); + +test("Openui5Resolver: _getLibraryMetadata", async (t) => { + const resolver = new Openui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const fetchPackageManifest = sinon.stub(resolver._installer, "fetchPackageManifest"); + fetchPackageManifest + .callsFake(async ({pkgName}) => { + throw new Error(`Unknown install call: ${pkgName}`); + }) + .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({}) + .withArgs({pkgName: "@openui5/sap.ui.lib2", version: "1.75.0"}).resolves({ + dependencies: { + "sap.ui.lib3": "1.2.3" + }, + devDependencies: { + "sap.ui.lib4": "4.5.6" + } + }); + + async function assert(libraryName, expectedMetadata) { + const pLibraryMetadata = resolver._getLibraryMetadata(libraryName); + const pLibraryMetadata2 = resolver._getLibraryMetadata(libraryName); + + const libraryMetadata = await pLibraryMetadata; + t.deepEqual(libraryMetadata, expectedMetadata, + libraryName + ": First call should resolve with expected metadata"); + const libraryMetadata2 = await pLibraryMetadata2; + t.deepEqual(libraryMetadata2, expectedMetadata, + libraryName + ": Second call should also resolve with expected metadata"); + + const libraryMetadata3 = await resolver._getLibraryMetadata(libraryName); + + t.deepEqual(libraryMetadata3, expectedMetadata, + libraryName + ": Third call should still return the same metadata"); + } + + await assert("sap.ui.lib1", { + id: "@openui5/sap.ui.lib1", + version: "1.75.0", + dependencies: [], + optionalDependencies: [] + }); + + await assert("sap.ui.lib2", { + id: "@openui5/sap.ui.lib2", + version: "1.75.0", + dependencies: [ + "sap.ui.lib3" + ], + optionalDependencies: [ + "sap.ui.lib4" + ] + }); + + t.is(fetchPackageManifest.callCount, 2, "fetchPackageManifest should be called twice"); +}); + +test("Openui5Resolver: handleLibrary", async (t) => { + const resolver = new Openui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const getLibraryMetadataStub = sinon.stub(resolver, "_getLibraryMetadata"); + getLibraryMetadataStub + .callsFake(async (libraryName) => { + throw new Error("_getLibraryMetadata stub called with unknown libraryName: " + libraryName); + }) + .withArgs("sap.ui.lib1").resolves({ + "id": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }); + + const _installPackage = sinon.stub(resolver._installer, "installPackage"); + _installPackage + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({pkgPath: "/foo/sap.ui.lib1"}); + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); +}); diff --git a/test/lib/ui5framework/Sapui5Resolver.js b/test/lib/ui5framework/Sapui5Resolver.js new file mode 100644 index 000000000..bd8b7d7e3 --- /dev/null +++ b/test/lib/ui5framework/Sapui5Resolver.js @@ -0,0 +1,96 @@ +const test = require("ava"); +const sinon = require("sinon"); +const path = require("path"); + +const Sapui5Resolver = require("../../../lib/ui5Framework/Sapui5Resolver"); + +test.serial("Sapui5Resolver: loadDistMetadata loads metadata once from @sapui5/distribution-metadata package", async (t) => { + const resolver = new Sapui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const getTargetDirForPackage = sinon.stub(resolver._installer, "_getTargetDirForPackage"); + getTargetDirForPackage.callsFake(({pkgName, version}) => { + throw new Error(`_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}`); + }); + getTargetDirForPackage.withArgs({ + pkgName: "@sapui5/distribution-metadata", + version: "1.75.0" + }).returns(path.join("/path", "to", "distribution-metadata", "1.75.0")); + const installPackage = sinon.stub(resolver._installer, "installPackage"); + installPackage.withArgs({ + pkgName: "@sapui5/distribution-metadata", + version: "1.75.0" + }).resolves({pkgPath: path.join("/path", "to", "distribution-metadata", "1.75.0")}); + + const expectedMetadata = { + libraries: { + "sap.ui.foo": { + "npmPackageName": "@openui5/sap.ui.foo", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + } + } + }; + sinon.stub(resolver._installer, "readJson") + .callThrough() + .withArgs(path.join("/path", "to", "distribution-metadata", "1.75.0", "metadata.json")) + .resolves(expectedMetadata); + + let distMetadata = await resolver.loadDistMetadata(); + t.is(installPackage.callCount, 1, "Distribution metadata package should be installed once"); + t.deepEqual(distMetadata, expectedMetadata, + "loadDistMetadata should resolve with expected metadata"); + + // Calling loadDistMetadata again should not load package again + distMetadata = await resolver.loadDistMetadata(); + + t.is(installPackage.callCount, 1, "Distribution metadata package should still be installed once"); + t.deepEqual(distMetadata, expectedMetadata, + "Metadata should still be the expected metadata after calling loadDistMetadata again"); +}); + +test("Sapui5Resolver: handleLibrary", async (t) => { + const resolver = new Sapui5Resolver({ + cwd: "/test-project/", + version: "1.75.0" + }); + + const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata"); + loadDistMetadataStub.resolves({ + libraries: { + "sap.ui.lib1": { + "npmPackageName": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + } + } + }); + + const installPackage = sinon.stub(resolver._installer, "installPackage"); + installPackage + .callsFake(async ({pkgName, version}) => { + throw new Error(`Unknown install call: ${pkgName}@${version}`); + }) + .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({pkgPath: "/foo/sap.ui.lib1"}); + + + const promises = await resolver.handleLibrary("sap.ui.lib1"); + + t.true(promises.metadata instanceof Promise, "Metadata promise should be returned"); + t.true(promises.install instanceof Promise, "Install promise should be returned"); + + const metadata = await promises.metadata; + t.deepEqual(metadata, { + "id": "@openui5/sap.ui.lib1", + "version": "1.75.0", + "dependencies": [], + "optionalDependencies": [] + }, "Expected library metadata should be returned"); + + t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object"); + t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once"); +}); diff --git a/test/lib/ui5framework/npm/Installer.js b/test/lib/ui5framework/npm/Installer.js new file mode 100644 index 000000000..66caa47d6 --- /dev/null +++ b/test/lib/ui5framework/npm/Installer.js @@ -0,0 +1,370 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const path = require("path"); + +const lockfile = require("lockfile"); + +let Installer; + +test.beforeEach((t) => { + t.context.mkdirpStub = sinon.stub().resolves(); + mock("mkdirp", t.context.mkdirpStub); + + t.context.lockStub = sinon.stub(lockfile, "lock"); + t.context.unlockStub = sinon.stub(lockfile, "unlock"); + + // Re-require to ensure that mocked modules are used + Installer = mock.reRequire("../../../../lib/ui5Framework/npm/Installer"); +}); + +test.afterEach.always(() => { + sinon.restore(); + mock.stopAll(); +}); + +test.serial("Installer: constructor", (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + t.true(installer instanceof Installer, "Constructor returns instance of class"); + t.is(installer._baseDir, path.join("/ui5Home/", "framework", "packages")); + t.is(installer._lockDir, path.join("/ui5Home/", "framework", "locks")); +}); + +test.serial("Installer: constructor requires 'cwd'", (t) => { + t.throws(() => { + new Installer({}); + }, `Installer: Missing parameter "cwd"`); +}); + +test.serial("Installer: constructor requires 'ui5HomeDir'", (t) => { + t.throws(() => { + new Installer({ + cwd: "/cwd/" + }); + }, `Installer: Missing parameter "ui5HomeDir"`); +}); + +test.serial("Installer: _getLockPath", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + const lockPath = installer._getLockPath({ + pkgName: "@openui5/sap.ui.lib1", + version: "1.2.3" + }); + + t.is(lockPath, path.join("/ui5Home/", "framework", "locks", "package-@openui5-sap.ui.lib1@1.2.3.lock")); +}); + +test.serial("Installer: fetchPackageManifest (without existing package.json)", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + const mockedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + }, + foo: "bar" + }; + + const expectedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + } + }; + + const requestPackageManifestStub = sinon.stub(installer._registry, "requestPackageManifest") + .callsFake((pkgName, version) => { + throw new Error( + "_registry.requestPackageManifest stub called with unknown arguments " + + `pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs("myPackage", "1.2.3").resolves(mockedManifest); + + const readJsonStub = sinon.stub(installer, "readJson") + .callsFake((path) => { + throw new Error( + `readJson stub called with unknown path: ${path}` + ); + }) + .withArgs(path.join("/path", "to", "myPackage", "1.2.3", "package.json")) + .callsFake(async (path) => { + const error = new Error(`ENOENT: no such file or directory, open '${path}'`); + error.code = "ENOENT"; + throw error; + }); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .callsFake(({pkgName, version}) => { + throw new Error( + `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs({ + pkgName: "myPackage", + version: "1.2.3" + }).returns(path.join("/path", "to", "myPackage", "1.2.3")); + + const manifest = await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"}); + + t.deepEqual(manifest, expectedManifest, "Should return expected manifest object"); + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.is(readJsonStub.callCount, 1, "readJson should be called once"); + t.is(requestPackageManifestStub.callCount, 1, "requestPackageManifest should be called once"); +}); + +test.serial("Installer: fetchPackageManifest (with existing package.json)", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + const mockedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + }, + foo: "bar" + }; + + const expectedManifest = { + name: "myPackage", + dependencies: { + "foo": "1.2.3" + }, + devDependencies: { + "bar": "4.5.6" + } + }; + + const requestPackageManifestStub = sinon.stub(installer._registry, "requestPackageManifest") + .rejects(new Error("Unexpected call")); + + const readJsonStub = sinon.stub(installer, "readJson") + .callsFake((path) => { + throw new Error( + `readJson stub called with unknown path: ${path}` + ); + }) + .withArgs(path.join("/path", "to", "myPackage", "1.2.3", "package.json")) + .resolves(mockedManifest); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .callsFake(({pkgName, version}) => { + throw new Error( + `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs({ + pkgName: "myPackage", + version: "1.2.3" + }).returns(path.join("/path", "to", "myPackage", "1.2.3")); + + const manifest = await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"}); + + t.deepEqual(manifest, expectedManifest, "Should return expected manifest object"); + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.is(readJsonStub.callCount, 1, "readJson should be called once"); + t.is(requestPackageManifestStub.callCount, 0, "requestPackageManifest should not be called"); +}); + +test.serial("Installer: fetchPackageManifest (readJson throws error)", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + const requestPackageManifestStub = sinon.stub(installer._registry, "requestPackageManifest") + .rejects(new Error("Unexpected call")); + + const readJsonStub = sinon.stub(installer, "readJson") + .rejects(new Error("Error from readJson")); + + const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage") + .callsFake(({pkgName, version}) => { + throw new Error( + `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}` + ); + }) + .withArgs({ + pkgName: "myPackage", + version: "1.2.3" + }).returns(path.join("/path", "to", "myPackage", "1.2.3")); + + await t.throwsAsync(async () => { + await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"}); + }, "Error from readJson"); + + t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once"); + t.is(readJsonStub.callCount, 1, "readJson should be called once"); + t.is(requestPackageManifestStub.callCount, 0, "requestPackageManifest should not be called"); +}); + +test.serial("Installer: _synchronize", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + t.context.mkdirpStub.resolves(); + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + const getLockPathStub = sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().resolves(); + + await installer._synchronize({ + pkgName: "@openui5/sap.ui.lib1", + version: "1.2.3" + }, callback); + + t.is(getLockPathStub.callCount, 1, "_getLockPath should be called once"); + t.deepEqual(getLockPathStub.getCall(0).args, [{pkgName: "@openui5/sap.ui.lib1", version: "1.2.3"}], + "_getLockPath should be called with expected args"); + + t.is(t.context.mkdirpStub.callCount, 1, "_mkdirp should be called once"); + t.deepEqual(t.context.mkdirpStub.getCall(0).args, [path.join("/ui5Home/", "framework", "locks")], + "_mkdirp should be called with expected args"); + + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.deepEqual(t.context.lockStub.getCall(0).args[0], "/locks/lockfile.lock", + "lock should be called with expected path"); + t.deepEqual(t.context.lockStub.getCall(0).args[1], {wait: 10000, stale: 60000, retries: 10}, + "lock should be called with expected options"); + + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); + t.deepEqual(t.context.unlockStub.getCall(0).args[0], "/locks/lockfile.lock", + "unlock should be called with expected path"); + + t.is(callback.callCount, 1, "callback should be called once"); + + t.true(t.context.lockStub.calledBefore(callback), "Lock should be called before invoking the callback"); + t.true(t.context.unlockStub.calledAfter(callback), "Unlock should be called after invoking the callback"); +}); + +test.serial("Installer: _synchronize should unlock when callback promise has resolved", async (t) => { + t.plan(4); + + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + t.context.mkdirpStub.resolves(); + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().callsFake(() => { + t.is(t.context.lockStub.callCount, 1, "lock should have been called when the callback is invoked"); + return Promise.resolve().then(() => { + t.is(t.context.unlockStub.callCount, 0, + "unlock should not be called when the callback did not fully resolve, yet"); + }); + }); + + await installer._synchronize({ + pkgName: "@openui5/sap.ui.lib1", + version: "1.2.3" + }, callback); + + t.is(callback.callCount, 1, "callback should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called after _synchronize has resolved"); +}); + +test.serial("Installer: _synchronize should throw when locking fails", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + t.context.mkdirpStub.resolves(); + t.context.lockStub.yieldsAsync(new Error("Locking error")); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub(); + + await t.throwsAsync(async () => { + await installer._synchronize({ + pkgName: "@openui5/sap.ui.lib1", + version: "1.2.3" + }, callback); + }, "Locking error"); + + t.is(callback.callCount, 0, "callback should not be called"); + t.is(t.context.unlockStub.callCount, 0, "unlock should not be called"); +}); + +test.serial("Installer: _synchronize should still unlock when callback throws an error", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + t.context.mkdirpStub.resolves(); + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().throws(new Error("Callback throws error")); + + await t.throwsAsync(async () => { + await installer._synchronize({ + pkgName: "@openui5/sap.ui.lib1", + version: "1.2.3" + }, callback); + }, "Callback throws error"); + + t.is(callback.callCount, 1, "callback should be called once"); + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); +}); + +test.serial("Installer: _synchronize should still unlock when callback rejects with error", async (t) => { + const installer = new Installer({ + cwd: "/cwd/", + ui5HomeDir: "/ui5Home/" + }); + + t.context.mkdirpStub.resolves(); + t.context.lockStub.yieldsAsync(); + t.context.unlockStub.yieldsAsync(); + + sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock"); + + const callback = sinon.stub().rejects(new Error("Callback rejects with error")); + + await t.throwsAsync(async () => { + await installer._synchronize({ + pkgName: "@openui5/sap.ui.lib1", + version: "1.2.3" + }, callback); + }, "Callback rejects with error"); + + t.is(callback.callCount, 1, "callback should be called once"); + t.is(t.context.lockStub.callCount, 1, "lock should be called once"); + t.is(t.context.unlockStub.callCount, 1, "unlock should be called once"); +});