Skip to content

Commit

Permalink
[JENKINS-37006] Import by version range (#2)
Browse files Browse the repository at this point in the history
* Import by version range

... history might look weird after this commit because I stupidly reset to an earlier commit and had to use the IDE's local history to recover the changes ... phew !!

* Created ModuleSpec and Version types

* Extract Version and ModuleSpec types into modules of their own

* Split out Version and ModuleSpec tests into spec files of their own

* More Version and ModuleSpec tests (and fixes)

* Higher level version range bundle load tests

* Fix module CSS url

* Fix ModuleSpec.prototype.getLoadBundleFileNamePrefix to normalize the package name

E.g. where there's an org in the name

* Fix for Array iteration bug + nsProvider in importAs

* set beta version

* Stash docs

* fix dep versions

* Remove dependency on window-handle

* New beta version

* 0.0.6 release version
  • Loading branch information
tfennelly authored Aug 24, 2016
1 parent 207f042 commit d573e5c
Show file tree
Hide file tree
Showing 14 changed files with 913 additions and 240 deletions.
Empty file added MODULE_LOADING.md
Empty file.
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@ This is a JavaScript "module bundle" loader for Jenkins ([NPM] package) i.e. a l

For more information on "module bundle" loading, read the
<a href="https://github.com/jenkinsci/js-modules/blob/master/FAQs.md#what-does-module-loading-mean">What does "module loading" mean</a> FAQ.

Install Package:

```
npm install --save @jenkins-cd/js-modules
```
__Table of Contents__:
<p>
<ul>
<a href="#usage">Usage</a><br/>
<a href="#problem--motivation">Problem / Motivation</a><br/>
<a href="https://github.com/jenkinsci/js-modules/blob/master/FAQs.md#do-i-really-need-to-learn-all-this-new-stuff">Do I really need to learn all this "new" stuff?</a><br/>
<a href="#about-jenkins-modules">About Jenkins Modules</a><br/>
Expand All @@ -27,6 +23,28 @@ __Table of Contents__:
</p>

<hr/>

# Usage

For the most part, you don't actually use `js-modules` directly (aside from installing the package). In general,
you use it via `js-builder` i.e. you use `js-builder` to build browser-loadable bundles, and those
bundles will use `js-modules` to load/"import" the modules/packages that they depend on, but do not contain
within their bundle.

> [js-builder]
Install Package:

```
npm install --save @jenkins-cd/js-modules
```

After installing `js-modules`, your next step is to start using [js-builder] and, as stated above, that's
mostly all you'll ever need to know about `js-modules` because the rest of it happens under the hood via
[js-builder].

That said, it's probably useful to have an understanding of how `js-modules` actually loads modules/packages.
For that, take a look at [MODULE_LOADING.md](MODULE_LOADING.md).

# Problem / Motivation
For the most part, the Jenkins GUI is constructed on the server side from [Jelly] files ([Stapler] etc). Client-side
Expand Down Expand Up @@ -73,8 +91,8 @@ The following slides attempt to bring you through `js-modules` in some more deta

The following [NPM] packages are designed to aid the use and adoption of `js-modules`:

* [jenkins-js-builder]: See this package for details on how to create module bundles for `js-modules`.
* [jenkins-js-test]: See this package for details on how to test modules for module bundles for `js-modules`. This package's functionality is indirectly available via [jenkins-js-builder].
* [js-builder]: See this package for details on how to create module bundles for `js-modules`.
* [js-test]: See this package for details on how to test modules for module bundles for `js-modules`. This package's functionality is indirectly available via [js-builder].

# Framework Libs (jenkinsci/js-libs)

Expand Down Expand Up @@ -104,5 +122,5 @@ We have already created Framework lib bundles for a number of common JavaScript
[Stapler]: http://stapler.kohsuke.org/
[jquery-detached]: https://github.com/tfennelly/jquery-detached
[jqueryui-detached]: https://github.com/tfennelly/jqueryui-detached
[jenkins-js-builder]: https://github.com/jenkinsci/js-builder
[jenkins-js-test]: https://github.com/jenkinsci/js-test
[js-builder]: https://github.com/jenkinsci/js-builder
[js-test]: https://github.com/jenkinsci/js-test
198 changes: 198 additions & 0 deletions js/ModuleSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* The MIT License
*
* Copyright (c) 2016, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

var Version = require('./Version');

function ModuleSpec(qName) {
var qNameTokens = qName.split(":");

if (qNameTokens.length === 2) {
var namespace = qNameTokens[0].trim();
var nsTokens = namespace.split("/");
var namespaceProvider = undefined;
if (nsTokens.length === 2) {
namespaceProvider = nsTokens[0].trim();
namespace = nsTokens[1].trim();
if (namespaceProvider !== 'plugin' && namespaceProvider !== 'core-assets') {
console.error('Unsupported module namespace provider "' + namespaceProvider + '". Setting to undefined.');
namespaceProvider = undefined;
}
}

var npmName = parseNPMName(qNameTokens[1].trim());

this.nsProvider = namespaceProvider;
this.namespace = namespace;
this.moduleName = npmName.name;
this.moduleVersion = npmName.version;
} else {
// The module/bundle is not in a namespace and doesn't
// need to be loaded i.e. it will load itself and export.
var npmName = parseNPMName(qNameTokens[0].trim());

this.moduleName = npmName.name;
this.moduleVersion = npmName.version;
}

// Attach version compatibility info
var versions = [];

if (this.moduleVersion) {
var moduleVersionTokens = this.moduleVersion.split(/[,|]+/);

for (var i = 0; i < moduleVersionTokens.length; i++) {
var moduleVersionToken = moduleVersionTokens[i].trim();
var parsedVersion = new Version(moduleVersionToken);
versions.push(parsedVersion);
}
}

this.moduleCompatVersions = versions;
}

ModuleSpec.prototype.getLoadBundleVersion = function() {
if (this.moduleCompatVersions.length === 0) {
// If no versions were specified on the name, then we
// just return undefined.
return undefined;
}
// If a version is specified, we use the first "specific" version
// e.g. "1.1.2" is specific while "1.1.x" and "any" are not.
for (var i = 0; i < this.moduleCompatVersions.length; i++) {
var version = this.moduleCompatVersions[i];
if (version.isSpecific()) {
return version;
}
}

// If there's no specific version then we return the first
// version in the list.
return this.moduleCompatVersions[0];
};

ModuleSpec.prototype.getLoadBundleName = function() {
var version = this.getLoadBundleVersion();
if (version) {
return this.moduleName + '@' + version.raw;
} else {
return this.moduleName;
}
};

ModuleSpec.prototype.importAs = function() {
if (this.moduleName.charAt(0) === '.') {
return this.moduleName;
}

var version = this.getLoadBundleVersion();
var importName = normalizePackageName(this.moduleName);
var importNS = this.namespace;

if (!importNS) {
importNS = importName;
}

var importAs = importNS + ':' + importName;
if (this.nsProvider) {
importAs = this.nsProvider + '/' + importAs;
}
if (version) {
importAs += '@' + version.raw;
}

return importAs;
};

ModuleSpec.prototype.getLoadBundleFileNamePrefix = function() {
var version = this.getLoadBundleVersion();
var normalizedName = normalizePackageName(this.moduleName);
if (version) {
// If a version was specified then we only do the script load if a
// specific version was provided i.e. loading does not get triggered
// by imports that specify non-specific version numbers e.g. "any"
// or "1.2.x". A specific version number would be e.g. "1.2.3" i.e.
// fully qualified. When loading is not triggered, the import is depending
// on another import (with a specific version) or on a bundle do an
// export of an internal dependency i.e. on another bundle "providing"
// the module be exporting it.
if (version.isSpecific()) {
return normalizedName + '-' + version.raw.replace(new RegExp('\\.', 'g'), '-');
} else {
return undefined;
}
} else {
return normalizedName;
}
};

/**
* Normalize an NPM package name by removing all non alpha numerics and replacing
* with hyphens.
* @param packageName The NPM package name.
* @returns The normalized NPM package name.
*/
function normalizePackageName(packageName) {
packageName = packageName.replace(/[^\w.]/g, "-"); // replace non alphanumerics.
if (packageName.charAt(0) === '-') {
packageName = packageName.substring(1);
}
return packageName;
};

function parseNPMName(resourceName) {
if (resourceName.length > 1) {
var npmName = {};
var orgSlashIndex = resourceName.indexOf('/');

if (resourceName.charAt(0) === '@' && orgSlashIndex > 0) {
// It's an NPM org package. Strip off the org and package
// and add it to the name. We'll get the rest then and parse
// that.
npmName.name = resourceName.substring(0, orgSlashIndex + 1);

// Remove the org part from the name and continue parsing.
resourceName = resourceName.substring(orgSlashIndex + 1);
} else {
// Initialise it so we can append to it below.
npmName.name = '';
}

var versionIndex = resourceName.indexOf('@');
if (versionIndex > 0) {
npmName.name += resourceName.substring(0, versionIndex);
npmName.version = resourceName.substring(versionIndex + 1);
} else {
npmName.name += resourceName;
}

return npmName;
} else {
return {
name: resourceName
}
}
}

module.exports = ModuleSpec;

76 changes: 76 additions & 0 deletions js/Version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* The MIT License
*
* Copyright (c) 2016, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

function Version(version) {
this.raw = version;

// The version string must start with a digit.
// It's not an error for it not to start with a number e.g. it can
// be "any" and we may introduce other aliases.
if (!version || version.length === 0 || isNaN(version.charAt(0))) {
return;
}

function normalizeToken(string) {
// remove anything that's not a digit, a dot or an x.
var normalized = string.replace(/[^\d]/g, '');
if (normalized === '') {
return undefined;
}
return normalized;
}

var versionTokens = version.split('.');

this.prerelease = undefined;

var patchAndPrerelease = '';
for (var i = 2; i < versionTokens.length; i++) {
if (patchAndPrerelease.length > 0) {
patchAndPrerelease += '.';
}
patchAndPrerelease += versionTokens[i];

var separatorIdx = patchAndPrerelease.indexOf('-');
if (separatorIdx !== -1) {
this.patch = normalizeToken(patchAndPrerelease.substring(0, separatorIdx));
this.prerelease = patchAndPrerelease.substring(separatorIdx + 1);
} else {
this.patch = normalizeToken(patchAndPrerelease);
}
}

if (versionTokens.length >= 2) {
this.minor = normalizeToken(versionTokens[1]);
}
if (versionTokens.length >= 1) {
this.major = normalizeToken(versionTokens[0]);
}
}

Version.prototype.isSpecific = function() {
return (this.major !== undefined && this.minor !== undefined && this.patch !== undefined);
};

module.exports = Version;
Loading

0 comments on commit d573e5c

Please sign in to comment.