Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

[loader] Add support for optional dependencies #1629

Merged
merged 22 commits into from

4 participants

@juandopazo
Collaborator

Optional dependencies are modules that are not in the requires list because they depend on a test passing. Contrary to the current condition option, the test is owned by the optional module itself, not by the required module.

Example:

YUI({
    modules: {
        foo: {
            path: 'foo-min.js',
            test: function () {
                return true;
            }
        },
        bar: {
            path: 'bar-min.js',
            optionalRequires: ['foo']
        }
    }
}).use('bar', function (Y) {
  // ...
});

This is WIP. I'm still figuring out what all the edge cases are and this need changes to Shifter to fully integrate it into YUI.

@juandopazo juandopazo added this to the Sprint 12 Code Freeze milestone
src/loader/js/loader.js
@@ -1581,6 +1589,25 @@ Y.Loader.prototype = {
}
}
+ // Optional dependencies are dependencies that define their own tests
+ // For example, if a module depends on a JSON polyfill and the JSON
+ // module has a test, it will be added to the dependency list if the
+ // test passes.
+ // This feature was designed specifically to be used when transpiling
+ // ES6 modules, in order to use polyfills without having to import them
+ // since they should already be available in the global scope.
+ if (optReqs) {
+ for (i = 0, length = optReqs.length; i < length; i++) {
+ m = this.getModule(optReqs[i]);
@caridy Owner
caridy added a note

m might not even exist in the registry, so this might return a falsy value, and the if that follows should take that in consideration as well.

normally, modules with affinity can be deployed to specific runtimes, and optionalRequires will facilitate the edge case where a module that is suppose to run in all runtimes (e.g.: server and client) has an optional require on a module that is suppose to run only on a client, and if you have the proper meta generation process per runtime, you don't need to add tests for those modules, just skip it from the undesired runtimes.

@juandopazo Collaborator

Got it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@caridy
Owner

Note: a module with a test should be analyzed during addModule() call, which is the one deciding whether or not the module is good enough for a runtime, while a module with optionalRequires is just an artifact to require things that might not be available in the runtime, they are not bound, which means we can have optionals with and without tests.

@saw

Hey what is the status of this PR, are you planning on merging or is this dead?

@caridy
Owner

@saw it is not dead, we just hit some roadblocks while doing the review of the code, and the tests. @juandopazo will continue working on this.

That being said, you have a patch in express-yui so you are not blocked by this one. The patch is essentially the same.

@juandopazo
Collaborator

@ezequiel just helped me find the issue. The sorting function that sorts the list of modules only looks at moduleInfo[foo].requires and we chose not to modify that list, so it's not sorting optional requires correctly.

@juandopazo
Collaborator

First pass at a solution. Please do not merge this. I'm experimenting with using the ignore and loaded lists and figuring out if I can avoid mutating moduleInfo[foo].requires.

@juandopazo
Collaborator

Just to have it on the record, can we write here the reasons why this use case isn´t covered by condition?

@caridy
Owner

Here is the full writeup about this issue:
#1585 (comment)

@yahoocla
Owner

CLA is valid!

@juandopazo
Collaborator

Reproduced the conflict with patterns. Once #1790 is merged this should be good to go.

@juandopazo
Collaborator

I'm happy to report this PR is now passing and ready to go. Project building YUI and trying to use this feature would still need Shifter to support the new test property of modules.

src/yui/js/yui.js
@@ -737,6 +737,12 @@ with any configuration info required for the module.
Y.Env._missed.splice(j, 1);
}
}
+
+ if (loader && !loader._canBeAttached(name)) {
@caridy Owner
caridy added a note

is this to support Y.use('optionalModule')?

@juandopazo Collaborator

This is to avoid attaching the module to the Y when the module has already been included (maybe in a bundle) and its test fails.

It probably deserves a comment.

@caridy Owner
caridy added a note

yeah, add a comment.

@juandopazo Collaborator

Comment added!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/loader/js/loader.js
@@ -1063,6 +1063,13 @@ Y.Loader.prototype = {
* @param {Object} [config.testresults] A hash of test results from `Y.Features.all()`
* @param {Function} [config.configFn] A function to exectute when configuring this module
* @param {Object} config.configFn.mod The module config, modifying this object will modify it's config. Returning false will delete the module's config.
+ * @param {String[]} [config.optionalRequires] List of dependencies that
+ have their own tests instead of a test associated with this module like
+ conditional dependencies. This is targeted mostly at polyfills, since
+ they may not be in the list of requires because they are assumed to be
+ available in the global scope. **Modules without a test will be ignored**.
@caridy Owner
caridy added a note

This line is misleading. Modules without a test will be consider valid modules and will be attached, at least that's what I can infer from the implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/loader/js/loader.js
@@ -1428,6 +1435,30 @@ Y.Loader.prototype = {
}
return r;
},
+
+ /**
+ Returns `true` if the module can be attached to the YUI instance. Runs
+ the module's test if there is one and caches its result.
+
+ @method _canBeAttached
+ @param {String|Object} module Module name or module object.
@caridy Owner
caridy added a note

can we remove the ambiguity of this argument, and only expect a string for the name?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/loader/js/loader.js
@@ -1480,6 +1512,20 @@ Y.Loader.prototype = {
return mod.expanded;
}
+ // Optional dependencies are dependencies that may or may not be
+ // available.
+ // This feature was designed specifically to be used when transpiling
+ // ES6 modules, in order to use polyfills and regular scripts that define
+ // global variables without having to import them since they should be
+ // available in the global scope.
+ if (optReqs) {
+ for (i = 0, length = optReqs.length; i < length; i++) {
+ m = this.getModule(optReqs[i]);
@caridy Owner
caridy added a note

optReqs[i] is the name of the optional module and can be used to call _canBeAttached() and to push it into the requires structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
juandopazo added some commits
@juandopazo juandopazo [loader] Fix API Docs for optionalRequires
Optional dependencies try to be consistent with the idea that they're
"optional". If the module is available (loaded and without a test or
with a passing test) it is used. If it is not available (with a failing
test), it is ignored.
fe9d8c5
@juandopazo juandopazo [loader] Accept only a string in _canBeAttached dbd8a8f
@caridy
Owner

LGTM, although I still see some travis failures happening.

@juandopazo juandopazo changed the title from [loader] (WIP) Add support for optional dependencies to [loader] Add support for optional dependencies
@juandopazo
Collaborator

@caridy that was just a Promise [slightly] flaky test.

@juandopazo juandopazo merged commit 3797a08 into from
@juandopazo juandopazo deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 11, 2014
  1. @juandopazo
  2. @juandopazo
  3. @juandopazo
  4. @juandopazo
  5. @juandopazo
  6. @juandopazo
Commits on Feb 13, 2014
  1. @juandopazo
  2. @juandopazo
Commits on Feb 26, 2014
  1. @juandopazo
  2. @juandopazo
Commits on Feb 27, 2014
  1. @juandopazo

    [loader] Check if requires list passes tests and add optionals to it

    juandopazo authored
    This way _sort() has knowledge of optional requires to sort them as
    dependencies
  2. @juandopazo
Commits on Feb 28, 2014
  1. @juandopazo
  2. @juandopazo
  3. @juandopazo
Commits on Apr 24, 2014
  1. @juandopazo

    [loader] Rename optTest to test and add tests for it

    juandopazo authored
  2. @juandopazo
Commits on Apr 29, 2014
  1. @juandopazo
Commits on May 6, 2014
  1. @juandopazo
  2. @juandopazo

    [loader] Fix API Docs for optionalRequires

    juandopazo authored
    Optional dependencies try to be consistent with the idea that they're
    "optional". If the module is available (loaded and without a test or
    with a passing test) it is used. If it is not available (with a failing
    test), it is ignored.
  3. @juandopazo
Commits on May 8, 2014
  1. @juandopazo
This page is out of date. Refresh to see the latest.
View
19 src/loader/HISTORY.md
@@ -4,7 +4,24 @@ YUI Loader Change History
@VERSION@
------
-* No changes.
+* Add support for optional dependencies. These dependencies are conditionally
+ loaded but each dependency is responsible for determining the result of the
+ test, the opposite of `condition`. Example:
+
+```js
+ YUI({
+ modules: {
+ foo: {
+ test: function (Y) {
+ return true;
+ }
+ },
+ bar: {
+ optionalRequires: ['foo']
+ }
+ }
+ }).use('bar', ...);
+```
3.16.0
------
View
55 src/loader/js/loader.js
@@ -1063,6 +1063,14 @@ Y.Loader.prototype = {
* @param {Object} [config.testresults] A hash of test results from `Y.Features.all()`
* @param {Function} [config.configFn] A function to exectute when configuring this module
* @param {Object} config.configFn.mod The module config, modifying this object will modify it's config. Returning false will delete the module's config.
+ * @param {String[]} [config.optionalRequires] List of dependencies that
+ may optionally be loaded by this loader. This is targeted mostly at
+ polyfills, since they should not be in the list of requires because
+ polyfills are assumed to be available in the global scope.
+ * @param {Function} [config.test] Test to be called when this module is
+ added as an optional dependency of another module. If the test function
+ returns `false`, the module will be ignored and will not be attached to
+ this YUI instance.
* @param {String} [name] The module name, required if not in the module data.
* @return {Object} the module definition or null if the object passed in did not provide all required attributes.
*/
@@ -1428,6 +1436,28 @@ Y.Loader.prototype = {
}
return r;
},
+
+ /**
+ Returns `true` if the module can be attached to the YUI instance. Runs
+ the module's test if there is one and caches its result.
+
+ @method _canBeAttached
+ @param {String} module Name of the module to check.
+ @return {Boolean} Result of the module's test if it has one, or `true`.
+ **/
+ _canBeAttached: function (m) {
+ m = this.getModule(m);
+ if (m && m.test) {
+ if (!m.hasOwnProperty('_testResult')) {
+ m._testResult = m.test(Y);
+ }
+ return m._testResult;
+ }
+ // return `true` for modules not registered as Loader will know what
+ // to do with them later on
+ return true;
+ },
+
/**
* Returns an object containing properties for all modules required
* in order to load the requested module
@@ -1449,9 +1479,10 @@ Y.Loader.prototype = {
//TODO add modue cache here out of scope..
- var i, m, j, add, packName, lang, testresults = this.testresults,
+ var i, m, j, length, add, packName, lang, testresults = this.testresults,
name = mod.name, cond,
adddef = ON_PAGE[name] && ON_PAGE[name].details,
+ optReqs = mod.optionalRequires,
d, go, def,
r, old_mod,
o, skinmod, skindef, skinpar, skinname,
@@ -1480,6 +1511,19 @@ Y.Loader.prototype = {
return mod.expanded;
}
+ // Optional dependencies are dependencies that may or may not be
+ // available.
+ // This feature was designed specifically to be used when transpiling
+ // ES6 modules, in order to use polyfills and regular scripts that define
+ // global variables without having to import them since they should be
+ // available in the global scope.
+ if (optReqs) {
+ for (i = 0, length = optReqs.length; i < length; i++) {
+ if (this._canBeAttached(optReqs[i])) {
+ mod.requires.push(optReqs[i]);
+ }
+ }
+ }
d = [];
hash = {};
@@ -1989,11 +2033,13 @@ Y.Loader.prototype = {
Y.log('Undefined module: ' + mname + ', matched a pattern: ' +
pname, 'info', 'loader');
// ext true or false?
- m = this.addModule(Y.merge(found, {test: void 0}), mname);
+ m = this.addModule(Y.merge(found, {
+ test: void 0,
+ temp: true
+ }), mname);
if (found.configFn) {
m.configFn = found.configFn;
}
- m.temp = true;
}
}
} else {
@@ -2455,8 +2501,7 @@ Y.log('Undefined module: ' + mname + ', matched a pattern: ' +
* @param {string} type the type of dependency to insert.
*/
insert: function(o, type, skipsort) {
- Y.log('public insert() ' + (type || '') + ', ' +
- Y.Object.keys(this.required), "info", "loader");
+ Y.log('public insert() ' + (type || '') + ', ' + Y.Object.keys(this.required), "info", "loader");
var self = this, copy = Y.merge(this);
delete copy.require;
delete copy.dirty;
View
3  src/loader/tests/assets/part1-mod.js
@@ -0,0 +1,3 @@
+YUI.add('part1-mod', function(Y) {
+ Y.Part1MOD = true;
+});
View
40 src/loader/tests/manual/optreqs.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8 />
+<title>JS Bin</title>
+<script src="../../../../build/yui/yui.js"></script>
+</head>
+<body class="yui3-skin-sam">
+ <script>
+YUI.add('foo', function (Y) {
+ Y.foo = 'hello';
+});
+YUI.add('bar', function (Y) {
+ Y.bar = Y.foo + ' world!';
+});
+YUI().use(function (Y) {
+ var loader = new Y.Loader({
+ maxURLLength: 8024,
+ combine: true,
+ ignoreRegistered: true,
+ modules: {
+ foo: {
+ path: 'foo-min.js',
+ test: function () {
+ return true;
+ }
+ },
+ bar: {
+ path: 'bar-min.js',
+ optionalRequires: ['foo']
+ }
+ },
+ require: ['bar']
+ });
+ var out = loader.resolve(true);
+ console.log(out);
+});
+ </script>
+</body>
+</html>
View
171 src/loader/tests/unit/assets/loader-tests.js
@@ -789,6 +789,177 @@ YUI.add('loader-tests', function(Y) {
Assert.areEqual('4two.js', out.js[0], 'Loaded modules in incorrect order');
Assert.areEqual('4one.js', out.js[1], 'Loaded modules in incorrect order');
},
+ 'test: optional dependencies': function () {
+ var loader = new Y.Loader({
+ maxURLLength: 8024,
+ combine: true,
+ ignoreRegistered: true,
+ modules: {
+ foo: {
+ path: 'foo-min.js'
+ },
+ bar: {
+ path: 'bar-min.js',
+ optionalRequires: ['foo']
+ }
+ },
+ require: ['bar']
+ });
+ var out = loader.resolve(true);
+ Assert.areSame(2, out.jsMods.length, 'Not included the correct number of modules');
+ Assert.areSame('foo', out.jsMods[0].name, 'Not included optional dependency');
+ Assert.areSame('bar', out.jsMods[1].name, 'Not included required module');
+ },
+ 'test: optional dependencies with tests': function () {
+ var loader = new Y.Loader({
+ maxURLLength: 8024,
+ combine: true,
+ ignoreRegistered: true,
+ modules: {
+ foo: {
+ path: 'foo-min.js',
+ test: function (Y) {
+ Assert.isInstanceOf(YUI, Y);
+ return true;
+ }
+ },
+ baz: {
+ path: 'baz-min.js',
+ test: function () {
+ return false;
+ }
+ },
+ bar: {
+ path: 'bar-min.js',
+ optionalRequires: ['foo']
+ }
+ },
+ require: ['bar']
+ });
+ var out = loader.resolve(true);
+ Assert.areSame(2, out.jsMods.length, 'Not included the correct number of modules');
+ Assert.areSame('foo', out.jsMods[0].name, 'Not included optional dependency');
+ Assert.areSame('bar', out.jsMods[1].name, 'Not included required module');
+ },
+ 'test: optional dependencies ignore undeclared modules': function () {
+ var loader = new Y.Loader({
+ maxURLLength: 8024,
+ combine: true,
+ ignoreRegistered: true,
+ modules: {
+ bar: {
+ path: 'bar-min.js',
+ optionalRequires: ['foobarbazasdflkj']
+ }
+ },
+ require: ['bar']
+ });
+ var out = loader.resolve(true);
+ Assert.areSame(1, out.jsMods.length, 'Not included the correct number of modules');
+ Assert.areSame('bar', out.jsMods[0].name, 'Not included required module');
+ },
+ 'test: optional dependencies and patterns': function () {
+ var test = this;
+ YUI.add('a-mod-with-opt-dep', function () {}, '', {
+ optionalRequires: ['foo']
+ });
+
+ YUI({
+ groups: {
+ patternDepIntegration: {
+ base: '../assets/',
+ filter: 'raw',
+ patterns: {
+ "part1-": {
+ configFn: function (me) {
+ //change from default format of part1-mod1/part1-mod1.js to just part1-mod1.js
+ me.path = me.path.replace(/part1-[^\/]+\//, "");
+ }
+ }
+ }
+ }
+ },
+ modules: {
+ 'a-mod-with-opt-dep': {
+ path: 'a-mod-with-opt-dep-min.js',
+ optionalRequires: ['part1-mod']
+ }
+ }
+ }).use('a-mod-with-opt-dep', function (Y) {
+ setTimeout(function () {
+ test.resume(function () {
+
+ });
+ }, 0);
+ });
+
+ test.wait();
+ },
+ 'test: already added module with failing test': function () {
+ YUI.add('mod121-foo', function (Y) {
+ Y.foo = 'hello';
+ });
+ YUI.add('mod122-bar', function (Y) {
+ Y.bar = Y.foo + ' world';
+ }, '', {
+ requires: ['mod121-foo']
+ });
+
+ var loader = new Y.Loader({
+ maxURLLength: 8024,
+ combine: true,
+ ignoreRegistered: true,
+ modules: {
+ 'mod121-foo': {
+ test: function () {
+ return false;
+ }
+ },
+ 'mod122-bar': {
+ requires: ['mod121-foo']
+ }
+ },
+ require: ['mod122-bar']
+ });
+ var out = loader.resolve(true);
+ Assert.areSame(2, out.jsMods.length, 'Not included the correct number of modules');
+ Assert.areSame('mod121-foo', out.jsMods[0].name, 'Not included optional dependency');
+ Assert.areSame('mod122-bar', out.jsMods[1].name, 'Not included required module');
+ },
+ 'test: correct attach order of optional dependencies': function () {
+ var test = this;
+
+ YUI.add('mod131', function (Y) {
+ Y.foo = 'hello';
+ });
+ YUI.add('mod132', function (Y) {
+ Y.bar = Y.foo + ' world';
+ }, '', {
+ optionalRequires: ['mod131']
+ });
+
+ var $Y = YUI({
+ modules: {
+ 'mod131': {
+ },
+ 'mod132': {
+ optionalRequires: ['mod131']
+ }
+ }
+ });
+
+ $Y.use('mod132', function (Y, result) {
+ setTimeout(function () {
+ test.resume(function () {
+ Assert.areSame('hello', Y.foo);
+ Assert.areSame('hello world', Y.bar);
+ Assert.isTrue(result.success);
+ });
+ });
+ });
+
+ test.wait();
+ },
test_css_stamp: function() {
var test = this,
links = document.getElementsByTagName('link').length + document.getElementsByTagName('style').length;
View
12 src/yui/js/yui.js
@@ -737,6 +737,18 @@ with any configuration info required for the module.
Y.Env._missed.splice(j, 1);
}
}
+
+ // Optional dependencies normally work by modifying the
+ // dependency list of a module. If the dependency's test
+ // passes it is added to the list. If not, it's not loaded.
+ // This following check ensures that optional dependencies
+ // are not attached when they were already loaded into the
+ // page (when bundling for example)
+ if (loader && !loader._canBeAttached(name)) {
+ Y.log('Failed to attach module ' + name, 'warn', 'yui');
+ return true;
+ }
+
/*
If it's a temp module, we need to redo it's requirements if it's already loaded
since it may have been loaded by another instance and it's dependencies might
Something went wrong with that request. Please try again.