diff --git a/index.js b/index.js index f59eac6c..a161559a 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,25 @@ 'use strict'; +var writeFile = require('broccoli-file-creator'); var VersionChecker = require('ember-cli-version-checker'); var path = require('path'); var isModuleUnification; +function mergeRecursivelyAddonResolverConfig(config, addon) { + + if (!config.hasOwnProperty(addon.name)) { + + if (addon.resolverConfig) { + config[addon.name] = addon.resolverConfig() || {}; + } + + addon.addons.forEach(nestedAddon => { + mergeRecursivelyAddonResolverConfig(config, nestedAddon); + }); + } + +} + module.exports = { name: 'ember-resolver', @@ -57,7 +73,60 @@ module.exports = { return new MergeTrees(addonTrees); }, - _moduleUnificationTrees() { + // Trigger exception if the result of `addon.resolverConfig` method has an unexpected format + // Show a warning if there are collisions between addon types or addon collections + validateAddonsConfig: function(addonsConfig) { + + let types = {}; + let collections = {}; + + Object.keys(addonsConfig).forEach(addonName => { + + let addonConfig = addonsConfig[addonName]; + if (addonConfig) { + if (typeof(addonConfig) !== 'object') { + throw new Error(`"addon.resolverConfig" returns an unexpected value. Addon: ${addonName}.`); + } + + let addonTypes = addonConfig.types || {}; + if (typeof(addonTypes) !== 'object') { + throw new Error(`"addon.resolverConfig" returns an unexpected "types" value. Addon: ${addonName}.`); + } + + let addonCollections = addonConfig.collections || {}; + if (typeof(addonCollections) !== 'object') { + throw new Error(`"addon.resolverConfig" returns an unexpected "collections" value. Addon: ${addonName}.`); + } + + Object.keys(addonTypes).forEach(key => { + if (!types.hasOwnProperty(key)) { + types[key] = key; + } else { + this.ui.writeLine(`Addon '${types[key]}' configured the type '${key}' on the resolver, but addon '${addonName}' has overwritten the type '${key}'.`); + } + }); + Object.keys(addonCollections).forEach(key => { + if (!collections.hasOwnProperty(key)) { + collections[key] = key; + } else { + this.ui.writeLine(`Addon '${types[key]}' configured the collection '${key}' on the resolver, but addon '${addonName}' has overwritten the collection '${key}'.`); + } + }); + } + }); + }, + + _moduleUnificationTrees: function() { + + let addonConfigs = {}; + + this.project.addons.forEach(addon => { + mergeRecursivelyAddonResolverConfig(addonConfigs, addon); + }); + this.validateAddonsConfig(addonConfigs); + + let addonConfigsFileContent = `export default ${JSON.stringify(addonConfigs)};`; + var resolve = require('resolve'); var Funnel = require('broccoli-funnel'); @@ -66,6 +135,11 @@ module.exports = { destDir: 'ember-resolver' }); + var addonsConfigTree = writeFile( + 'ember-resolver/addons-config.js', + addonConfigsFileContent + ); + var glimmerResolverSrc = require.resolve('@glimmer/resolver/package'); var glimmerResolverPath = path.dirname(glimmerResolverSrc); var glimmerResolverTree = new Funnel(glimmerResolverPath, { @@ -80,6 +154,7 @@ module.exports = { }); return [ + this.preprocessJs(addonsConfigTree, { registry: this.registry }), this.preprocessJs(featureTree, { registry: this.registry }), this.preprocessJs(glimmerResolverTree, { registry: this.registry }), this.preprocessJs(glimmerDITree, { registry: this.registry }), diff --git a/mu-trees/addon/ember-config.js b/mu-trees/addon/ember-config.js index 624ab7ee..da0ae5b0 100644 --- a/mu-trees/addon/ember-config.js +++ b/mu-trees/addon/ember-config.js @@ -1,115 +1,17 @@ -/* - * This config describes canonical Ember, as described in the - * module unification spec: - * - * https://github.com/emberjs/rfcs/blob/master/text/0143-module-unification.md - * - */ +import addonsConfig from 'ember-resolver/addons-config'; +import moduleConfig from 'ember-resolver/module-config'; +import mergeAddonsConfig from 'ember-resolver/utils/merge-addons-config'; + export default function generateConfig(name) { - return { + + let config = { app: { name, rootName: name }, - types: { - adapter: { definitiveCollection: 'models' }, - application: { definitiveCollection: 'main' }, - config: { definitiveCollection: 'config' }, - controller: { definitiveCollection: 'routes' }, - component: { definitiveCollection: 'components' }, - 'component-lookup': { definitiveCollection: 'main' }, - 'component-manager': { definitiveCollection: 'component-managers' }, - event_dispatcher: { definitiveCollection: 'main' }, - helper: { definitiveCollection: 'components' }, - initializer: { definitiveCollection: 'initializers' }, - 'instance-initializers': { definitiveCollection: 'instance-initializer' }, - location: { definitiveCollection: 'main' }, - model: { definitiveCollection: 'models' }, - modifier: { definitiveCollection: 'components' }, - 'modifier-manager': { definitiveCollection: 'modifier-managers' }, - partial: { definitiveCollection: 'partials' }, - renderer: { definitiveCollection: 'main' }, - route: { definitiveCollection: 'routes' }, - router: { definitiveCollection: 'main' }, - 'route-map': { definitiveCollection: 'main' }, - serializer: { definitiveCollection: 'models' }, - service: { definitiveCollection: 'services' }, - template: { definitiveCollection: 'components' }, - 'template-compiler': { definitiveCollection: 'main' }, - transform: { definitiveCollection: 'transforms' }, - view: { definitiveCollection: 'views' }, - '-view-registry': { definitiveCollection: 'main' }, - '-bucket-cache': { definitiveCollection: 'main' }, - '-environment': { definitiveCollection: 'main' }, - '-application-instance': { definitiveCollection: 'main' } - }, - collections: { - 'main': { - types: ['router', '-bucket-cache', 'component-lookup', '-view-registry', 'event_dispatcher', 'application', 'location', 'renderer', '-environment', '-application-instance', 'route-map'] - }, - components: { - group: 'ui', - privateCollections: ['utils'], - types: ['component', 'helper', 'template', 'modifier'] - }, - 'component-managers': { - types: ['component-manager'] - }, - config: { - unresolvable: true - }, - initializers: { - group: 'init', - defaultType: 'initializer', - privateCollections: ['utils'], - types: ['initializer'] - }, - 'instance-initializers': { - group: 'init', - defaultType: 'instance-initializer', - privateCollections: ['utils'], - types: ['instance-initializers'] - }, - models: { - group: 'data', - defaultType: 'model', - privateCollections: ['utils'], - types: ['model', 'adapter', 'serializer'] - }, - 'modifier-managers': { - types: ['modifier-manager'] - }, - partials: { - group: 'ui', - defaultType: 'partial', - privateCollections: ['utils'], - types: ['partial'] - }, - routes: { - group: 'ui', - defaultType: 'route', - privateCollections: ['components', 'utils'], - types: ['route', 'controller', 'template'] - }, - services: { - defaultType: 'service', - privateCollections: ['utils'], - types: ['service'] - }, - utils: { - unresolvable: true - }, - views: { - defaultType: 'view', - privateCollections: ['utils'], - types: ['view'] - }, - transforms: { - group: 'data', - defaultType: 'transform', - privateCollections: ['utils'], - types: ['transform'] - } - } }; + + mergeAddonsConfig(moduleConfig, addonsConfig); + + return Object.assign(config, moduleConfig); } diff --git a/mu-trees/addon/module-config.js b/mu-trees/addon/module-config.js new file mode 100644 index 00000000..f6cae730 --- /dev/null +++ b/mu-trees/addon/module-config.js @@ -0,0 +1,109 @@ +/* + * This config describes canonical Ember, as described in the + * module unification spec: + * + * https://github.com/emberjs/rfcs/blob/master/text/0143-module-unification.md + * + */ +export default { + types: { + adapter: { definitiveCollection: 'models' }, + application: { definitiveCollection: 'main' }, + config: { definitiveCollection: 'config' }, + controller: { definitiveCollection: 'routes' }, + component: { definitiveCollection: 'components' }, + 'component-lookup': { definitiveCollection: 'main' }, + 'component-manager': { definitiveCollection: 'component-managers' }, + event_dispatcher: { definitiveCollection: 'main' }, + helper: { definitiveCollection: 'components' }, + initializer: { definitiveCollection: 'initializers' }, + 'instance-initializers': { definitiveCollection: 'instance-initializer' }, + location: { definitiveCollection: 'main' }, + model: { definitiveCollection: 'models' }, + modifier: { definitiveCollection: 'components' }, + 'modifier-manager': { definitiveCollection: 'modifier-managers' }, + partial: { definitiveCollection: 'partials' }, + renderer: { definitiveCollection: 'main' }, + route: { definitiveCollection: 'routes' }, + router: { definitiveCollection: 'main' }, + 'route-map': { definitiveCollection: 'main' }, + serializer: { definitiveCollection: 'models' }, + service: { definitiveCollection: 'services' }, + template: { definitiveCollection: 'components' }, + 'template-compiler': { definitiveCollection: 'main' }, + transform: { definitiveCollection: 'transforms' }, + view: { definitiveCollection: 'views' }, + '-view-registry': { definitiveCollection: 'main' }, + '-bucket-cache': { definitiveCollection: 'main' }, + '-environment': { definitiveCollection: 'main' }, + '-application-instance': { definitiveCollection: 'main' } + }, + collections: { + 'main': { + types: ['router', '-bucket-cache', 'component-lookup', '-view-registry', 'event_dispatcher', 'application', 'location', 'renderer', '-environment', '-application-instance', 'route-map'] + }, + components: { + group: 'ui', + privateCollections: ['utils'], + types: ['component', 'helper', 'template', 'modifier'] + }, + 'component-managers': { + types: ['component-manager'] + }, + config: { + unresolvable: true + }, + initializers: { + group: 'init', + defaultType: 'initializer', + privateCollections: ['utils'], + types: ['initializer'] + }, + 'instance-initializers': { + group: 'init', + defaultType: 'instance-initializer', + privateCollections: ['utils'], + types: ['instance-initializers'] + }, + models: { + group: 'data', + defaultType: 'model', + privateCollections: ['utils'], + types: ['model', 'adapter', 'serializer'] + }, + 'modifier-managers': { + types: ['modifier-manager'] + }, + partials: { + group: 'ui', + defaultType: 'partial', + privateCollections: ['utils'], + types: ['partial'] + }, + routes: { + group: 'ui', + defaultType: 'route', + privateCollections: ['components', 'utils'], + types: ['route', 'controller', 'template'] + }, + services: { + defaultType: 'service', + privateCollections: ['utils'], + types: ['service'] + }, + utils: { + unresolvable: true + }, + views: { + defaultType: 'view', + privateCollections: ['utils'], + types: ['view'] + }, + transforms: { + group: 'data', + defaultType: 'transform', + privateCollections: ['utils'], + types: ['transform'] + } + } +}; diff --git a/mu-trees/addon/utils/merge-addons-config.js b/mu-trees/addon/utils/merge-addons-config.js new file mode 100644 index 00000000..e4e249cf --- /dev/null +++ b/mu-trees/addon/utils/merge-addons-config.js @@ -0,0 +1,43 @@ +/* + This function merges the types and collections from addons `addonsConfig` into `config`. + + It will throw an exception if an addon tries to override + an existing type or collection on the resolver config object or + if two addons provide the same type or collection. + + - `config`: is a resolver config object. + + - `addonsConfig`: is a hash object containing the result of the call of + `addon.resolverConfig` method on all the project addons. + + ```js + { + my-addon1: { types: { ... } }, + my-addon2: { collections: { ... } }, + hola-addon: { types: { ... }, collections: { ... } } + } + ``` + */ +export default function mergeAddonsConfig(config, addonsConfig) { + + Object.keys(addonsConfig).forEach(function (addonName) { + let addonConfig = addonsConfig[addonName]; + let addonTypes = addonConfig.types || {}; + let addonCollections = addonConfig.collections || {}; + + Object.keys(addonTypes).forEach(function (key) { + if (!config.types.hasOwnProperty(key)) { + config.types[key] = addonTypes[key]; + } else { + throw new Error(`Addon '${addonName}' attempts to configure the type '${key}' on the resolver but '${key}' has already been configured.`); + } + }); + Object.keys(addonCollections).forEach(function (key) { + if (!config.collections.hasOwnProperty(key)) { + config.collections[key] = addonCollections[key]; + } else { + throw new Error(`Addon '${addonName}' attempts to configure the collection '${key}' on the resolver but '${key}' has already been configured.`); + } + }); + }); +} diff --git a/mu-trees/tests/unit/utils/merge-addons-config-test.js b/mu-trees/tests/unit/utils/merge-addons-config-test.js new file mode 100644 index 00000000..cdd3971f --- /dev/null +++ b/mu-trees/tests/unit/utils/merge-addons-config-test.js @@ -0,0 +1,74 @@ +import { module, test } from "qunit"; +import mergeAddonsConfig from "ember-resolver/utils/merge-addons-config"; + +module("ember-resolver/utils/merge-addons-config"); + +const emptyConfig = function() { + return { types: {}, collections: {} }; +}; + +test("Trigger error if two addons configure the same type", function(assert) { + let addonsConfig = { + "my-addon1": { types: { hola: "hola" } }, + "my-addon2": { types: { hola: "adios" } } + }; + + assert.throws(function() { + mergeAddonsConfig(emptyConfig(), addonsConfig); + }); +}); + +test("Trigger error if two addons configure the same collection", function(assert) { + let addonsConfig = { + "my-addon1": { collections: { hola: "hola" } }, + "my-addon2": { collections: { hola: "adios" } } + }; + + assert.throws(function() { + mergeAddonsConfig(emptyConfig(), addonsConfig); + }); +}); + +test("Trigger error if addon overwrite an existing type", function(assert) { + let addonsConfig = { + "my-addon1": { types: { hola: "new-hola" } } + }; + + let config = { types: { hola: "hola" } }; + assert.throws(function() { + mergeAddonsConfig(config, addonsConfig); + }); +}); + +test("Trigger error if addon overwrite an existing collection", function(assert) { + let addonsConfig = { + "my-addon1": { collections: { hola: "new-hola" } } + }; + + let config = { collections: { hola: "hola" } }; + assert.throws(function() { + mergeAddonsConfig(config, addonsConfig); + }); +}); + +test("Can merge collections", function(assert) { + let addonsConfig = { + "my-addon1": { collections: { col1: "foo" } }, + "my-addon2": { collections: { col2: "baz" } } + }; + + let result = emptyConfig(); + mergeAddonsConfig(result, addonsConfig); + assert.deepEqual(result.collections, { col1: "foo", col2: "baz" }); +}); + +test("Can merge types", function(assert) { + let addonsConfig = { + "my-addon1": { types: { col1: "foo" } }, + "my-addon2": { types: { col2: "baz" } } + }; + + let result = emptyConfig(); + mergeAddonsConfig(result, addonsConfig); + assert.deepEqual(result.types, { col1: "foo", col2: "baz" }); +}); diff --git a/package.json b/package.json index 03f7f3c2..a3e20f5e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@glimmer/resolver": "^0.4.1", "babel-plugin-debug-macros": "^0.1.10", + "broccoli-file-creator": "^2.1.1", "broccoli-funnel": "^2.0.2", "broccoli-merge-trees": "^3.0.0", "ember-cli-babel": "^6.16.0", diff --git a/yarn.lock b/yarn.lock index f86322b1..06e2fa93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1542,6 +1542,14 @@ broccoli-debug@^0.6.4, broccoli-debug@^0.6.5: symlink-or-copy "^1.1.8" tree-sync "^1.2.2" +broccoli-file-creator@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/broccoli-file-creator/-/broccoli-file-creator-2.1.1.tgz#7351dd2496c762cfce7736ce9b49e3fce0c7b7db" + integrity sha512-YpjOExWr92C5vhnK0kmD81kM7U09kdIRZk9w4ZDCDHuHXW+VE/x6AGEOQQW3loBQQ6Jk+k+TSm8dESy4uZsnjw== + dependencies: + broccoli-plugin "^1.1.0" + mkdirp "^0.5.1" + broccoli-filter@^1.0.1: version "1.3.0" resolved "https://registry.yarnpkg.com/broccoli-filter/-/broccoli-filter-1.3.0.tgz#71e3a8e32a17f309e12261919c5b1006d6766de6" @@ -1695,9 +1703,10 @@ broccoli-persistent-filter@^2.1.1: symlink-or-copy "^1.0.1" walk-sync "^0.3.1" -broccoli-plugin@^1.0.0, broccoli-plugin@^1.2.0, broccoli-plugin@^1.2.1, broccoli-plugin@^1.3.0, broccoli-plugin@^1.3.1: +broccoli-plugin@^1.0.0, broccoli-plugin@^1.1.0, broccoli-plugin@^1.2.0, broccoli-plugin@^1.2.1, broccoli-plugin@^1.3.0, broccoli-plugin@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.3.1.tgz#a26315732fb99ed2d9fb58f12a1e14e986b4fabd" + integrity sha512-DW8XASZkmorp+q7J4EeDEZz+LoyKLAd2XZULXyD9l4m9/hAKV3vjHmB1kiUshcWAYMgTP1m2i4NnqCE/23h6AQ== dependencies: promise-map-series "^0.2.1" quick-temp "^0.1.3"