@qooxdoo/framework
Version:
The JS Framework for Coders
643 lines (578 loc) • 20.5 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2017 Christian Boulanger
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Christian Boulanger (info@bibliograph.org, @cboulanger)
************************************************************************ */
const semver = require("semver");
const columnify = require("columnify");
const path = require("upath");
/**
* Lists compatible packages
*/
qx.Class.define("qx.tool.cli.commands.package.List", {
extend: qx.tool.cli.commands.Package,
statics: {
/**
* The name of a "fake" repository containing libraries from local paths
*/
localPathRepoName: "_local_",
/**
* Returns the yargs command data
* @return {Object}
*/
getYargsCommand() {
return {
command: "list [repository]",
describe:
'if no repository name is given, lists all available packages that are compatible with the project\'s qooxdoo version ("--all" lists incompatible ones as well). Otherwise, list all compatible packages.',
builder: {
all: {
alias: "a",
describe: "Show all versions, including incompatible ones"
},
verbose: {
alias: "v",
describe: "Verbose logging"
},
quiet: {
alias: "q",
describe: "No output"
},
json: {
alias: "j",
describe: "Output list as JSON literal"
},
installed: {
alias: "i",
describe: "Show only installed libraries"
},
namespace: {
alias: "n",
describe: "Display library namespace"
},
match: {
alias: "m",
describe: "Filter by regular expression (case-insensitive)"
},
libraries: {
alias: "l",
describe: "List libraries only (no repositories)"
},
short: {
alias: "s",
describe: "Omit title and description to make list more compact"
},
noheaders: {
alias: "H",
describe: "Omit header and footer"
},
prereleases: {
alias: "p",
describe: "Include prereleases into latest compatible releases"
},
"uris-only": {
alias: "u",
describe:
"Output only the GitHub URIs of the packages which are used to install the packages. Implies --noheaders and --libraries."
},
"qx-version": {
check: argv => semver.valid(argv.qxVersion),
describe:
"A semver string. If given, the qooxdoo version for which to generate the listings"
}
}
};
}
},
members: {
/**
* Lists library packages compatible with the current project
*/
async process() {
await super.process();
this.__repositories = [];
this.__libraries = {};
this.__latestCompatible = {};
const localPathRepoName = this.self(arguments).localPathRepoName;
let repos_cache = this.getCache().repos;
// implicit qx package update, disabled
// if (repos_cache.list.length === 0 || this.getCache().version !== qx.tool.config.Lockfile.getInstance().getVersion()) {
// await (new qx.tool.cli.commands.package.Update({quiet:true})).process();
// }
let qxVersion;
try {
qxVersion = await this.getAppQxVersion();
} catch (ex) {
qx.tool.compiler.Console.error(
`Cannot determine a qooxdoo version to show packages only for this version, because you are not in a project directory.`
);
process.exit(1);
}
let num_compat_repos = await this.__createIndexes(qxVersion);
if (this.argv.verbose) {
this.debug(
`>>> We have ${num_compat_repos} packages compatible with qooxdoo version ${qxVersion}`
);
}
if (num_compat_repos === 0 && !this.argv.all && !this.argv.quiet) {
qx.tool.compiler.Console.info(
`Currently, no packages compatible with qooxdoo version ${qxVersion} exist.`
);
return;
}
// detailed repo information
let repo = this.argv.repository;
if (repo) {
if (!repos_cache.list.includes(repo)) {
throw new qx.tool.utils.Utils.UserError(
`Repository ${repo} does not exist or is not a qooxdoo package repo.`
);
}
if (this.__libraries[repo] && this.__libraries[repo].length) {
let columnify_options = {
columnSplitter: " ",
config: {
description: { maxWidth: 60 },
compatibility: {
dataTransform(data) {
switch (data) {
case "false":
return "not compatible / untested";
case "true":
return "√";
default:
return "";
}
}
},
installedVersion: {
dataTransform(data) {
switch (data) {
case "false":
return "-";
default:
return data;
}
}
}
}
};
if (!this.argv.quiet) {
let data = this.__libraries[repo]
// shallow copy
.map(row => Object.assign({}, row))
// sort
.sort((a, b) => a.name.localeCompare(b.name));
let pretty = data
// another shallow copy
.map(row => Object.assign({}, row))
// clean up and omit redundant cell values
.map((row, index) => {
delete row.manifest;
if (index) {
let previousRow = data[index - 1];
for (let key of Object.getOwnPropertyNames(row).reverse()) {
if (
["compatibility", "required_qx_version"].indexOf(key) > -1
) {
continue;
}
if (
row[key] === previousRow[key] &&
row.name === previousRow.name
) {
row[key] = "";
}
}
}
return row;
});
// output list
if (this.argv.json) {
// as JSON
qx.tool.compiler.Console.info(JSON.stringify(data, null, 2));
} else {
qx.tool.compiler.Console.info(
columnify(pretty, columnify_options)
);
}
}
} else if (this.argv.verbose) {
qx.tool.compiler.Console.info(
`Repository ${repo} does not contain suitable qooxdoo libraries.`
);
}
return;
}
// list output
let columns;
if (this.argv.urisOnly) {
columns = ["uri"];
this.argv.noheaders = true;
this.argv.libraries = true;
} else if (this.argv.short) {
columns = [
"uri",
"installedVersion",
"latestVersion",
"latestCompatible"
];
} else {
columns = [
"uri",
"name",
"description",
"installedVersion",
"latestVersion",
"latestCompatible"
];
}
if (this.argv.namespace || this.argv.installed) {
columns.splice(1, 0, "namespace");
}
let columnify_options = {
showHeaders: !this.argv.noheaders,
columnSplitter: " ",
columns,
config: {
name: { maxWidth: 25 },
description: { maxWidth: 60 },
installedVersion: {
headingTransform: () => "INSTALLED",
dataTransform: data => (data === "false" ? "" : data)
},
latestVersion: {
headingTransform: () => "LATEST",
dataTransform: data => (data === "false" ? "-" : data)
},
latestCompatible: {
headingTransform: () => "COMPATIBLE",
dataTransform: data => (data === "false" ? "-" : data)
}
}
};
// filter by compatibility unless --all
let list = this.argv.all
? this.__repositories
: this.__repositories.filter(
item =>
item.latestCompatible ||
(this.argv.installed && item.name === localPathRepoName)
);
// sort
list.sort((l, r) => {
l = l.name.toLowerCase();
r = r.name.toLowerCase();
return l < r ? -1 : l > r ? 1 : 0;
});
// list all libraries contained in a repo
let expanded_list = [];
for (let repo of list) {
let repo_libs = [];
if (!qx.lang.Type.isArray(this.__libraries[repo.name])) {
continue;
}
for (let library of this.__libraries[repo.name]) {
if (!semver.valid(library.version)) {
if (this.argv.verbose) {
qx.tool.compiler.Console.warn(
`>>> Ignoring '${repo.name}' ${library.name}': invalid version format '${library.version}'.`
);
}
continue;
}
if (
repo.name === localPathRepoName ||
semver.eq(library.version, repo.latestVersion)
) {
let uri =
repo.name === this.self(arguments).localPathRepoName
? library.path
: path.join(repo.name, library.path || "");
repo_libs.push({
type: "library",
uri,
namespace: library.namespace,
name: library.name,
description: library.summary || repo.description,
installedVersion: library.installedVersion,
latestVersion: repo.latestVersion,
latestCompatible: repo.latestCompatible,
manifest: library.manifest
});
}
}
// add title to multiple-library repos
if (
repo_libs.length > 1 &&
!(
this.argv.libraries ||
this.argv.short ||
repo.name === localPathRepoName
)
) {
expanded_list.push({
type: "repository",
uri: repo.name,
name: "",
description: repo.description,
installedVersion: "",
latestVersion: repo.latestVersion,
latestCompatible: repo.latestCompatible
});
if (
!this.argv.json &&
!this.argv.installed &&
!this.argv.match &&
!this.argv.urisOnly
) {
// add an indent to group libraries in a repository
repo_libs = repo_libs.map(lib => {
lib.uri = "| " + lib.uri;
return lib;
});
}
}
expanded_list = expanded_list.concat(repo_libs);
}
// filter by regular expression if requested
if (this.argv.match) {
let exp = new RegExp(this.argv.match, "i");
expanded_list = expanded_list.filter(
lib =>
lib.uri.match(exp) ||
lib.name.match(exp) ||
lib.description.match(exp)
);
}
// show only installed libraries if requested
if (this.argv.installed) {
expanded_list = expanded_list.filter(lib =>
Boolean(lib.installedVersion)
);
}
// output list
if (this.argv.json) {
// as JSON
qx.tool.compiler.Console.info(JSON.stringify(expanded_list, null, 2));
} else if (!this.argv.quiet) {
// as columns
qx.tool.compiler.Console.info(
columnify(expanded_list, columnify_options)
);
if (!this.argv.noheaders) {
qx.tool.compiler.Console.info();
qx.tool.compiler.Console.info(
"Note on columns: LATEST: Latest release that can be installed with this CLI;"
);
qx.tool.compiler.Console.info(
" COMPATIBLE: Latest release that is semver-compatible with the qooxdoo version used."
);
if (!this.argv.all) {
qx.tool.compiler.Console.info(
"To see all libraries, including potentially incompatible ones, use 'qx package list --all'."
);
}
}
}
// save to cache
this.getCache().compat[qxVersion] = this.__latestCompatible[qxVersion];
await this.saveCache();
},
/**
* compatibility indexes
*/
__repositories: null,
__libraries: null,
__latestCompatible: null,
/**
* Create compatibilty indexes of repositories and the contained libraries
* @param qooxdoo_version {String} The qooxdoo version to check compatibiity with
* @return {Number} The number of repositories containing compatible libraries
*/
async __createIndexes(qooxdoo_version) {
if (this.argv.installed) {
// local libraries
const localPathRepoName = this.self(arguments).localPathRepoName;
this.__repositories.push({
name: localPathRepoName,
description: "Libraries on local filesystem"
});
this.__libraries[localPathRepoName] = [];
let libData = await this.getLockfileData();
for (let lib of libData.libraries) {
if (!lib.repo_name) {
let manifest_path = path.join(
process.cwd(),
lib.path,
qx.tool.config.Manifest.config.fileName
);
let manifest = await qx.tool.utils.Json.loadJsonAsync(
manifest_path
);
let info = manifest.info;
this.__libraries[localPathRepoName].push({
name: info.name,
namespace: manifest.provides.namespace,
summary: info.summary,
version: "v" + info.version,
compatibility: semver.satisfies(
qooxdoo_version,
manifest.requires["qooxdoo-sdk"],
true
),
path: path.relative(process.cwd(), path.dirname(manifest_path)),
installedVersion: "v" + info.version,
manifest
});
}
}
}
// repositories
let repos_cache = this.getCache().repos;
let num_compat_repos = 0;
if (this.__latestCompatible[qooxdoo_version] === undefined) {
this.__latestCompatible[qooxdoo_version] = {};
}
// iterate over repositories
for (let repo_name of repos_cache.list) {
let repo_data = repos_cache.data[repo_name];
// filter out repositories that are deprecated or should not be listed unless --all
let d = repo_data.description;
if (
!this.argv.all &&
d &&
(d.includes("(deprecated)") || d.includes("(unlisted)"))
) {
if (this.argv.verbose) {
qx.tool.compiler.Console.warn(
`>>> Ignoring ${repo_name}: Deprecated or unlisted. `
);
}
continue;
}
let tag_names = repo_data.releases.list;
let { description } = repo_data;
let hasCompatibleRelease = false;
let latestVersion = false;
let repoInstalledVersion = false;
// iterate over releases
for (let tag_name of tag_names) {
let release_data = repo_data.releases.data[tag_name];
let { prerelease, manifests } = release_data;
// iterate over library manifests in that release
for (let manifest of manifests) {
let { qx_versions, info, provides, path: manifest_path } = manifest;
let installedVersion = false;
if (info === undefined) {
if (this.argv.verbose) {
qx.tool.compiler.Console.warn(
`>>> Ignoring ${repo_name} ${tag_name}: Undefined info field. `
);
}
continue;
}
// library version MUST match tag name (which can be longer, for example with pre-release info (alpha, beta, pre, rc etc)
let library_name = info.name;
let version = info.version;
let tag_version = tag_name.replace(/v/, "");
if (version !== tag_version.substr(0, version.length)) {
if (this.argv.verbose) {
qx.tool.compiler.Console.warn(
`>>> Ignoring ${repo_name} ${tag_name}, library '${library_name}': mismatch between tag version '${tag_version}' and library version '${version}'.`
);
}
continue;
}
// save latest version
try {
if (!latestVersion || semver.gt(version, latestVersion, true)) {
latestVersion = tag_name;
}
} catch (e) {
if (this.argv.verbose) {
qx.tool.compiler.Console.warn(
`>>> Ignoring ${repo_name} ${tag_name}, library '${library_name}': invalid version format '${version}'.`
);
}
}
// installed from GitHub?
let installed = await this.getInstalledLibraryTag(
repo_name,
library_name
);
if (installed) {
installedVersion = installed;
repoInstalledVersion = installed;
} else {
let lib = await this.getInstalledLibraryData(library_name);
if (lib) {
installedVersion = "v" + lib.library_version;
}
}
// check compatibility of library
let compatibility = semver.satisfies(
qooxdoo_version,
qx_versions,
true
);
// prepare indexes
if (this.__libraries[repo_name] === undefined) {
this.__libraries[repo_name] = [];
}
// use the latest compatible release, i.e the one that satisfies the following conditions:
// 1) must be semver-compatible with the qooxdoo version
// 2) must be the higher than any other version found so far
// 3) should not be a pre-release unless there are no other compatible releases
let latestCompatibleRelease =
this.__latestCompatible[qooxdoo_version][repo_name];
let latestCompatibleVersion = latestCompatibleRelease
? latestCompatibleRelease.replace(/v/, "")
: undefined;
if (
compatibility === true &&
(latestCompatibleRelease === undefined ||
(semver.gt(tag_version, latestCompatibleVersion, false) &&
(!prerelease || this.argv.prereleases)))
) {
this.__latestCompatible[qooxdoo_version][repo_name] = tag_name;
hasCompatibleRelease = true;
}
// save data
this.__libraries[repo_name].push({
name: info.name,
namespace: provides ? provides.namespace : "",
summary: info.summary,
version,
compatibility,
required_qx_version: qx_versions,
path: path.dirname(manifest_path),
installedVersion,
manifest
});
}
}
if (hasCompatibleRelease) {
num_compat_repos++;
}
// add to list
this.__repositories.push({
name: repo_name,
description,
installedVersion: repoInstalledVersion,
latestVersion,
latestCompatible: hasCompatibleRelease
? this.__latestCompatible[qooxdoo_version][repo_name]
: false
});
}
return num_compat_repos;
}
}
});