From ad81146cddc2ad00e23a4dd4baf5e5e9e79e40b8 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Sun, 4 Mar 2018 10:19:59 -0800 Subject: [PATCH] Introduce namespace::name invocation --- packages/container/lib/container.js | 4 +- packages/container/lib/registry.js | 33 ++-- packages/container/tests/container_test.js | 61 +++++++- packages/container/tests/registry_test.js | 39 +++++ packages/ember-glimmer/lib/resolver.ts | 32 +++- .../components/namespaced-lookup-test.js | 142 ++++++++++++++++++ packages/ember-metal/lib/injected_property.js | 16 +- .../ember-runtime/lib/system/core_object.js | 5 +- packages/ember-runtime/tests/inject_test.js | 4 +- packages/ember-utils/lib/index.d.ts | 3 +- .../ember-views/lib/utils/lookup-component.js | 4 +- .../ember/tests/service_injection_test.js | 16 ++ .../lib/test-cases/abstract-rendering.js | 38 ++++- .../lib/test-resolver.js | 10 +- 14 files changed, 365 insertions(+), 42 deletions(-) create mode 100644 packages/ember-glimmer/tests/integration/components/namespaced-lookup-test.js diff --git a/packages/container/lib/container.js b/packages/container/lib/container.js index 6c2c9390f92..b58a06ee8bb 100644 --- a/packages/container/lib/container.js +++ b/packages/container/lib/container.js @@ -147,7 +147,7 @@ export default class Container { assert('fullName must be a proper full name', this.registry.isValidFullName(normalizedName)); - if (options.source) { + if (options.source || options.namespace) { let expandedFullName = this.registry.expandLocalLookup(fullName, options); // if expandLocalLookup returns falsey, we do not support local lookup if (!EMBER_MODULE_UNIFICATION) { @@ -207,7 +207,7 @@ function isInstantiatable(container, fullName) { function lookup(container, fullName, options = {}) { let normalizedName = fullName; - if (options.source) { + if (options.source || options.namespace) { let expandedFullName = container.registry.expandLocalLookup(fullName, options); if (!EMBER_MODULE_UNIFICATION) { diff --git a/packages/container/lib/registry.js b/packages/container/lib/registry.js index ea216249faf..113f3c02ee5 100644 --- a/packages/container/lib/registry.js +++ b/packages/container/lib/registry.js @@ -321,8 +321,9 @@ export default class Registry { } let source = options && options.source && this.normalize(options.source); + let namespace = (options && options.namespace) || undefined; - return has(this, this.normalize(fullName), source); + return has(this, this.normalize(fullName), source, namespace); } /** @@ -586,13 +587,12 @@ export default class Registry { expandLocalLookup(fullName, options) { if (this.resolver !== null && this.resolver.expandLocalLookup) { assert('fullName must be a proper full name', this.isValidFullName(fullName)); - assert('options.source must be provided to expandLocalLookup', options && options.source); - assert('options.source must be a proper full name', this.isValidFullName(options.source)); + assert('options.source must be a proper full name', !options.source || this.isValidFullName(options.source)); let normalizedFullName = this.normalize(fullName); let normalizedSource = this.normalize(options.source); - return expandLocalLookup(this, normalizedFullName, normalizedSource); + return expandLocalLookup(this, normalizedFullName, normalizedSource, options.namespace); } else if (this.fallback !== null) { return this.fallback.expandLocalLookup(fullName, options); } else { @@ -616,13 +616,14 @@ if (DEBUG) { for (let key in hash) { if (hash.hasOwnProperty(key)) { - let { specifier, source } = hash[key]; + let { specifier, source, namespace } = hash[key]; assert(`Expected a proper full name, given '${specifier}'`, this.isValidFullName(specifier)); injections.push({ property: key, specifier, - source + source, + namespace }); } } @@ -634,14 +635,14 @@ if (DEBUG) { if (!injections) { return; } for (let i = 0; i < injections.length; i++) { - let {specifier, source} = injections[i]; + let {specifier, source, namespace} = injections[i]; - assert(`Attempting to inject an unknown injection: '${specifier}'`, this.has(specifier, {source})); + assert(`Attempting to inject an unknown injection: '${specifier}'`, this.has(specifier, {source, namespace})); } }; } -function expandLocalLookup(registry, normalizedName, normalizedSource) { +function expandLocalLookup(registry, normalizedName, normalizedSource, namespace) { let cache = registry._localLookupCache; let normalizedNameCache = cache[normalizedName]; @@ -649,17 +650,19 @@ function expandLocalLookup(registry, normalizedName, normalizedSource) { normalizedNameCache = cache[normalizedName] = Object.create(null); } - let cached = normalizedNameCache[normalizedSource]; + let cacheKey = namespace || normalizedSource; + + let cached = normalizedNameCache[cacheKey]; if (cached !== undefined) { return cached; } - let expanded = registry.resolver.expandLocalLookup(normalizedName, normalizedSource); + let expanded = registry.resolver.expandLocalLookup(normalizedName, normalizedSource, namespace); - return normalizedNameCache[normalizedSource] = expanded; + return normalizedNameCache[cacheKey] = expanded; } function resolve(registry, normalizedName, options) { - if (options && options.source) { + if (options && (options.source || options.namespace)) { // when `source` is provided expand normalizedName // and source into the full normalizedName let expandedNormalizedName = registry.expandLocalLookup(normalizedName, options); @@ -702,8 +705,8 @@ function resolve(registry, normalizedName, options) { return resolved; } -function has(registry, fullName, source) { - return registry.resolve(fullName, { source }) !== undefined; +function has(registry, fullName, source, namespace) { + return registry.resolve(fullName, { source, namespace }) !== undefined; } const privateNames = dictionary(null); diff --git a/packages/container/tests/container_test.js b/packages/container/tests/container_test.js index a98ace52673..a8dd24460df 100644 --- a/packages/container/tests/container_test.js +++ b/packages/container/tests/container_test.js @@ -650,7 +650,9 @@ if (EMBER_MODULE_UNIFICATION) { let registry = new Registry(); let resolveCount = 0; let expandedKey = 'boom, special expanded key'; - registry.expandLocalLookup = function() { + registry.expandLocalLookup = (specifier, options) => { + this.assert.strictEqual(specifier, lookup, 'specifier is expanded'); + this.assert.strictEqual(options.source, expectedSource, 'source is expanded'); return expandedKey; }; registry.resolve = function(fullName) { @@ -673,7 +675,9 @@ if (EMBER_MODULE_UNIFICATION) { let expectedSource = 'template:routes/application'; let registry = new Registry(); let expandedKey = 'boom, special expanded key'; - registry.expandLocalLookup = function() { + registry.expandLocalLookup = (specifier, options) => { + this.assert.strictEqual(specifier, lookup, 'specifier is expanded'); + this.assert.strictEqual(options.source, expectedSource, 'source is expanded'); return expandedKey; }; registry.resolve = function(fullName) { @@ -690,5 +694,58 @@ if (EMBER_MODULE_UNIFICATION) { this.assert.ok(container.cache[expandedKey] instanceof PrivateComponent, 'The correct factory was stored in the cache with the correct key which includes the source.'); } + + ['@test The container can expand and resolve a namespace to factoryFor'](assert) { + let PrivateComponent = factory(); + let lookup = 'component:my-input'; + let expectedNamespace = 'my-addon'; + let registry = new Registry(); + let resolveCount = 0; + let expandedKey = 'boom, special expanded key'; + registry.expandLocalLookup = (specifier, options) => { + this.assert.strictEqual(specifier, lookup, 'specifier is expanded'); + this.assert.strictEqual(options.namespace, expectedNamespace, 'namespace is expanded'); + return expandedKey; + }; + registry.resolve = function(fullName) { + resolveCount++; + if (fullName === expandedKey) { + return PrivateComponent; + } + }; + + let container = registry.container(); + + assert.strictEqual(container.factoryFor(lookup, { namespace: expectedNamespace }).class, PrivateComponent, 'The correct factory was provided'); + assert.strictEqual(container.factoryFor(lookup, { namespace: expectedNamespace }).class, PrivateComponent, 'The correct factory was provided again'); + assert.equal(resolveCount, 1, 'resolve called only once and a cached factory was returned the second time'); + } + + ['@test The container can expand and resolve a namespace to lookup']() { + let PrivateComponent = factory(); + let lookup = 'component:my-input'; + let expectedNamespace = 'my-addon'; + let registry = new Registry(); + let expandedKey = 'boom, special expanded key'; + registry.expandLocalLookup = (specifier, options) => { + this.assert.strictEqual(specifier, lookup, 'specifier is expanded'); + this.assert.strictEqual(options.namespace, expectedNamespace, 'namespace is expanded'); + return expandedKey; + }; + registry.resolve = function(fullName) { + if (fullName === expandedKey) { + return PrivateComponent; + } + }; + + let container = registry.container(); + + let result = container.lookup(lookup, { namespace: expectedNamespace }); + this.assert.ok(result instanceof PrivateComponent, 'The correct factory was provided'); + + this.assert.ok(container.cache[expandedKey] instanceof PrivateComponent, + 'The correct factory was stored in the cache with the correct key which includes the source.'); + } }); + } diff --git a/packages/container/tests/registry_test.js b/packages/container/tests/registry_test.js index 1bd4c760be1..8e18c48dda2 100644 --- a/packages/container/tests/registry_test.js +++ b/packages/container/tests/registry_test.js @@ -760,6 +760,11 @@ if (EMBER_MODULE_UNIFICATION) { resolver.add({specifier, source}, PrivateComponent); let registry = new Registry({ resolver }); + assert.strictEqual( + registry.resolve(specifier), + undefined, + 'Not returned when specifier not scoped' + ); assert.strictEqual( registry.resolve(specifier, { source }), PrivateComponent, @@ -771,5 +776,39 @@ if (EMBER_MODULE_UNIFICATION) { 'The correct factory was provided again' ); } + + ['@test The registry can pass a namespace to the resolver'](assert) { + let PrivateComponent = factory(); + let type = 'component'; + let name = 'my-input'; + let specifier = `${type}:${name}`; + let source = 'template:routes/application'; + let namespace = 'my-addon'; + + let resolver = new ModuleBasedTestResolver(); + resolver.add({specifier, source, namespace}, PrivateComponent); + let registry = new Registry({ resolver }); + + assert.strictEqual( + registry.resolve(specifier), + undefined, + 'Not returned when specifier not scoped' + ); + assert.strictEqual( + registry.resolve(specifier, {source}), + undefined, + 'Not returned when specifier is missing namespace' + ); + assert.strictEqual( + registry.resolve(specifier, { source, namespace }), + PrivateComponent, + 'The correct factory was provided' + ); + assert.strictEqual( + registry.resolve(specifier, { source, namespace }), + PrivateComponent, + 'The correct factory was provided again' + ); + } }); } diff --git a/packages/ember-glimmer/lib/resolver.ts b/packages/ember-glimmer/lib/resolver.ts index 556b900b40d..3821642fcc6 100644 --- a/packages/ember-glimmer/lib/resolver.ts +++ b/packages/ember-glimmer/lib/resolver.ts @@ -52,8 +52,11 @@ function instrumentationPayload(name: string) { return { object: `component:${name}` }; } -function makeOptions(moduleName: string) { - return moduleName !== undefined ? { source: `template:${moduleName}`} : undefined; +function makeOptions(moduleName: string, namespace?: string): LookupOptions { + return { + source: moduleName !== undefined ? `template:${moduleName}` : undefined, + namespace + }; } const BUILTINS_HELPERS = { @@ -208,15 +211,17 @@ export default class RuntimeResolver implements IRuntimeResolver { - const helper = this.builtInHelpers[name]; + private _lookupHelper(_name: string, meta: OwnedTemplateMeta): Option { + const helper = this.builtInHelpers[_name]; if (helper !== undefined) { return helper; } const { owner, moduleName } = meta; - const options: LookupOptions | undefined = makeOptions(moduleName); + const {name, namespace} = this._parseNameForNamespace(_name); + + const options: LookupOptions = makeOptions(moduleName, namespace); const factory = owner.factoryFor(`helper:${name}`, options) || owner.factoryFor(`helper:${name}`); @@ -257,8 +262,21 @@ export default class RuntimeResolver implements IRuntimeResolver { - let { layout, component } = lookupComponent(meta.owner, name, makeOptions(meta.moduleName)); + private _parseNameForNamespace(_name: string) { + let name = _name; + let namespace = undefined; + let namespaceDelimiterOffset = _name.indexOf('::'); + if (namespaceDelimiterOffset !== -1) { + name = _name.slice(namespaceDelimiterOffset+2); + namespace = _name.slice(0, namespaceDelimiterOffset); + } + + return {name, namespace}; + } + + private _lookupComponentDefinition(_name: string, meta: OwnedTemplateMeta): Option { + let {name, namespace} = this._parseNameForNamespace(_name); + let { layout, component } = lookupComponent(meta.owner, name, makeOptions(meta.moduleName, namespace)); if (layout && !component && ENV._TEMPLATE_ONLY_GLIMMER_COMPONENTS) { return new TemplateOnlyComponentDefinition(layout); diff --git a/packages/ember-glimmer/tests/integration/components/namespaced-lookup-test.js b/packages/ember-glimmer/tests/integration/components/namespaced-lookup-test.js new file mode 100644 index 00000000000..e4a391c39b4 --- /dev/null +++ b/packages/ember-glimmer/tests/integration/components/namespaced-lookup-test.js @@ -0,0 +1,142 @@ +import { moduleFor, RenderingTest } from '../../utils/test-case'; +import { EMBER_MODULE_UNIFICATION } from 'ember/features'; +import { Component, helper } from 'ember-glimmer'; + +if (EMBER_MODULE_UNIFICATION) { + + moduleFor('Namespaced lookup', class extends RenderingTest { + ['@test it can render a namespaced component']() { + this.addTemplate({ + specifier: 'template:components/my-component', + namespace: 'my-addon' + }, 'namespaced template {{myProp}}'); + + this.add({ + specifier: 'component:my-component', + namespace: 'my-addon' + }, Component.extend({ + myProp: 'My property' + })); + + this.addComponent('x-outer', { template: '{{my-addon::my-component}}' }); + + this.render('{{x-outer}}'); + + this.assertText('namespaced template My property'); + + this.runTask(() => this.rerender()); + + this.assertText('namespaced template My property'); + } + + ['@test it can render a nested namespaced component']() { + this.addTemplate({ + specifier: 'template:components/my-component', + namespace: 'second-addon' + }, 'second namespaced template'); + + this.addTemplate({ + specifier: 'template:components/my-component', + namespace: 'first-addon' + }, 'first namespaced template - {{second-addon::my-component}}'); + + this.addComponent('x-outer', { template: '{{first-addon::my-component}}' }); + + this.render('{{x-outer}}'); + + this.assertText('first namespaced template - second namespaced template'); + + this.runTask(() => this.rerender()); + + this.assertText('first namespaced template - second namespaced template'); + } + + ['@test it can render a nested un-namespaced component']() { + this.addTemplate({ + specifier: 'template:components/addon-component', + source: 'template:first-addon/src/ui/components/my-component.hbs' + }, 'un-namespaced addon template'); + + this.addTemplate({ + specifier: 'template:components/my-component', + moduleName: 'first-addon/src/ui/components/my-component.hbs', + namespace: 'first-addon' + }, '{{addon-component}}'); + + this.addComponent('x-outer', { template: '{{first-addon::my-component}}' }); + + this.render('{{x-outer}}'); + + this.assertText('un-namespaced addon template'); + + this.runTask(() => this.rerender()); + + this.assertText('un-namespaced addon template'); + } + + ['@test it can render a namespaced main component']() { + this.addTemplate({ + specifier: 'template:components/addon-component', + soruce: 'template:first-addon/src/ui/components/main.hbs' + }, 'Nested namespaced component'); + + this.addTemplate({ + specifier: 'template:components/first-addon', + moduleName: 'first-addon/src/ui/components/main.hbs' + }, '{{addon-component}}'); + + this.addComponent('x-outer', { template: '{{first-addon}}' }); + + this.render('{{x-outer}}'); + + this.assertText('Nested namespaced component'); + + this.runTask(() => this.rerender()); + + this.assertText('Nested namespaced component'); + } + + ['@test it does not render a main component when using a namespace']() { + this.addTemplate({ + specifier: 'template:components/main', + namespace: 'my-addon' + }, 'namespaced template {{myProp}}'); + + this.add({ + specifier: 'component:main', + namespace: 'my-addon' + }, Component.extend({ + myProp: 'My property' + })); + + this.add({ + specifier: 'helper:my-addon', + namespace: 'empty-namespace' + + }, helper(() => 'my helper')); + + this.render('{{empty-namespace::my-addon}}'); + + this.assertText('my helper'); // component should be not found + + this.runTask(() => this.rerender()); + + this.assertText('my helper'); + } + + ['@test it renders a namespaced helper']() { + this.add({ + specifier: 'helper:my-helper', + namespace: 'my-namespace' + }, helper(() => 'my helper')); + + this.render('{{my-namespace::my-helper}}'); + + this.assertText('my helper'); + + this.runTask(() => this.rerender()); + + this.assertText('my helper'); + } + }); +} diff --git a/packages/ember-metal/lib/injected_property.js b/packages/ember-metal/lib/injected_property.js index 54ee1574fcf..887ed0ef6e0 100644 --- a/packages/ember-metal/lib/injected_property.js +++ b/packages/ember-metal/lib/injected_property.js @@ -24,8 +24,20 @@ export default class InjectedProperty extends ComputedProperty { super(injectedPropertyGet); this.type = type; - this.name = name; this.source = options ? options.source : undefined; + + if (name) { + let namespaceDelimiterOffset = name.indexOf('::'); + if (namespaceDelimiterOffset === -1) { + this.name = name; + this.namespace = undefined; + } else { + this.name = name.slice(namespaceDelimiterOffset+2); + this.namespace = name.slice(0, namespaceDelimiterOffset); + } + } else { + this.name = undefined; + } } } @@ -37,5 +49,5 @@ function injectedPropertyGet(keyName) { assert(`Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.`, owner); let specifier = `${desc.type}:${desc.name || keyName}`; - return owner.lookup(specifier, {source: desc.source}); + return owner.lookup(specifier, {source: desc.source, namespace: desc.namespace}); } diff --git a/packages/ember-runtime/lib/system/core_object.js b/packages/ember-runtime/lib/system/core_object.js index 7be9f52bd7c..f698d520e13 100644 --- a/packages/ember-runtime/lib/system/core_object.js +++ b/packages/ember-runtime/lib/system/core_object.js @@ -1021,8 +1021,9 @@ if (DEBUG) { desc = descriptorFor(proto, key); if (desc instanceof InjectedProperty) { injections[key] = { - specifier: `${desc.type}:${desc.name || key}`, - source: desc.source + namespace: desc.namespace, + source: desc.source, + specifier: `${desc.type}:${desc.name || key}` }; } } diff --git a/packages/ember-runtime/tests/inject_test.js b/packages/ember-runtime/tests/inject_test.js index 54988df6b1a..a18c884b8b1 100644 --- a/packages/ember-runtime/tests/inject_test.js +++ b/packages/ember-runtime/tests/inject_test.js @@ -64,8 +64,8 @@ if (DEBUG) { assert.deepEqual( AnObject._lazyInjections(), { - 'foo': { specifier: 'foo:bar', source: undefined }, - 'bar': { specifier: 'quux:bar', source: undefined } + 'foo': { specifier: 'foo:bar', source: undefined, namespace: undefined }, + 'bar': { specifier: 'quux:bar', source: undefined, namespace: undefined } }, 'should return injected container keys'); }); } diff --git a/packages/ember-utils/lib/index.d.ts b/packages/ember-utils/lib/index.d.ts index a84928aff09..a1bbf890848 100644 --- a/packages/ember-utils/lib/index.d.ts +++ b/packages/ember-utils/lib/index.d.ts @@ -8,7 +8,8 @@ export interface Factory { } export interface LookupOptions { - source: string; + source?: string; + namespace?: string; } export interface Owner { diff --git a/packages/ember-views/lib/utils/lookup-component.js b/packages/ember-views/lib/utils/lookup-component.js index 5b142edf0e7..b93a401d122 100644 --- a/packages/ember-views/lib/utils/lookup-component.js +++ b/packages/ember-views/lib/utils/lookup-component.js @@ -56,9 +56,7 @@ function lookupComponentPair(componentLookup, owner, name, options) { export default function lookupComponent(owner, name, options) { let componentLookup = owner.lookup('component-lookup:main'); - let source = options && options.source; - - if (source) { + if (options && (options.source || options.namespace)) { let localResult = lookupComponentPair(componentLookup, owner, name, options); if (localResult.component || localResult.layout) { diff --git a/packages/ember/tests/service_injection_test.js b/packages/ember/tests/service_injection_test.js index a0fa447e4f6..bf946c445ee 100644 --- a/packages/ember/tests/service_injection_test.js +++ b/packages/ember/tests/service_injection_test.js @@ -168,6 +168,22 @@ if (EMBER_MODULE_UNIFICATION) { }); } + ['@test Service with namespace can be injected and is resolved'](assert) { + this.add('controller:application', Controller.extend({ + myService: inject.service('my-namespace::my-service') + })); + let MyService = Service.extend(); + this.add({ + specifier: 'service:my-service', + namespace: 'my-namespace' + }, MyService); + + this.visit('/').then(() => { + let controller = this.applicationInstance.lookup('controller:application'); + assert.ok(controller.get('myService') instanceof MyService); + }); + } + }); } diff --git a/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js b/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js index dba6aeaeb85..c9357c14bfe 100644 --- a/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js +++ b/packages/internal-test-helpers/lib/test-cases/abstract-rendering.js @@ -2,6 +2,7 @@ import { assign } from 'ember-utils'; import { compile } from 'ember-template-compiler'; import { EventDispatcher } from 'ember-views'; import { helper, Helper, Component, _resetRenderers} from 'ember-glimmer'; +import { ModuleBasedResolver } from '../test-resolver'; import AbstractTestCase from './abstract'; import buildOwner from '../build-owner'; @@ -41,7 +42,42 @@ export default class AbstractRenderingTestCase extends AbstractTestCase { getOwnerOptions() { } getBootOptions() { } - getResolver() { } + + get resolver() { + return this.owner.__registry__.fallback.resolver; + } + + getResolver() { + return new ModuleBasedResolver(); + } + + add(specifier, factory) { + this.resolver.add(specifier, factory); + } + + addTemplate(templateName, templateString) { + if (typeof templateName === 'string') { + this.resolver.add(`template:${templateName}`, this.compile(templateString, { + moduleName: templateName + })); + } else { + this.resolver.add(templateName, this.compile(templateString, { + moduleName: templateName.moduleName + })); + } + } + + addComponent(name, { ComponentClass = null, template = null }) { + if (ComponentClass) { + this.resolver.add(`component:${name}`, ComponentClass); + } + + if (typeof template === 'string') { + this.resolver.add(`template:components/${name}`, this.compile(template, { + moduleName: `components/${name}` + })); + } + } afterEach() { try { diff --git a/packages/internal-test-helpers/lib/test-resolver.js b/packages/internal-test-helpers/lib/test-resolver.js index 47a67d0576a..7cafcdc703f 100644 --- a/packages/internal-test-helpers/lib/test-resolver.js +++ b/packages/internal-test-helpers/lib/test-resolver.js @@ -2,8 +2,8 @@ import { compile } from 'ember-template-compiler'; const DELIMITER = '%'; -function serializeKey(specifier, source) { - return [specifier, source].join(DELIMITER); +function serializeKey(specifier, source, namespace) { + return [specifier, (namespace ? '[source invalid due to namespace]' : source), namespace].join(DELIMITER); } class Resolver { @@ -14,8 +14,8 @@ class Resolver { resolve(specifier) { return this._registered[specifier] || this._registered[serializeKey(specifier)]; } - expandLocalLookup(specifier, source) { - let key = serializeKey(specifier, source); + expandLocalLookup(specifier, source, namespace) { + let key = serializeKey(specifier, source, namespace); if (this._registered[key]) { return key; } @@ -35,7 +35,7 @@ class Resolver { key = serializeKey(lookup); break; case 'object': - key = serializeKey(lookup.specifier, lookup.source); + key = serializeKey(lookup.specifier, lookup.source, lookup.namespace); break; default: throw new Error('Specifier string has an unknown type');