Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Resolvers: Allow ranges / npm tags for version resolution #649

Merged
merged 9 commits into from
Sep 5, 2023
109 changes: 71 additions & 38 deletions lib/ui5Framework/AbstractResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ import {getLogger} from "@ui5/logger";
const log = getLogger("ui5Framework:AbstractResolver");
import semver from "semver";

// Matches Semantic Versioning 2.0.0 versions
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
//
// This needs to be aligned with the ui5.yaml JSON schema:
// lib/validation/schema/specVersion/kind/project.json#/definitions/framework/properties/version/pattern
//
// eslint-disable-next-line max-len
const SEMVER_VERSION_REGEXP = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;

// Reduced Semantic Versioning pattern
// Matches MAJOR or MAJOR.MINOR as a simple version range to be resolved to the latest minor/patch
const VERSION_RANGE_REGEXP = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-SNAPSHOT)?$/;
Expand Down Expand Up @@ -215,33 +206,26 @@ class AbstractResolver {
}

static async resolveVersion(version, {ui5HomeDir, cwd} = {}) {
let spec;
const isSnapshotVersion = version.toLowerCase().endsWith("-snapshot");
if (version === "latest" || version === "latest-snapshot") {
// Use a wildcard to resolve to the latest available version
spec = "*";
} else if (SEMVER_VERSION_REGEXP.test(version)) {
// Fully qualified version, can be used directly
spec = version;
} else {
const versionMatch = version.match(VERSION_RANGE_REGEXP);
if (versionMatch) {
if (isSnapshotVersion) {
// For snapshot version ranges we need to insert a stand-in "x" for the patch level
// and - in case none is provided - another "x" for the major version in order to
// make the semver check work: "1-SNAPSHOT" or "1.112-SNAPSHOT" becomes "1.112.x-SNAPSHOT"
spec = `${versionMatch[1]}.${versionMatch[2] || "x"}.x-SNAPSHOT`;
} else {
spec = version;
}
} else {
throw new Error(`Framework version specifier "${version}" is incorrect or not supported`);
}
// Don't allow nullish values
// An empty string is a valid semver range that converts to "*", which should not be supported
if (!version) {
throw new Error(`Framework version specifier "${version}" is incorrect or not supported`);
}

const spec = await this._getVersionSpec(version, {ui5HomeDir, cwd});

// For all invalid cases which are not explicitly handled in _getVersionSpec
if (!spec) {
throw new Error(`Framework version specifier "${version}" is incorrect or not supported`);
}

const versions = await this.fetchAllVersions({ui5HomeDir, cwd});
const resolvedVersion = semver.maxSatisfying(versions, spec, {
d3xter666 marked this conversation as resolved.
Show resolved Hide resolved
includePrerelease: isSnapshotVersion
// Allow ranges that end with -SNAPSHOT to match any -SNAPSHOT version
// like a normal version in order to support ranges like 1.x.x-SNAPSHOT.
includePrerelease: this._isSnapshotVersionOrRange(version)
});

if (!resolvedVersion) {
if (semver.valid(spec)) {
if (this.name === "Sapui5Resolver" && semver.lt(spec, "1.76.0")) {
Expand All @@ -258,9 +242,61 @@ class AbstractResolver {
`Could not resolve framework version ${version}. ` +
`Make sure the version is valid and available in the configured registry.`);
}

return resolvedVersion;
}

static async _getVersionSpec(version, {ui5HomeDir, cwd}) {
if (this._isSnapshotVersionOrRange(version)) {
const versionMatch = version.match(VERSION_RANGE_REGEXP);
if (versionMatch) {
// For snapshot version ranges we need to insert a stand-in "x" for the patch level
// and - in case none is provided - another "x" for the major version in order to
// convert it to a valid semver range:
// "1-SNAPSHOT" becomes "1.x.x-SNAPSHOT" and "1.112-SNAPSHOT" becomes "1.112.x-SNAPSHOT"
return `${versionMatch[1]}.${versionMatch[2] || "x"}.x-SNAPSHOT`;
}
}

// Covers versions and ranges, as versions are also valid ranges
if (semver.validRange(version)) {
return version;
}

// Check for invalid tag name (same check as npm does)
if (encodeURIComponent(version) !== version) {
return null;
}

const allTags = await this.fetchAllTags({ui5HomeDir, cwd});

if (!allTags) {
// Resolver doesn't support tags (e.g. Sapui5MavenSnapshotResolver)
// Only latest and latest-snapshot are supported which both resolve
// to the latest available version.
// See "isSnapshotVersionOrRange" for -snapshot handling
if ((version === "latest" || version === "latest-snapshot")) {
return "*";
d3xter666 marked this conversation as resolved.
Show resolved Hide resolved
} else {
return null;
}
}

if (!allTags[version]) {
throw new Error(
`Could not resolve framework version via tag '${version}'. ` +
`Make sure the tag is available in the configured registry.`
);
}

// Use version from tag
return allTags[version];
}

static _isSnapshotVersionOrRange(version) {
return version.toLowerCase().endsWith("-snapshot");
}

// To be implemented by resolver
async getLibraryMetadata(libraryName) {
throw new Error("AbstractResolver: getLibraryMetadata must be implemented!");
Expand All @@ -271,12 +307,9 @@ class AbstractResolver {
static fetchAllVersions(options) {
throw new Error("AbstractResolver: static fetchAllVersions must be implemented!");
}
}

/* istanbul ignore else */
if (process.env.NODE_ENV === "test") {
// Export pattern for testing to be checked against JSON schema pattern
AbstractResolver._SEMVER_VERSION_REGEXP = SEMVER_VERSION_REGEXP;
static fetchAllTags(options) {
return null;
}
}

export default AbstractResolver;
15 changes: 12 additions & 3 deletions lib/ui5Framework/Openui5Resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,23 @@ class Openui5Resolver extends AbstractResolver {
})
};
}
static async fetchAllVersions({ui5HomeDir, cwd} = {}) {
const installer = new Installer({
static async fetchAllVersions(options) {
const installer = this._getInstaller(options);
return await installer.fetchPackageVersions({pkgName: OPENUI5_CORE_PACKAGE});
}

static async fetchAllTags(options) {
const installer = this._getInstaller(options);
return installer.fetchPackageDistTags({pkgName: OPENUI5_CORE_PACKAGE});
}

static _getInstaller({ui5HomeDir, cwd} = {}) {
return new Installer({
cwd: cwd ? path.resolve(cwd) : process.cwd(),
ui5HomeDir:
ui5HomeDir ? path.resolve(ui5HomeDir) :
path.join(os.homedir(), ".ui5")
});
return await installer.fetchPackageVersions({pkgName: OPENUI5_CORE_PACKAGE});
}
}

Expand Down
15 changes: 12 additions & 3 deletions lib/ui5Framework/Sapui5Resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,23 @@ class Sapui5Resolver extends AbstractResolver {
})
};
}
static async fetchAllVersions({ui5HomeDir, cwd} = {}) {
const installer = new Installer({
static async fetchAllVersions(options) {
const installer = this._getInstaller(options);
return await installer.fetchPackageVersions({pkgName: DIST_PKG_NAME});
}

static async fetchAllTags(options) {
const installer = this._getInstaller(options);
return installer.fetchPackageDistTags({pkgName: DIST_PKG_NAME});
}

static _getInstaller({ui5HomeDir, cwd} = {}) {
return new Installer({
cwd: cwd ? path.resolve(cwd) : process.cwd(),
ui5HomeDir:
ui5HomeDir ? path.resolve(ui5HomeDir) :
path.join(os.homedir(), ".ui5")
});
return await installer.fetchPackageVersions({pkgName: DIST_PKG_NAME});
}
}

Expand Down
5 changes: 5 additions & 0 deletions lib/ui5Framework/npm/Installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ class Installer extends AbstractInstaller {
return Object.keys(packument.versions);
}

async fetchPackageDistTags({pkgName}) {
const packument = await this.getRegistry().requestPackagePackument(pkgName);
return packument["dist-tags"];
}

async fetchPackageManifest({pkgName, version}) {
const targetDir = this._getTargetDirForPackage({pkgName, version});
try {
Expand Down
1 change: 1 addition & 0 deletions test/lib/graph/helpers/ui5Framework.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ test.afterEach.always((t) => {
esmock.purge(t.context.Registry);
esmock.purge(t.context.Installer);
esmock.purge(t.context.AbstractResolver);
esmock.purge(t.context.Openui5Resolver);
esmock.purge(t.context.Sapui5Resolver);
esmock.purge(t.context.Application);
esmock.purge(t.context.Library);
Expand Down
Loading
Loading