Skip to content

Commit

Permalink
Introduce source to injections
Browse files Browse the repository at this point in the history
Ember templates use a `source` argument to the container. This hints
where to look for "local lookup", and also what namespace to look in for
a partial specifier. For example a template in an addon's `src` tree can
invoke components and helpers in that tree becuase it has a `source`
argument passed to lookups.

This introduces the same functionality for service injections. A service
can now accept a `source` argument and its lookup will apply to the
namespace of that file.

Additionally, refactor away from using a `source` argument to the
`resolve` API of resolvers. Instead leveral the `expandLocalLookup` API
to pass the source and get an identifier that can be treated as
canonical for that factory.
  • Loading branch information
mixonic committed Mar 4, 2018
1 parent 2020953 commit d64b165
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 100 deletions.
5 changes: 2 additions & 3 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ for a detailed explanation.
([RFC](https://github.com/dgeb/rfcs/blob/module-unification/text/0000-module-unification.md))
to Ember. This includes:

- Passing the `source` of a `lookup`/`factoryFor` call as the second argument
to an Ember resolver's `resolve` method (as a positional arg we will call
`referrer`).
- Passing the `source` of a `lookup`/`factoryFor` call as an argument to
`expandLocalLookup` on the resolver.
- Making `lookupComponentPair` friendly to local/private resolutions. The
new code ensures a local resolution is not paired with a global resolution.

Expand Down
88 changes: 49 additions & 39 deletions packages/container/lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ export default class Container {
return { [OWNER]: this.owner };
}

_resolverCacheKey(name, options) {
return this.registry.resolverCacheKey(name, options);
_resolverCacheKey(name) {
return this.registry.resolverCacheKey(name);
}

/**
Expand Down Expand Up @@ -168,29 +168,7 @@ export default class Container {
}
}

let cacheKey = this._resolverCacheKey(normalizedName, options);
let cached = this.factoryManagerCache[cacheKey];

if (cached !== undefined) { return cached; }

let factory = EMBER_MODULE_UNIFICATION ? this.registry.resolve(normalizedName, options) : this.registry.resolve(normalizedName);

if (factory === undefined) {
return;
}

if (DEBUG && factory && typeof factory._onLookup === 'function') {
factory._onLookup(fullName);
}

let manager = new FactoryManager(this, factory, fullName, normalizedName);

if (DEBUG) {
manager = wrapManagerInDeprecationProxy(manager);
}

this.factoryManagerCache[cacheKey] = manager;
return manager;
return factoryFor(this, normalizedName, fullName);
}
}
/*
Expand Down Expand Up @@ -232,6 +210,7 @@ function isInstantiatable(container, fullName) {
}

function lookup(container, fullName, options = {}) {
let normalizedName = fullName;
if (options.source) {
let expandedFullName = container.registry.expandLocalLookup(fullName, options);

Expand All @@ -241,24 +220,51 @@ function lookup(container, fullName, options = {}) {
return;
}

fullName = expandedFullName;
normalizedName = expandedFullName;
} else if (expandedFullName) {
// with ember-module-unification, if expandLocalLookup returns something,
// pass it to the resolve without the source
fullName = expandedFullName;
normalizedName = expandedFullName;
options = {};
}
}

if (options.singleton !== false) {
let cacheKey = container._resolverCacheKey(fullName, options);
let cacheKey = container._resolverCacheKey(normalizedName);
let cached = container.cache[cacheKey];
if (cached !== undefined) {
return cached;
}
}

return instantiateFactory(container, fullName, options);
return instantiateFactory(container, normalizedName, fullName, options);
}


function factoryFor(container, normalizedName, fullName) {
let cacheKey = container._resolverCacheKey(normalizedName);
let cached = container.factoryManagerCache[cacheKey];

if (cached !== undefined) { return cached; }

let factory = container.registry.resolve(normalizedName);

if (factory === undefined) {
return;
}

if (DEBUG && factory && typeof factory._onLookup === 'function') {
factory._onLookup(fullName); // What should this pass? fullname or the normalized key?
}

let manager = new FactoryManager(container, factory, fullName, normalizedName);

if (DEBUG) {
manager = wrapManagerInDeprecationProxy(manager);
}

container.factoryManagerCache[cacheKey] = manager;
return manager;
}

function isSingletonClass(container, fullName, { instantiate, singleton }) {
Expand All @@ -277,8 +283,8 @@ function isFactoryInstance(container, fullName, { instantiate, singleton }) {
return instantiate !== false && (singleton !== false || isSingleton(container, fullName)) && isInstantiatable(container, fullName);
}

function instantiateFactory(container, fullName, options) {
let factoryManager = EMBER_MODULE_UNIFICATION && options && options.source ? container.factoryFor(fullName, options) : container.factoryFor(fullName);
function instantiateFactory(container, normalizedName, fullName, options) {
let factoryManager = factoryFor(container, normalizedName, fullName);

if (factoryManager === undefined) {
return;
Expand All @@ -287,7 +293,7 @@ function instantiateFactory(container, fullName, options) {
// SomeClass { singleton: true, instantiate: true } | { singleton: true } | { instantiate: true } | {}
// By default majority of objects fall into this case
if (isSingletonInstance(container, fullName, options)) {
let cacheKey = container._resolverCacheKey(fullName, options);
let cacheKey = container._resolverCacheKey(normalizedName);
return container.cache[cacheKey] = factoryManager.create();
}

Expand All @@ -313,12 +319,15 @@ function buildInjections(container, injections) {
container.registry.validateInjections(injections);
}

let injection;
for (let i = 0; i < injections.length; i++) {
injection = injections[i];
hash[injection.property] = lookup(container, injection.fullName);
let {property, specifier, source} = injections[i];
if (source) {
hash[property] = lookup(container, specifier, {source});
} else {
hash[property] = lookup(container, specifier);
}
if (!isDynamic) {
isDynamic = !isSingleton(container, injection.fullName);
isDynamic = !isSingleton(container, specifier);
}
}
}
Expand Down Expand Up @@ -355,12 +364,13 @@ function resetCache(container) {
}

function resetMember(container, fullName) {
let member = container.cache[fullName];
let cacheKey = container._resolverCacheKey(fullName);
let member = container.cache[cacheKey];

delete container.factoryManagerCache[fullName];
delete container.factoryManagerCache[cacheKey];

if (member) {
delete container.cache[fullName];
delete container.cache[cacheKey];

if (member.destroy) {
member.destroy();
Expand Down
39 changes: 18 additions & 21 deletions packages/container/lib/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,10 @@ export default class Registry {
assert(`Attempting to register an unknown factory: '${fullName}'`, factory !== undefined);

let normalizedName = this.normalize(fullName);
assert(`Cannot re-register: '${fullName}', as it has already been resolved.`, !this._resolveCache[normalizedName]);
let cacheKey = this.resolverCacheKey(normalizedName);
assert(`Cannot re-register: '${fullName}', as it has already been resolved.`, !this._resolveCache[cacheKey]);

this._failSet.delete(normalizedName);
this._failSet.delete(cacheKey);
this.registrations[normalizedName] = factory;
this._options[normalizedName] = options;
}
Expand All @@ -179,13 +180,14 @@ export default class Registry {
assert('fullName must be a proper full name', this.isValidFullName(fullName));

let normalizedName = this.normalize(fullName);
let cacheKey = this.resolverCacheKey(normalizedName);

this._localLookupCache = Object.create(null);

delete this.registrations[normalizedName];
delete this._resolveCache[normalizedName];
delete this._resolveCache[cacheKey];
delete this._options[normalizedName];
this._failSet.delete(normalizedName);
this._failSet.delete(cacheKey);
}

/**
Expand Down Expand Up @@ -224,7 +226,6 @@ export default class Registry {
@return {Function} fullName's factory
*/
resolve(fullName, options) {
assert('fullName must be a proper full name', this.isValidFullName(fullName));
let factory = resolve(this, this.normalize(fullName), options);
if (factory === undefined && this.fallback !== null) {
factory = this.fallback.resolve(...arguments);
Expand Down Expand Up @@ -450,7 +451,7 @@ export default class Registry {
let injections = this._typeInjections[type] ||
(this._typeInjections[type] = []);

injections.push({ property, fullName });
injections.push({ property, specifier: fullName });
}

/**
Expand Down Expand Up @@ -514,7 +515,7 @@ export default class Registry {
let injections = this._injections[normalizedName] ||
(this._injections[normalizedName] = []);

injections.push({ property, fullName: normalizedInjectionName });
injections.push({ property, specifier: normalizedInjectionName });
}

/**
Expand Down Expand Up @@ -566,12 +567,8 @@ export default class Registry {
return injections;
}

resolverCacheKey(name, options) {
if (!EMBER_MODULE_UNIFICATION) {
return name;
}

return (options && options.source) ? `${options.source}:${name}` : name;
resolverCacheKey(name) {
return name;
}

/**
Expand Down Expand Up @@ -625,11 +622,13 @@ if (DEBUG) {

for (let key in hash) {
if (hash.hasOwnProperty(key)) {
assert(`Expected a proper full name, given '${hash[key]}'`, this.isValidFullName(hash[key]));
let { specifier, source } = hash[key];
assert(`Expected a proper full name, given '${specifier}'`, this.isValidFullName(specifier));

injections.push({
property: key,
fullName: hash[key]
specifier,
source
});
}
}
Expand All @@ -640,12 +639,10 @@ if (DEBUG) {
Registry.prototype.validateInjections = function(injections) {
if (!injections) { return; }

let fullName;

for (let i = 0; i < injections.length; i++) {
fullName = injections[i].fullName;
let {specifier, source} = injections[i];

assert(`Attempting to inject an unknown injection: '${fullName}'`, this.has(fullName));
assert(`Attempting to inject an unknown injection: '${specifier}'`, this.has(specifier, {source}));
}
};
}
Expand Down Expand Up @@ -688,15 +685,15 @@ function resolve(registry, normalizedName, options) {
}
}

let cacheKey = registry.resolverCacheKey(normalizedName, options);
let cacheKey = registry.resolverCacheKey(normalizedName);
let cached = registry._resolveCache[cacheKey];
if (cached !== undefined) { return cached; }
if (registry._failSet.has(cacheKey)) { return; }

let resolved;

if (registry.resolver) {
resolved = registry.resolver.resolve(normalizedName, options && options.source);
resolved = registry.resolver.resolve(normalizedName);
}

if (resolved === undefined) {
Expand Down
35 changes: 23 additions & 12 deletions packages/container/tests/container_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,15 +388,18 @@ moduleFor('Container', class extends AbstractTestCase {
assert.deepEqual(resolveWasCalled, ['foo:post']);
}

['@test A factory\'s lazy injections are validated when first instantiated'](assert) {
[`@test A factory's lazy injections are validated when first instantiated`](assert) {
let registry = new Registry();
let container = registry.container();
let Apple = factory();
let Orange = factory();

Apple.reopenClass({
_lazyInjections() {
return ['orange:main', 'banana:main'];
return [
{specifier: 'orange:main'},
{specifier: 'banana:main'}
];
}
});

Expand All @@ -419,7 +422,9 @@ moduleFor('Container', class extends AbstractTestCase {
Apple.reopenClass({
_lazyInjections: () => {
assert.ok(true, 'should call lazy injection method');
return ['orange:main'];
return [
{specifier: 'orange:main'}
];
}
});

Expand Down Expand Up @@ -637,18 +642,20 @@ moduleFor('Container', class extends AbstractTestCase {
});

if (EMBER_MODULE_UNIFICATION) {
QUnit.module('Container module unification');

moduleFor('Container module unification', class extends AbstractTestCase {
['@test The container can pass a source to factoryFor'](assert) {
['@test The container can expand and resolve a source to factoryFor'](assert) {
let PrivateComponent = factory();
let lookup = 'component:my-input';
let expectedSource = 'template:routes/application';
let registry = new Registry();
let resolveCount = 0;
registry.resolve = function(fullName, { source }) {
let expandedKey = 'boom, special expanded key';
registry.expandLocalLookup = function() {
return expandedKey;
};
registry.resolve = function(fullName) {
resolveCount++;
if (fullName === lookup && source === expectedSource) {
if (fullName === expandedKey) {
return PrivateComponent;
}
};
Expand All @@ -660,13 +667,17 @@ if (EMBER_MODULE_UNIFICATION) {
assert.equal(resolveCount, 1, 'resolve called only once and a cached factory was returned the second time');
}

['@test The container can pass a source to lookup']() {
['@test The container can expand and resolve a source to lookup']() {
let PrivateComponent = factory();
let lookup = 'component:my-input';
let expectedSource = 'template:routes/application';
let registry = new Registry();
registry.resolve = function(fullName, { source }) {
if (fullName === lookup && source === expectedSource) {
let expandedKey = 'boom, special expanded key';
registry.expandLocalLookup = function() {
return expandedKey;
};
registry.resolve = function(fullName) {
if (fullName === expandedKey) {
return PrivateComponent;
}
};
Expand All @@ -676,7 +687,7 @@ if (EMBER_MODULE_UNIFICATION) {
let result = container.lookup(lookup, { source: expectedSource });
this.assert.ok(result instanceof PrivateComponent, 'The correct factory was provided');

this.assert.ok(container.cache[`template:routes/application:component:my-input`] instanceof PrivateComponent,
this.assert.ok(container.cache[expandedKey] instanceof PrivateComponent,
'The correct factory was stored in the cache with the correct key which includes the source.');
}
});
Expand Down
Loading

0 comments on commit d64b165

Please sign in to comment.