├── test ├── nested │ ├── project_modules │ └── test.js ├── project_modules │ ├── testModuleTwo │ │ └── index.js │ ├── metadataProject │ │ └── index.js │ ├── sameNameAsTransitiveDependency │ │ └── index.js │ ├── testModuleSimple │ │ └── index.js │ ├── testModuleLocalOnly │ │ └── index.js │ ├── nestedModulesTest │ │ └── nestedModule │ │ │ └── index.js │ ├── testDifferentConstruct │ │ └── index.js │ └── testOrderOfOperations │ │ └── index.js ├── node_modules │ ├── testModuleThree │ │ └── index.js │ ├── testModuleFour │ │ ├── index.js │ │ └── node_modules │ │ │ └── testModuleFourParent │ │ │ └── index.js │ ├── sameNameAsTransitiveDependency │ │ └── index.js │ ├── metadataNpm │ │ └── index.js │ ├── improveTestOriginal │ │ └── index.js │ ├── replaceTestOriginal │ │ └── index.js │ ├── testBundle │ │ ├── index.js │ │ └── localModules │ │ │ ├── bundleModuleOne │ │ │ └── index.js │ │ │ └── bundleModuleTwo │ │ │ └── index.js │ ├── improveTestReplacement │ │ └── index.js │ ├── replaceTestReplacement │ │ └── index.js │ ├── testModule │ │ └── index.js │ ├── testBeforeConstructAsync │ │ └── index.js │ ├── failingModuleSync │ │ └── index.js │ ├── testModuleTwo │ │ └── index.js │ ├── failingBeforeConstructSync │ │ └── index.js │ ├── testOrderOfOperations │ │ └── index.js │ ├── failingModuleAsync │ │ └── index.js │ └── failingBeforeConstructAsync │ │ └── index.js ├── package.json └── test.js ├── .travis.yml ├── badges ├── npm-audit-badge.png └── npm-audit-badge.svg ├── .gitignore ├── package.json ├── index.js └── README.md /test/nested/project_modules: -------------------------------------------------------------------------------- 1 | ../project_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - lts/* 5 | -------------------------------------------------------------------------------- /test/project_modules/testModuleTwo/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'red' 3 | }; -------------------------------------------------------------------------------- /test/node_modules/testModuleThree/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extend: 'testModuleTwo' 3 | }; 4 | -------------------------------------------------------------------------------- /test/node_modules/testModuleFour/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extend: 'testModuleFourParent' 3 | }; 4 | -------------------------------------------------------------------------------- /badges/npm-audit-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/moog-require/main/badges/npm-audit-badge.png -------------------------------------------------------------------------------- /test/project_modules/metadataProject/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'green', 3 | extend: 'metadataNpm' 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | npm-debug.log 3 | *.DS_Store 4 | /node_modules 5 | # We do not commit CSS, only LESS 6 | public/css/*.css 7 | -------------------------------------------------------------------------------- /test/project_modules/sameNameAsTransitiveDependency/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | construct(self) { 3 | self.confirm = 'loaded'; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /test/node_modules/sameNameAsTransitiveDependency/index.js: -------------------------------------------------------------------------------- 1 | throw new Error('This should never have been loaded from npm because it is not in package.json'); 2 | -------------------------------------------------------------------------------- /test/node_modules/metadataNpm/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'red', 3 | construct: function(self, options) { 4 | self._options = options; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/node_modules/improveTestOriginal/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'red', 3 | construct: function(self, options) { 4 | self._options = options; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/node_modules/replaceTestOriginal/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'red', 3 | construct: function(self, options) { 4 | self._options = options; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/project_modules/testModuleSimple/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | construct: function(self, options) { 3 | self._options = options; 4 | }, 5 | color: 'red' 6 | }; 7 | -------------------------------------------------------------------------------- /test/node_modules/testBundle/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moogBundle: { 3 | modules: ['bundleModuleOne', 'bundleModuleTwo'], 4 | directory: 'localModules' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/project_modules/testModuleLocalOnly/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'purple', 3 | construct: function(self, options) { 4 | self._options = options; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/node_modules/testModuleFour/node_modules/testModuleFourParent/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | age: 70, 3 | construct: function(self, options) { 4 | self._options = options; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/node_modules/improveTestReplacement/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | size: 'large', 3 | improve: 'improveTestOriginal', 4 | construct: function(self, options) { 5 | self._options = options; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/node_modules/replaceTestReplacement/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | size: 'large', 3 | replace: 'replaceTestOriginal', 4 | construct: function(self, options) { 5 | self._options = options; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/project_modules/nestedModulesTest/nestedModule/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | construct: function(self, options) { 3 | console.log('constructing'); 4 | self._options = options; 5 | }, 6 | color: 'green' 7 | }; 8 | -------------------------------------------------------------------------------- /test/node_modules/testModule/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | beforeConstruct: function(self, options) { 4 | self._bcOptions = options; 5 | }, 6 | construct: function(self, options) { 7 | self._options = options; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/project_modules/testDifferentConstruct/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | beforeConstruct: function(self, options) { 3 | self._bcDifferentOptions = options; 4 | }, 5 | construct: function(self, options) { 6 | self._differentOptions = options; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/node_modules/testBeforeConstructAsync/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | age: 30, 4 | beforeConstruct: function(self, options, callback) { 5 | self._options = options; 6 | return setImmediate(callback); 7 | }, 8 | construct: function(self, options) { } 9 | }; -------------------------------------------------------------------------------- /test/node_modules/failingModuleSync/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | beforeConstruct: function(self, options) { }, 4 | construct: function(self, options) { 5 | self._options = options; 6 | // a wild error appears! 7 | throw new Error('I have failed.'); 8 | } 9 | }; -------------------------------------------------------------------------------- /test/node_modules/testModuleTwo/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | age: 30, 4 | beforeConstruct: function(self, options) { }, 5 | construct: function(self, options, callback) { 6 | self._options = options; 7 | return setImmediate(callback); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/node_modules/testBundle/localModules/bundleModuleOne/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | beforeConstruct: function(self, options) { 4 | self._bcOptions = options; 5 | }, 6 | construct: function(self, options) { 7 | self._options = options; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/node_modules/testBundle/localModules/bundleModuleTwo/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | beforeConstruct: function(self, options) { 4 | self._bcOptions = options; 5 | }, 6 | construct: function(self, options) { 7 | self._options = options; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/node_modules/failingBeforeConstructSync/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | beforeConstruct: function(self, options) { 4 | self._options = options; 5 | // a wild error appears! 6 | throw new Error('I have failed.'); 7 | }, 8 | construct: function(self, options) { } 9 | }; -------------------------------------------------------------------------------- /test/node_modules/testOrderOfOperations/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | beforeConstruct: function(self, options) { 3 | self._bcOrderOfOperations = (self._bcOrderOfOperations || []).concat('last'); 4 | }, 5 | construct: function(self, options) { 6 | self._orderOfOperations = (self._orderOfOperations || []).concat('first'); 7 | } 8 | }; -------------------------------------------------------------------------------- /test/project_modules/testOrderOfOperations/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | beforeConstruct: function(self, options) { 3 | self._bcOrderOfOperations = (self._bcOrderOfOperations || []).concat('notlast'); 4 | }, 5 | construct: function(self, options) { 6 | self._orderOfOperations = (self._orderOfOperations || []).concat('second'); 7 | } 8 | }; -------------------------------------------------------------------------------- /test/node_modules/failingModuleAsync/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | beforeConstruct: function(self, options) { }, 4 | construct: function(self, options, callback) { 5 | self._options = options; 6 | // a wild error appears! 7 | return setImmediate(function() { 8 | return callback(new Error('I have failed.')); 9 | }); 10 | } 11 | }; -------------------------------------------------------------------------------- /test/node_modules/failingBeforeConstructAsync/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | color: 'blue', 3 | beforeConstruct: function(self, options, callback) { 4 | self._options = options; 5 | // a wild error appears! 6 | return setImmediate(function() { 7 | return callback(new Error('I have failed.')); 8 | }); 9 | }, 10 | construct: function(self, options) { } 11 | }; -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "failingBeforeConstructAsync": "1.0.0", 4 | "failingBeforeConstructSync": "1.0.0", 5 | "failingModuleAsync": "1.0.0", 6 | "failingModuleSync": "1.0.0", 7 | "improveTestOriginal": "1.0.0", 8 | "improveTestReplacement": "1.0.0", 9 | "metadataNpm": "1.0.0", 10 | "replaceTestOriginal": "1.0.0", 11 | "replaceTestReplacement": "1.0.0", 12 | "testBeforeConstructAsync": "1.0.0" 13 | }, 14 | "devDependencies": { 15 | "testBundle": "1.0.0", 16 | "testModule": "1.0.0", 17 | "testModuleFour": "1.0.0", 18 | "testModuleThree": "1.0.0", 19 | "testModuleTwo": "1.0.0", 20 | "testOrderOfOperations": "1.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /badges/npm-audit-badge.svg: -------------------------------------------------------------------------------- 1 | vulnerabilitiesvulnerabilities00 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Apostrophe Technologies" 4 | }, 5 | "bugs": { 6 | "url": "https://github.com/apostrophecms/moog-require/issues" 7 | }, 8 | "bundleDependencies": false, 9 | "dependencies": { 10 | "async": "^1.0.0", 11 | "glob": "^7.1.3", 12 | "import-fresh": "^3.0.0", 13 | "lodash": "^4.0.0", 14 | "moog": "^1.0.0", 15 | "resolve": "^1.7.1", 16 | "resolve-from": "^4.0.0" 17 | }, 18 | "deprecated": false, 19 | "description": "moog-require extends moog with support for type definitions in local files and npm modules.", 20 | "devDependencies": { 21 | "mocha": "^5.0.0" 22 | }, 23 | "homepage": "https://github.com/apostrophecms/moog-require#readme", 24 | "keywords": [ 25 | "apostrophe", 26 | "subclass", 27 | "subclassing", 28 | "module", 29 | "loader" 30 | ], 31 | "license": "MIT", 32 | "main": "index.js", 33 | "name": "moog-require", 34 | "repository": { 35 | "type": "git", 36 | "url": "git://github.com/apostrophecms/moog-require.git" 37 | }, 38 | "scripts": { 39 | "test": "mocha test/test.js test/nested/test.js" 40 | }, 41 | "version": "1.3.2" 42 | } 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var _ = require('lodash'); 3 | var fs = require('fs'); 4 | var npmResolve = require('resolve'); 5 | var path = require('path'); 6 | var glob = require('glob'); 7 | var importFresh = require('import-fresh'); 8 | var resolveFrom = require('resolve-from'); 9 | 10 | module.exports = function(options) { 11 | var self = require('moog')(options); 12 | 13 | if (!self.options.root) { 14 | throw 'The root option is required. Pass the node variable "module" as root. This allows moog to require modules on your behalf.'; 15 | } 16 | 17 | self.root = self.options.root; 18 | 19 | self.bundled = {}; 20 | 21 | self.improvements = {}; 22 | 23 | if (self.options.bundles) { 24 | _.each(self.options.bundles, function(bundleName) { 25 | var bundlePath = getNpmPath(self.root.filename, bundleName); 26 | if (!bundlePath) { 27 | throw 'The configured bundle ' + bundleName + ' was not found in npm.'; 28 | } 29 | var bundle = importFresh(bundlePath); 30 | if (!bundle.moogBundle) { 31 | throw 'The configured bundle ' + bundleName + ' does not export a moogBundle property.'; 32 | } 33 | var modules = bundle.moogBundle.modules; 34 | if (!modules) { 35 | throw 'The configured bundle ' + bundleName + ' does not have a "modules" property within its "moogBundle" property.'; 36 | } 37 | _.each(modules, function(name) { 38 | self.bundled[name] = path.normalize(path.dirname(bundlePath) + '/' + bundle.moogBundle.directory + '/' + name + '/index.js'); 39 | }); 40 | }); 41 | } 42 | 43 | var superDefine = self.define; 44 | 45 | if (options.nestedModuleSubdirs) { 46 | self._globCache = {}; 47 | } 48 | 49 | self.define = function(type, definition, extending) { 50 | 51 | var result; 52 | 53 | // For the define-many-at-once case let the base class do the work 54 | if (typeof(type) === 'object') { 55 | return superDefine(type); 56 | } 57 | 58 | var projectLevelDefinition; 59 | var npmDefinition; 60 | var originalType; 61 | var projectLevelPath = self.options.localModules + '/' + type + '/index.js'; 62 | 63 | if (options.nestedModuleSubdirs) { 64 | if (!self._indexes) { 65 | // Fetching a list of index.js files on the first call and then searching it each time for 66 | // one that refers to the right type name shaves as much as 60 seconds off the startup 67 | // time in a large project, compared to using the glob cache feature 68 | self._indexes = glob.sync(self.options.localModules + '/**/index.js'); 69 | } 70 | var matches = self._indexes.filter(function(index) { 71 | return index.endsWith('/' + type + '/index.js'); 72 | }); 73 | if (matches.length > 1) { 74 | throw new Error('The module ' + type + ' appears in multiple locations:\n' + matches.join('\n')); 75 | } 76 | projectLevelPath = matches[0] ? path.normalize(matches[0]) : projectLevelPath; 77 | } 78 | if (fs.existsSync(projectLevelPath)) { 79 | projectLevelDefinition = importFresh(resolveFrom(path.dirname(self.root.filename), projectLevelPath)); 80 | } 81 | 82 | var relativeTo; 83 | if (extending) { 84 | relativeTo = extending.__meta.filename; 85 | } else { 86 | relativeTo = self.root.filename; 87 | } 88 | 89 | var npmPath = getNpmPath(relativeTo, type); 90 | if (npmPath) { 91 | npmDefinition = importFresh(npmPath); 92 | npmDefinition.__meta = { 93 | npm: true, 94 | dirname: path.dirname(npmPath), 95 | filename: npmPath, 96 | name: type 97 | }; 98 | if (npmDefinition.improve) { 99 | // Remember which types were actually improvements of other types for 100 | // the benefit of applications that would otherwise instantiate them all 101 | self.improvements[type] = true; 102 | // Improve an existing type with an implicit subclass, 103 | // rather than defining one under a new name 104 | originalType = type; 105 | type = npmDefinition.improve; 106 | // If necessary, start by autoloading the original type 107 | if (!self.isDefined(type, { autoload: false })) { 108 | self.define(type); 109 | } 110 | } else if (npmDefinition.replace) { 111 | // Replace an existing type with the one defined by 112 | // this npm module 113 | delete self.definitions[npmDefinition.replace]; 114 | type = npmDefinition.replace; 115 | } 116 | } 117 | 118 | if (!(definition || projectLevelDefinition || npmDefinition)) { 119 | // Can't find it nohow. Use the standard undefined type error message 120 | return superDefine(type); 121 | } 122 | 123 | if (!definition) { 124 | definition = {}; 125 | } 126 | 127 | projectLevelDefinition = projectLevelDefinition || {}; 128 | 129 | projectLevelDefinition.__meta = { 130 | dirname: path.dirname(projectLevelPath), 131 | filename: projectLevelPath 132 | }; 133 | 134 | _.defaults(definition, projectLevelDefinition); 135 | 136 | // Insert the npm definition as a defined type, then let the 137 | // base class define the local definition normally. This results 138 | // in an implicit base class, allowing local template overrides 139 | // even if there is no other local code 140 | if (npmDefinition) { 141 | result = superDefine(type, npmDefinition); 142 | if (npmDefinition.improve) { 143 | // Restore the name of the improving module as otherwise our asset chains have 144 | // multiple references to my-foo which is ambiguous 145 | result.__meta.name = originalType; 146 | } 147 | } 148 | result = superDefine(type, definition); 149 | if (npmDefinition && npmDefinition.improve) { 150 | // Restore the name of the improving module as otherwise our asset chains have 151 | // multiple references to my-foo which is ambiguous 152 | result.__meta.name = self.originalToMy(originalType); 153 | } 154 | return result; 155 | }; 156 | 157 | function getNpmPath(parentPath, type) { 158 | parentPath = path.resolve(parentPath); 159 | if (_.has(self.bundled, type)) { 160 | return self.bundled[type]; 161 | } 162 | // Even if the package exists in node_modules it might just be a 163 | // sub-dependency due to npm/yarn flattening, which means we could be 164 | // confused by an unrelated npm module with the same name as an Apostrophe 165 | // module unless we verify it is a real project-level dependency. However 166 | // if no package.json at all exists at project level we do search up the 167 | // tree until we find one to accommodate patterns like `src/app.js` 168 | if (!self.validPackages) { 169 | let info = null; 170 | const initialFolder = path.dirname(self.root.filename); 171 | let folder = initialFolder; 172 | while (true) { 173 | const file = `${folder}/package.json`; 174 | if (fs.existsSync(file)) { 175 | const info = JSON.parse(fs.readFileSync(file, 'utf8')); 176 | self.validPackages = new Set([ ...Object.keys(info.dependencies || {}), ...Object.keys(info.devDependencies || {}) ]); 177 | break; 178 | } else { 179 | folder = path.dirname(folder); 180 | if (!folder.length) { 181 | throw new Error(`package.json was not found in ${initialFolder} or any of its parent folders.`); 182 | } 183 | } 184 | } 185 | } 186 | if (!self.validPackages.has(type)) { 187 | return null; 188 | } 189 | try { 190 | return npmResolve.sync(type, { basedir: path.dirname(parentPath) }); 191 | } catch (e) { 192 | // Not found via npm. This does not mean it doesn't 193 | // exist as a project-level thing 194 | return null; 195 | } 196 | } 197 | 198 | self.isImprovement = function(name) { 199 | return _.has(self.improvements, name); 200 | }; 201 | 202 | return self; 203 | }; 204 | 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/punkave/moog-require.svg?branch=master)](https://travis-ci.org/punkave/moog-require) 2 | 3 | # moog-require 4 | 5 | `moog-require` provides powerful module subclassing for server-side development. It extends the features of [moog](https://github.com/punkave/moog) with the following additions: 6 | 7 | * Fetches modules from a local modules folder if they are not defined explicitly 8 | * If a module is defined explicitly and also exists in localModules, the local modules folder becomes a source of defaults for properties not defined explicitly 9 | * Fetches modules from npm if they are not defined either explicitly or via the local modules folder 10 | * If a module exists by the same name both in npm and via explicit definition or local modules, automatically extends the npm module without the need for a new name (like the "category" feature of Objective C) 11 | * Provides access to an "asset chain" of subclass module directories and type names, to implement template overrides and the like 12 | * Also supports bundling moog modules in a single npm module, if explicitly configured 13 | 14 | ## Example 15 | 16 | ```javascript 17 | 18 | // IMPLICIT BASE CLASS OF ALL MODULES 19 | // (if configured - see app.js below) 20 | // 21 | // In node_modules/module/index.js 22 | 23 | module.exports = { 24 | self.construct = function(self, options) { 25 | self.renderTemplate = function(name, data) { 26 | var i; 27 | for (i = 0; (i < options.__meta.length); i++) { 28 | var meta = options.__meta[i]; 29 | var path = meta.dirname + '/views/' + name + '.html'; 30 | if (fs.existsSync(path)) { 31 | // Deepest subclass wins 32 | return templateEngine.render(path, data); 33 | } 34 | } 35 | }; 36 | 37 | }; 38 | }; 39 | 40 | // NPM MODULE 41 | // 42 | // In node_modules/events/index.js 43 | 44 | module.exports = { 45 | color: 'red', 46 | 47 | construct: function(self, options) { 48 | 49 | self.defaultTags = options.tags; 50 | 51 | self.get = function(params, callback) { 52 | // Go get some events 53 | return callback(events); 54 | }; 55 | } 56 | } 57 | 58 | // PROJECT LEVEL SUBCLASS OF NPM MODULE 59 | // 60 | // in lib/modules/events/index.js 61 | module.exports = { 62 | color: 'green', 63 | 64 | construct: function(self, options) { 65 | var superGet = self.get; 66 | self.get = function(params, callback) { 67 | // override: only interested in upcoming events 68 | params.upcoming = true; 69 | return superGet(params, callback); 70 | }; 71 | } 72 | }; 73 | 74 | // in app.js 75 | 76 | var synth = require('moog-require')({ 77 | localModules: __dirname + '/lib/modules', 78 | defaultBaseClass: 'module' 79 | }); 80 | 81 | synth.define({ 82 | 83 | // SETTING A DEFAULT OPTION THAT APPLIES TO *ALL* MODULES 84 | // (because we're setting it for the defaultBaseClass) 85 | 'module': { 86 | color: 'gray' 87 | }, 88 | 89 | // CONFIGURATION (IMPLICIT SUBCLASS) OF A PROJECT-LEVEL MODULE 90 | // (same technique works to configure an npm module) 91 | 92 | 'events': { 93 | color: 'blue', 94 | // More overrides in lib/modules/events/index.js (above). 95 | // Anything here in app.js wins 96 | }, 97 | 98 | // EXTENDING A PROJECT-LEVEL MODULE TO CREATE A NEW ONE 99 | 'parties': { 100 | 101 | // Let's subclass a module right here in app.js (usually we'd just 102 | // set site-specific options here and put code in 103 | // lib/modules/parties/index.js, but you're not restricted) 104 | 105 | extend: 'events', 106 | color: 'lavender', 107 | 108 | // Let's alter the "tags" option before the 109 | // base class constructors are aware of it 110 | 111 | beforeConstruct: function(self, options) { 112 | options.tags = (options.tags || []).concat('party'); 113 | }, 114 | 115 | // This constructor can take a callback, even though 116 | // the base classes don't. You can mix and match 117 | 118 | construct: function(self, options, callback) { 119 | // options.color will be lavender 120 | var superGet = self.get; 121 | self.get = function(params, callback) { 122 | // override: only interested in parties. Let's 123 | // assume the base class uses this as a query 124 | params.title = /party/i; 125 | return superGet(params, callback); 126 | }; 127 | 128 | // Output names and full folder paths of all modules in the 129 | // subclassing chain; we can use this to push assets and 130 | // implement template overrides 131 | console.log(options._directories); 132 | }, 133 | 134 | setBridge: function(modules) { 135 | // Do something that requires access to the 136 | // other modules, which are properties of 137 | // the modules object 138 | } 139 | } 140 | }); 141 | 142 | // Instantiate all the modules, passing in some 143 | // universal options that are provided to all of them. This 144 | // only instantiates modules mentioned in `definitions`, but 145 | // they may override or subclass modules in npm or the 146 | // project-level modules folder 147 | 148 | return synth.createAll({ mailer: myMailer }, function(err, modules) { 149 | return modules.events.get({ ... }, function(err, events) { 150 | ... 151 | }); 152 | }); 153 | 154 | // We can also tell the modules about each other. This 155 | // invokes the setBridge method of each module, if any, 156 | // and passes the modules object to it 157 | 158 | synth.bridge(modules); 159 | 160 | // We can also create an instance of any module at any time, 161 | // and pass it additional options. This is useful if you are 162 | // not following the singleton pattern. We don't promise 163 | // killer performance if you create thousands of objects 164 | // per second 165 | 166 | return synth.create('parties', { color: 'purple' }, function(err, party) { 167 | ... 168 | }); 169 | ``` 170 | 171 | ## Replacing a module with another npm module 172 | 173 | The `monsters` npm module works great for most people, but you've created a superior replacement, `scary-monsters`. And you want people to be able to use it as a drop-in replacement, without changing code that refers to the `monsters` module. 174 | 175 | This is especially useful if you want other moog types that subclass `monsters` to automatically subclass `scary-monsters` instead. 176 | 177 | So the `index.js` of your `scary-monsters` npm module might look like: 178 | 179 | ```javascript 180 | module.exports = { 181 | replace: 'monsters', 182 | construct: function(self, options) { ... } 183 | } 184 | ``` 185 | 186 | Note the `replace` property. 187 | 188 | Now, an application developer who wants to use `scary-monsters` instead of the usual `monsters` module can simply configure it instead of `monsters`. The type name `monsters` will still be defined. The type name `scary-monsters` is **not** defined. 189 | 190 | ```javascript 191 | var synth = require('moog-require')({ 192 | localModules: __dirname + '/lib/modules', 193 | defaultBaseClass: 'module' 194 | }); 195 | 196 | synth.define({ 197 | 'scary-monsters': { ... configuration ... } 198 | }); 199 | 200 | // This works 201 | synth.create('monsters', {}); 202 | 203 | // This does NOT work 204 | synth.create('scary-monsters', {}); 205 | ``` 206 | 207 | Note that if you want to further extend `scary-monsters` at project level, you should use a `lib/modules/scary-monsters` folder. Anything in `lib/modules/monsters` will be ignored. Similarly, in `app.js`, don't configure `monsters`, just configure `scary-monsters`. 208 | 209 | ## Improving a module with another npm module: implicit subclassing 210 | 211 | The `improve` property is similar to `replace`, but allows you to implicitly subclass an existing type rather than completely replacing it. If you `improve` the `monsters` type, all other code will regard your subclass as the `monsters` type. 212 | 213 | This is useful if you wish to release an npm module that subclasses a well-known module to add more functionality, without requiring developers to change the source code of other modules in order to use it. 214 | 215 | Here is an example: 216 | 217 | So the `index.js` of your `scary-monsters` npm module might look like: 218 | 219 | ```javascript 220 | module.exports = { 221 | improve: 'monsters', 222 | construct: function(self, options) { 223 | var superJump = self.jump; 224 | self.jump = function(howHigh) { 225 | // Limit height of jumps 226 | if (howHigh > 100) { 227 | howHigh = 100; 228 | } 229 | // Call original version 230 | superJump(howHigh / 2); 231 | }; 232 | } 233 | } 234 | ``` 235 | 236 | Note the `improve` property. 237 | 238 | Just like the `replace` option, the `improve` option defines the type with the name specified by `improve`. That is, your subclass is substituted everywhere for the `monsters` type. The `scary-monsters` type is **not** defined. 239 | 240 | Here is an example of application-level code: 241 | 242 | ```javascript 243 | var synth = require('moog-require')({ 244 | localModules: __dirname + '/lib/modules', 245 | defaultBaseClass: 'module' 246 | }); 247 | 248 | synth.define({ 249 | 'scary-monsters': { ... configuration ... } 250 | }); 251 | 252 | // This works 253 | synth.create('monsters', {}); 254 | 255 | // This does NOT work 256 | synth.create('scary-monsters', {}); 257 | ``` 258 | 259 | Note that if you want to further extend `scary-monsters` at project level, you should use a `lib/modules/scary-monsters` folder. Code in `lib/modules/monsters` will be loaded, but it will subclass the original `monsters` module, and then `scary-monsters` will subclass that. This is probably not what you want. Similarly, in `app.js`, don't configure `monsters`, just configure `scary-monsters`. 260 | 261 | ## Calling `require` yourself 262 | 263 | Don't. 264 | 265 | Well, okay... 266 | 267 | If you want to write this: 268 | 269 | ```javascript 270 | `extend': require('./lib/weird-place/my-module/index.js') 271 | ``` 272 | 273 | You may do so, but in that case your module must export its `__name`, `__dirname` and `__filename`, like so: 274 | 275 | ```javascript 276 | module.exports = { 277 | __name: 'my-module', 278 | __dirname: __dirname, 279 | __filename: __filename, 280 | construct: function(self, options) { ... } 281 | }; 282 | ``` 283 | 284 | This is only necessary if you are using `require` directly. Most of the time, you will be happier if you just specify a module name and let us `require` it for you. This even works in npm modules. (Yes, it will still find it if it is an npm dependency of your own module.) 285 | 286 | ## Packaging multiple moog-require modules in a single npm module 287 | 288 | Sometimes several modules are conceptually distinct, but are developed and versioned in tandem. In these cases there is no benefit from separate packaging, just a significant delay in `npm install`. npm peer dependencies are one way to handle this, but [npm peer dependencies may be on the chopping block](http://dailyjs.com/2014/04/16/node-roundup/), and they are significantly slower than pre-packaging modules together. 289 | 290 | The difficulty of course is that the link between npm module names and moog-require module names is broken when we do this. So we need another way to indicate to moog-require that it should look in the appropriate place. 291 | 292 | Since searching for "X", where X is actually provided by module "Y", is not a core feature of npm itself we have kept this mechanism simple: you can give `moog-require` an array of npm module names that contain a "bundle" of definitions rather than a single definition. An npm "bundle" module then must export a `moogBundle` array property which contains the names of the moog-require modules it defines. The actual definitions live in `lib/modules/module-one/index.js`, `lib/modules/module-two/index.js`, etc. *within the bundle npm module*. `moog-require` will find these automatically and will consider these first before requiring normally from npm. 293 | 294 | Here's an example: 295 | 296 | ```javascript 297 | // In node_modules/mybundle/index.js 298 | 299 | module.exports = { 300 | moogBundle: { 301 | modules: [ 'module-one', 'module-two' ], 302 | directory: 'lib/modules' 303 | } 304 | }; 305 | 306 | // In node_modules/mybundle/lib/modules/module-one/index.js 307 | 308 | module.exports = { 309 | construct: function(self, options) { ... } 310 | }; 311 | 312 | // In node_modules/mybundle/lib/modules/module-two/index.js 313 | 314 | module.exports = { 315 | construct: function(self, options) { ... } 316 | }; 317 | ``` 318 | 319 | ```javascript 320 | // In our application 321 | 322 | var synth = require('moog-require')({ 323 | bundles: [ 'mybundle' ], 324 | localModules: __dirname + '/lib/modules', 325 | defaultBaseClass: 'module' 326 | }); 327 | 328 | synth.define({ 329 | 'module-one': {}, 330 | 'module-two': {} 331 | }); 332 | ``` 333 | 334 | Note that just as before, we must include these modules in our explicit `define` calls if we want to instantiate them with `createAll`, although we don't have to override any properties; we can pass empty objects to just use the defaults defined in the project level folder, and/or implicitly inherit from npm. 335 | 336 | However, you may explicitly `create` a type that exists only in the project level folder and/or npm. 337 | 338 | ## Nesting modules in subdirectories 339 | 340 | For your convenience, `moog-require` has optional support for loading modules from nested subdirectories. The rules of the game are very simple: 341 | 342 | * You must set the `nestedModuleSubdirs` option to `true`. 343 | * Modules can now be found nested beneath your `localModules` folder, at any depth. 344 | * The names of the parent directories **do not matter**. They are purely for your organizational convenience. 345 | * The name of the actual module directory must still be the full name of the module. 346 | 347 | If the same module exists in two places, an exception is thrown. 348 | 349 | ## Changelog 350 | 351 | **The 2.x series is deprecated for new work, as its functionality was folded into Apostrophe 3.x. See below for 1.x release notes relevant to maintenance of Apostrophe 2.x.** 352 | 353 | 1.3.2: starting in version 1.3.1, this module only loads other modules via `npm` if they are explicit npm dependencies, which is necessary for stability and security. However, it is too strict: if the project has no `package.json` at all at the level of `app.js`, `npm` search up the tree, and this module should too. Beginning in verison 1.3.2, it does search up the tree. However it stops at the first `package.json` found. 354 | 355 | 1.3.1: `moog-require` loads modules from npm if they exist there and are configured by name in the application. This was always intended only as a way to load direct, intentional dependencies of your project. However, since npm "flattens" the dependency tree, dependencies of dependencies that happen to have the same name as a project-level module could be loaded by default, crashing the site or causing unexpected behavior. So beginning with this release, `moog-require` scans `package.json` to verify an npm module is actually a dependency of the project itself before attempting to load it. 356 | 357 | 1.3.0: achieved an approximately 100x performance improvement when `nestedModuleSubdirs` is in use by fetching 358 | a list of index.js files on the first `define` call and then searching that prefetched list each 359 | time. This solution is much faster than the glob module cache. 360 | 361 | 1.2.0: use `originalToMy` to handle moog class names with npm namespaces in them. 362 | 363 | 1.1.1: use `importFresh` to avoid bugs when two instances of `moog-require` are loading the same module definitions. Previously if modules created by the two instances later modified sub-properties of `options`, they would inadvertently share values. This fix is critical for both `apostrophe-monitor` and `apostrophe-multisite`. 364 | 365 | 1.1.0: support for the `nestedModuleSubdirs` option. 366 | 367 | 1.0.1: shallowly clone the result of `require` rather than attaching `.__meta` to a potentially shared object. This allows multiple instances of `moog-require` in multiple instances of `apostrophe` to independently track where modules were loaded from. 368 | 369 | 1.0.0: `moog`, `async` and `lodash` dependencies updated to satisfy `npm audit`. Declared 1.x as this has been a stable part of Apostrophe 2.x for a long time. 370 | 371 | 0.4.1: fixed `moog` dependency to use the version that supports `autoload: false`. 372 | 373 | 0.4.0: added `isImprovement` method which returns true if a type name turned out to be an improvement of another type via the `improve` keyword. This is useful when you wish to instantiate all of the types except for those that are just improvements of others. 374 | 375 | 0.3.0: introduced the `replace` and `improve` options, which allow an npm module to substitute itself for another moog type completely, or enhance it via implicit subclassing. This is useful when releasing a drop-in replacement for a well-known module. 376 | 377 | 0.2.0: depends on `moog` 0.2.0 which introduces the `mirror` method. 378 | 379 | 0.1.0: compatible with `moog` 0.1.0 in which the `__meta` property became an object with `chain` and `name` properties. 380 | 381 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | // console.log = function(s) { 4 | // console.trace(s); 5 | // }; 6 | 7 | describe('moog', function() { 8 | 9 | describe('synth', function() { 10 | it('exists', function() { 11 | assert( require('../index.js') ); 12 | }); 13 | 14 | var synth = require('../index.js')({ 15 | localModules: __dirname + '/project_modules', 16 | root: module 17 | }); 18 | 19 | it('has a `create` method', function() { 20 | assert(synth.create); 21 | }); 22 | it('has a `createAll` method', function() { 23 | assert(synth.createAll); 24 | }); 25 | it('has a `bridge` method', function() { 26 | assert(synth.bridge); 27 | }); 28 | }); 29 | 30 | describe('synth.create', function() { 31 | var synth; 32 | 33 | it('should create a subclass with no options', function(done) { 34 | synth = require('../index.js')({ 35 | localModules: __dirname + '/project_modules', 36 | root: module 37 | }); 38 | 39 | synth.define({ 40 | 'testModule': { } 41 | }); 42 | 43 | synth.create('testModule', {}, function(err, testModule) { 44 | assert(!err); 45 | assert(testModule); 46 | assert(testModule._options.color === 'blue'); 47 | return done(); 48 | }); 49 | }); 50 | 51 | it('should create a subclass with overrides of default options', function(done) { 52 | synth = require('../index.js')({ 53 | localModules: __dirname + '/project_modules', 54 | root: module 55 | }); 56 | 57 | synth.define({ 58 | 'testModule': { 59 | color: 'red' 60 | } 61 | }); 62 | 63 | synth.create('testModule', {}, function(err, testModule) { 64 | assert(!err); 65 | assert(testModule._options.color === 'red'); 66 | return done(); 67 | }); 68 | }); 69 | 70 | it('should create a subclass with overrides of default options in localModules folder and npm', function(done) { 71 | synth = require('../index.js')({ 72 | localModules: __dirname + '/project_modules', 73 | root: module 74 | }); 75 | 76 | synth.define({ 77 | // testModuleTwo is defined in ./project_modules and 78 | // ./node_modules 79 | 'testModuleTwo': { } 80 | } 81 | ); 82 | 83 | synth.create('testModuleTwo', {}, function(err, testModuleTwo) { 84 | assert(!err); 85 | assert(testModuleTwo._options.color === 'red'); 86 | return done(); 87 | }); 88 | }); 89 | 90 | it('should create a subclass with overrides of default options at runtime', function(done) { 91 | synth = require('../index.js')({ 92 | localModules: __dirname + '/project_modules', 93 | root: module 94 | }); 95 | 96 | synth.define({ 97 | 'testModule': { } 98 | }); 99 | 100 | synth.create('testModule', { color: 'purple' }, function(err, testModule) { 101 | assert(!err); 102 | assert(testModule._options.color === 'purple'); 103 | return done(); 104 | }); 105 | }); 106 | 107 | it('should create a subclass with a new name using the `extend` property', function(done) { 108 | synth = require('../index.js')({ 109 | localModules: __dirname + '/project_modules', 110 | root: module 111 | }); 112 | 113 | synth.define({ 114 | 'myTestModuleExtend': { 115 | extend: 'testModule', 116 | color: 'red' 117 | } 118 | }); 119 | 120 | synth.create('myTestModuleExtend', {}, function(err, myTestModule) { 121 | if (err) { 122 | console.error(err); 123 | } 124 | assert(!err); 125 | assert(myTestModule); 126 | assert(myTestModule._options.color === 'red'); 127 | return done(); 128 | }); 129 | }); 130 | 131 | it('should create a subclass with a new name by extending a module defined in localModules', function(done) { 132 | synth = require('../index.js')({ 133 | localModules: __dirname + '/project_modules', 134 | root: module 135 | }); 136 | 137 | synth.define({ 138 | 'myTestModule': { 139 | extend: 'testModuleLocalOnly', 140 | newProperty: 42 141 | } 142 | }); 143 | 144 | synth.create('myTestModule', {}, function(err, myTestModule) { 145 | assert(!err); 146 | assert(myTestModule); 147 | assert(myTestModule._options.color === 'purple'); 148 | assert(myTestModule._options.newProperty === 42); 149 | return done(); 150 | }); 151 | }); 152 | 153 | it('should create a subclass of a subclass', function(done) { 154 | synth = require('../index.js')({ 155 | localModules: __dirname + '/project_modules', 156 | root: module 157 | }); 158 | 159 | synth.define({ 160 | 'myTestModule': { 161 | extend: 'testModule' 162 | }, 163 | 'mySubTestModule': { 164 | extend: 'myTestModule', 165 | color: 'orange' 166 | } 167 | }); 168 | 169 | synth.create('myTestModule', {}, function(err, myTestModule) { 170 | assert(!err); 171 | assert(myTestModule); 172 | assert(myTestModule._options.color === 'blue'); 173 | synth.create('mySubTestModule', {}, function(err, mySubTestModule) { 174 | assert(!err); 175 | assert(mySubTestModule); 176 | assert(mySubTestModule._options.color === 'orange'); 177 | return done(); 178 | }); 179 | }); 180 | }); 181 | 182 | it('should create a subclass when both parent and subclass are in npm', function(done) { 183 | synth = require('../index.js')({ 184 | localModules: __dirname + '/project_modules', 185 | root: module 186 | }); 187 | 188 | synth.define({ 189 | 'testModuleThree': {} 190 | }); 191 | 192 | synth.create('testModuleThree', {}, function(err, testModuleThree) { 193 | if (err) { 194 | console.error(err); 195 | } 196 | assert(!err); 197 | assert(testModuleThree); 198 | assert(testModuleThree._options.age === 30); 199 | return done(); 200 | }); 201 | }); 202 | 203 | }); 204 | 205 | 206 | describe('synth.createAll', function() { 207 | var synth; 208 | 209 | it('should create two subclasses', function(done) { 210 | synth = require('../index.js')({ 211 | localModules: __dirname + '/project_modules', 212 | root: module 213 | }); 214 | 215 | synth.define({ 216 | 'testModule': { }, 217 | 'testModuleTwo': { } 218 | }); 219 | 220 | synth.createAll({}, {}, function(err, modules) { 221 | assert(!err); 222 | assert(modules.testModule); 223 | assert(modules.testModuleTwo); 224 | return done(); 225 | }); 226 | }); 227 | 228 | it('should create two subclasses with runtime options passed using `specific` options', function(done) { 229 | synth = require('../index.js')({ 230 | localModules: __dirname + '/project_modules', 231 | root: module 232 | }); 233 | 234 | synth.define({ 235 | 'testModule': { }, 236 | 'testModuleTwo': { } 237 | }); 238 | 239 | synth.createAll({}, { 240 | testModule: { color: 'green' }, 241 | testModuleTwo: { color: 'green' } 242 | }, function(err, modules) { 243 | assert(!err); 244 | assert(modules.testModule); 245 | assert(modules.testModule._options.color === 'green'); 246 | assert(modules.testModuleTwo); 247 | assert(modules.testModuleTwo._options.color === 'green'); 248 | return done(); 249 | }); 250 | }); 251 | 252 | it('should create two subclasses with runtime options passed using `global` options', function(done) { 253 | synth = require('../index.js')({ 254 | localModules: __dirname + '/project_modules', 255 | root: module 256 | }); 257 | 258 | synth.define({ 259 | 'testModule': { }, 260 | 'testModuleTwo': { } 261 | }); 262 | 263 | synth.createAll({ color: 'green' }, { }, function(err, modules) { 264 | assert(!err); 265 | assert(modules.testModule); 266 | assert(modules.testModule._options.color === 'green'); 267 | assert(modules.testModuleTwo); 268 | assert(modules.testModuleTwo._options.color === 'green'); 269 | return done(); 270 | }); 271 | }); 272 | }); 273 | 274 | describe('synth.bridge', function() { 275 | it('should run successfully', function(done) { 276 | var synth = require('../index.js')({ 277 | localModules: __dirname + '/project_modules', 278 | root: module 279 | }); 280 | 281 | synth.define({ 282 | 'testModule': { }, 283 | 'testModuleTwo': { } 284 | }); 285 | 286 | synth.createAll({ }, { }, function(err, modules) { 287 | synth.bridge(modules); 288 | assert(!err); 289 | assert(modules.testModule); 290 | assert(modules.testModuleTwo); 291 | return done(); 292 | }); 293 | }); 294 | 295 | it('should pass modules to each other', function(done) { 296 | var synth = require('../index.js')({ 297 | localModules: __dirname + '/project_modules', 298 | root: module 299 | }); 300 | 301 | synth.define({ 302 | 'testModule': { 303 | construct: function(self, options) { 304 | self.setBridge = function(modules) { 305 | self.otherModule = modules.testModuleTwo; 306 | }; 307 | } 308 | }, 309 | 'testModuleTwo': { 310 | construct: function(self, options) { 311 | self.setBridge = function(modules) { 312 | self.otherModule = modules.testModule; 313 | }; 314 | } 315 | } 316 | }); 317 | 318 | synth.createAll({ }, { }, function(err, modules) { 319 | assert(!err); 320 | synth.bridge(modules); 321 | assert(modules.testModule.otherModule); 322 | assert(modules.testModuleTwo.otherModule); 323 | return done(); 324 | }); 325 | }); 326 | }); 327 | 328 | describe('module structure', function() { 329 | 330 | it('should accept a `defaultBaseClass` that is inherited by empty definitions', function(done) { 331 | var synth = require('../index.js')({ 332 | localModules: __dirname + '/project_modules', 333 | defaultBaseClass: 'testModule', 334 | root: module 335 | }); 336 | 337 | synth.define({ 338 | 'newModule': { } 339 | }); 340 | 341 | synth.createAll({ }, { }, function(err, modules) { 342 | assert(!err); 343 | assert(modules.newModule); 344 | assert(modules.newModule._options.color === 'blue'); 345 | return done(); 346 | }); 347 | }); 348 | 349 | // ================================================================= 350 | // PASSING 351 | // ================================================================= 352 | 353 | it('should accept a synchronous `construct` method', function(done) { 354 | var synth = require('../index.js')({ 355 | localModules: __dirname + '/project_modules', 356 | root: module 357 | }); 358 | 359 | synth.define({ 360 | 'testModule': { } 361 | }); 362 | 363 | synth.createAll({ }, { }, function(err, modules) { 364 | assert(!err); 365 | assert(modules.testModule); 366 | return done(); 367 | }); 368 | }); 369 | 370 | it('should accept an asynchronous `construct` method', function(done) { 371 | var synth = require('../index.js')({ 372 | localModules: __dirname + '/project_modules', 373 | root: module 374 | }); 375 | 376 | synth.define({ 377 | 'testModuleTwo': { } 378 | }); 379 | 380 | synth.createAll({ }, { }, function(err, modules) { 381 | assert(!err); 382 | assert(modules.testModuleTwo); 383 | return done(); 384 | }); 385 | }); 386 | 387 | it('should accept a synchronous `beforeConstruct` method', function(done) { 388 | var synth = require('../index.js')({ 389 | localModules: __dirname + '/project_modules', 390 | root: module 391 | }); 392 | 393 | synth.define({ 394 | 'testModule': { } 395 | }); 396 | 397 | synth.createAll({ }, { }, function(err, modules) { 398 | assert(!err); 399 | assert(modules.testModule); 400 | return done(); 401 | }); 402 | }); 403 | 404 | it('should accept an asynchronous `beforeConstruct` method', function(done) { 405 | var synth = require('../index.js')({ 406 | localModules: __dirname + '/project_modules', 407 | root: module 408 | }); 409 | 410 | synth.define({ 411 | 'testBeforeConstructAsync': { } 412 | }); 413 | 414 | synth.createAll({ }, { }, function(err, modules) { 415 | assert(!err); 416 | assert(modules.testBeforeConstructAsync); 417 | return done(); 418 | }); 419 | }); 420 | 421 | // ================================================================= 422 | // FAILING 423 | // ================================================================= 424 | 425 | it('should catch a synchronous Error during `construct`', function(done) { 426 | var synth = require('../index.js')({ 427 | localModules: __dirname + '/project_modules', 428 | root: module 429 | }); 430 | 431 | synth.define({ 432 | 'failingModuleSync': { } 433 | }); 434 | 435 | synth.createAll({ }, { }, function(err, modules) { 436 | assert(err); 437 | assert(err.message === 'I have failed.'); 438 | return done(); 439 | }); 440 | }); 441 | 442 | it('should catch an asynchronous Error during `construct`', function(done) { 443 | var synth = require('../index.js')({ 444 | localModules: __dirname + '/project_modules', 445 | root: module 446 | }); 447 | 448 | synth.define({ 449 | 'failingModuleAsync': { } 450 | }); 451 | 452 | synth.createAll({ }, { }, function(err, modules) { 453 | assert(err); 454 | assert(err.message === 'I have failed.'); 455 | return done(); 456 | }); 457 | }); 458 | 459 | it('should catch a synchronous Error during `beforeConstruct`', function(done) { 460 | var synth = require('../index.js')({ 461 | localModules: __dirname + '/project_modules', 462 | root: module 463 | }); 464 | 465 | synth.define({ 466 | 'failingBeforeConstructSync': { } 467 | }); 468 | 469 | synth.createAll({ }, { }, function(err, modules) { 470 | assert(err); 471 | assert(err.message === 'I have failed.'); 472 | return done(); 473 | }); 474 | }); 475 | 476 | it('should catch an asynchronous Error during `beforeConstruct`', function(done) { 477 | var synth = require('../index.js')({ 478 | localModules: __dirname + '/project_modules', 479 | root: module 480 | }); 481 | 482 | synth.define({ 483 | 'failingBeforeConstructAsync': { } 484 | }); 485 | 486 | synth.createAll({ }, { }, function(err, modules) { 487 | assert(err); 488 | assert(err.message === 'I have failed.'); 489 | return done(); 490 | }); 491 | }); 492 | }); 493 | 494 | describe('order of operations', function() { 495 | 496 | // ================================================================= 497 | // MULTIPLE `construct`s AND `beforeConstruct`s 498 | // ================================================================= 499 | 500 | it('should call both the project-level `construct` and the npm module\'s `construct`', function(done) { 501 | var synth = require('../index.js')({ 502 | localModules: __dirname + '/project_modules', 503 | root: module 504 | }); 505 | 506 | synth.define({ 507 | 'testDifferentConstruct': { 508 | extend: 'testModule' 509 | } 510 | }); 511 | 512 | synth.createAll({ }, { }, function(err, modules) { 513 | assert(!err); 514 | assert(modules.testDifferentConstruct._options); 515 | assert(modules.testDifferentConstruct._differentOptions); 516 | return done(); 517 | }); 518 | }); 519 | 520 | it('should call both the project-level `beforeConstruct` and the npm module\'s `beforeConstruct`', function(done) { 521 | var synth = require('../index.js')({ 522 | localModules: __dirname + '/project_modules', 523 | root: module 524 | }); 525 | 526 | synth.define({ 527 | 'testDifferentConstruct': { 528 | extend: 'testModule' 529 | } 530 | }); 531 | 532 | synth.createAll({ }, { }, function(err, modules) { 533 | assert(!err); 534 | assert(modules.testDifferentConstruct._bcOptions); 535 | assert(modules.testDifferentConstruct._bcDifferentOptions); 536 | return done(); 537 | }); 538 | }); 539 | 540 | it('should override the project-level `construct` using a definitions-level `construct`', function(done) { 541 | var synth = require('../index.js')({ 542 | localModules: __dirname + '/project_modules', 543 | root: module 544 | }); 545 | 546 | synth.define({ 547 | 'testDifferentConstruct': { 548 | extend: 'testModule', 549 | construct: function(self, options) { 550 | self._definitionsLevelOptions = options; 551 | } 552 | } 553 | }); 554 | 555 | synth.createAll({ }, { }, function(err, modules) { 556 | assert(!err); 557 | assert(modules.testDifferentConstruct._options); 558 | assert(!modules.testDifferentConstruct._differentOptions); 559 | assert(modules.testDifferentConstruct._definitionsLevelOptions); 560 | return done(); 561 | }); 562 | }); 563 | 564 | it('should override the project-level `beforeConstruct` using a definitions-level `beforeConstruct`', function(done) { 565 | var synth = require('../index.js')({ 566 | localModules: __dirname + '/project_modules', 567 | root: module 568 | }); 569 | 570 | synth.define({ 571 | 'testDifferentConstruct': { 572 | extend: 'testModule', 573 | beforeConstruct: function(self, options) { 574 | self._bcDefinitionsLevelOptions = options; 575 | } 576 | } 577 | }); 578 | 579 | synth.createAll({ }, { }, function(err, modules) { 580 | assert(!err); 581 | assert(modules.testDifferentConstruct._bcOptions); 582 | assert(!modules.testDifferentConstruct._bcDifferentOptions); 583 | assert(modules.testDifferentConstruct._bcDefinitionsLevelOptions); 584 | return done(); 585 | }); 586 | }); 587 | 588 | // ================================================================= 589 | // ORDER OF OPERATIONS 590 | // ================================================================= 591 | 592 | it('should respect baseClass-first order-of-operations for `beforeConstruct` and `construct`', function(done) { 593 | var synth = require('../index.js')({ 594 | localModules: __dirname + '/project_modules', 595 | root: module 596 | }); 597 | 598 | synth.define({ 599 | 'testOrderOfOperations': { } 600 | }); 601 | 602 | synth.createAll({ }, { }, function(err, modules) { 603 | assert(!err); 604 | assert(modules.testOrderOfOperations._bcOrderOfOperations[0] === 'notlast'); 605 | assert(modules.testOrderOfOperations._bcOrderOfOperations[1] === 'last'); 606 | assert(modules.testOrderOfOperations._orderOfOperations[0] === 'first'); 607 | assert(modules.testOrderOfOperations._orderOfOperations[1] === 'second'); 608 | return done(); 609 | }); 610 | }); 611 | 612 | it('should respect baseClass-first order-of-operations for `beforeConstruct` and `construct` with subclassing', function(done) { 613 | var synth = require('../index.js')({ 614 | localModules: __dirname + '/project_modules', 615 | root: module 616 | }); 617 | 618 | synth.define({ 619 | 'subTestOrderOfOperations': { 620 | extend: 'testOrderOfOperations', 621 | beforeConstruct: function(self, options) { 622 | self._bcOrderOfOperations = (self._bcOrderOfOperations || []).concat('first'); 623 | }, 624 | construct: function(self, options) { 625 | self._orderOfOperations = (self._orderOfOperations || []).concat('third'); 626 | } 627 | } 628 | }); 629 | 630 | synth.createAll({ }, { }, function(err, modules) { 631 | assert(!err); 632 | assert(modules.subTestOrderOfOperations._bcOrderOfOperations[0] === 'first'); 633 | assert(modules.subTestOrderOfOperations._bcOrderOfOperations[1] === 'notlast'); 634 | assert(modules.subTestOrderOfOperations._bcOrderOfOperations[2] === 'last'); 635 | assert(modules.subTestOrderOfOperations._orderOfOperations[0] === 'first'); 636 | assert(modules.subTestOrderOfOperations._orderOfOperations[1] === 'second'); 637 | assert(modules.subTestOrderOfOperations._orderOfOperations[2] === 'third'); 638 | return done(); 639 | }); 640 | }); 641 | }); 642 | 643 | describe('bundles', function() { 644 | it('should expose two new modules via a bundle', function(done) { 645 | var synth = require('../index.js')({ 646 | localModules: __dirname + '/project_modules', 647 | root: module, 648 | bundles: ['testBundle'] 649 | }); 650 | 651 | synth.define({ 652 | 'bundleModuleOne': { }, 653 | 'bundleModuleTwo': { } 654 | }); 655 | 656 | synth.createAll({ }, { }, function(err, modules) { 657 | assert(!err); 658 | assert(modules.bundleModuleOne); 659 | assert(modules.bundleModuleOne._options.color === 'blue'); 660 | assert(modules.bundleModuleTwo); 661 | assert(modules.bundleModuleTwo._options.color === 'blue'); 662 | return done(); 663 | }); 664 | }); 665 | }); 666 | 667 | describe('metadata', function() { 668 | it('should expose correct dirname metadata for npm, project level, and explicitly defined classes in the chain', function(done) { 669 | var synth = require('../index.js')({ 670 | localModules: __dirname + '/project_modules', 671 | root: module 672 | }); 673 | 674 | synth.define('metadataExplicit', { 675 | color: 'red', 676 | extend: 'metadataProject' 677 | }); 678 | 679 | synth.create('metadataExplicit', { }, function(err, module) { 680 | if (err) { 681 | console.error(err); 682 | } 683 | assert(!err); 684 | assert(module); 685 | assert(module.__meta); 686 | assert(module.__meta.chain); 687 | assert(module.__meta.chain[0]); 688 | assert(module.__meta.chain[0].dirname === __dirname + '/node_modules/metadataNpm'); 689 | assert(module.__meta.chain[1]); 690 | assert(module.__meta.chain[1].dirname === __dirname + '/project_modules/metadataNpm'); 691 | assert(module.__meta.chain[2]); 692 | assert(module.__meta.chain[2].dirname === __dirname + '/project_modules/metadataProject'); 693 | assert(module.__meta.chain[3]); 694 | assert(module.__meta.chain[3].dirname === __dirname + '/project_modules/metadataExplicit'); 695 | return done(); 696 | }); 697 | }); 698 | }); 699 | 700 | describe('error handling', function() { 701 | it('should prevent cyclical module definitions', function(done) { 702 | var synth = require('../index.js')({ 703 | localModules: __dirname + '/project_modules', 704 | root: module 705 | }); 706 | 707 | synth.define({ 708 | 'myNewModuleOne': { 709 | extend: 'myNewModuleTwo' 710 | }, 711 | 'myNewModuleTwo': { 712 | extend: 'myNewModuleOne' 713 | } 714 | }); 715 | 716 | synth.createAll({ }, { }, function(err, modules) { 717 | assert(err); 718 | return done(); 719 | }); 720 | }); 721 | }); 722 | 723 | describe('replace option', function() { 724 | it('should substitute a replacement type when replace option is used', function() { 725 | var synth = require('../index.js')({ 726 | localModules: __dirname + '/project_modules', 727 | root: module 728 | }); 729 | synth.define('replaceTestOriginal'); 730 | synth.define('replaceTestReplacement'); 731 | var instance = synth.create('replaceTestOriginal', {}); 732 | assert(instance._options); 733 | assert(!instance._options.color); 734 | assert(instance._options.size === 'large'); 735 | }); 736 | }); 737 | 738 | describe('improve option', function() { 739 | it('should substitute an implicit subclass when improve option is used', function() { 740 | var synth = require('../index.js')({ 741 | localModules: __dirname + '/project_modules', 742 | root: module 743 | }); 744 | synth.define('improveTestOriginal'); 745 | synth.define('improveTestReplacement'); 746 | var instance = synth.create('improveTestOriginal', {}); 747 | assert(instance._options); 748 | assert(instance._options.color === 'red'); 749 | assert(instance._options.size === 'large'); 750 | }); 751 | it('should require the original for you if needed', function() { 752 | var synth = require('../index.js')({ 753 | localModules: __dirname + '/project_modules', 754 | root: module 755 | }); 756 | synth.define('improveTestReplacement'); 757 | var instance = synth.create('improveTestOriginal', {}); 758 | assert(instance._options); 759 | assert(instance._options.color === 'red'); 760 | assert(instance._options.size === 'large'); 761 | }); 762 | }); 763 | 764 | describe('nestedModuleSubdirs option', function() { 765 | it('should load a module from a regular folder without the nesting feature enabled', function() { 766 | var synth = require('../index.js')({ 767 | localModules: __dirname + '/project_modules', 768 | root: module 769 | }); 770 | synth.define('testModuleSimple'); 771 | var instance = synth.create('testModuleSimple', {}); 772 | assert(instance._options); 773 | assert(instance._options.color === 'red'); 774 | }); 775 | it('should load a module from a nested or non-nested folder with the nesting option enabled', function() { 776 | var synth = require('../index.js')({ 777 | localModules: __dirname + '/project_modules', 778 | nestedModuleSubdirs: true, 779 | root: module 780 | }); 781 | synth.define('testModuleSimple'); 782 | var instance = synth.create('testModuleSimple', {}); 783 | assert(instance._options); 784 | assert(instance._options.color === 'red'); 785 | synth.define('nestedModule'); 786 | var instance = synth.create('nestedModule', {}); 787 | assert(instance._options); 788 | assert(instance._options.color === 'green'); 789 | }); 790 | }); 791 | it('should load a project level module properly when a transitive dependency not in package.json nevertheless has the same name and appears in node_modules', function() { 792 | var synth = require('../index.js')({ 793 | localModules: __dirname + '/project_modules', 794 | root: module 795 | }); 796 | synth.define('sameNameAsTransitiveDependency'); 797 | var instance = synth.create('sameNameAsTransitiveDependency', {}); 798 | assert(instance.confirm === 'loaded'); 799 | }); 800 | }); 801 | -------------------------------------------------------------------------------- /test/nested/test.js: -------------------------------------------------------------------------------- 1 | // Same darn tests but loading package.json via searching up the tree, 2 | // to verify that feature 3 | 4 | const assert = require('assert'); 5 | const path = require('path'); 6 | 7 | // console.log = function(s) { 8 | // console.trace(s); 9 | // }; 10 | 11 | describe('moog', function() { 12 | 13 | describe('synth', function() { 14 | it('exists', function() { 15 | assert( require('../../index.js') ); 16 | }); 17 | 18 | var synth = require('../../index.js')({ 19 | localModules: __dirname + '/../project_modules', 20 | root: module 21 | }); 22 | 23 | it('has a `create` method', function() { 24 | assert(synth.create); 25 | }); 26 | it('has a `createAll` method', function() { 27 | assert(synth.createAll); 28 | }); 29 | it('has a `bridge` method', function() { 30 | assert(synth.bridge); 31 | }); 32 | }); 33 | 34 | describe('synth.create', function() { 35 | var synth; 36 | 37 | it('should create a subclass with no options', function(done) { 38 | synth = require('../../index.js')({ 39 | localModules: __dirname + '/../project_modules', 40 | root: module 41 | }); 42 | 43 | synth.define({ 44 | 'testModule': { } 45 | }); 46 | 47 | synth.create('testModule', {}, function(err, testModule) { 48 | assert(!err); 49 | assert(testModule); 50 | assert(testModule._options.color === 'blue'); 51 | return done(); 52 | }); 53 | }); 54 | 55 | it('should create a subclass with overrides of default options', function(done) { 56 | synth = require('../../index.js')({ 57 | localModules: __dirname + '/../project_modules', 58 | root: module 59 | }); 60 | 61 | synth.define({ 62 | 'testModule': { 63 | color: 'red' 64 | } 65 | }); 66 | 67 | synth.create('testModule', {}, function(err, testModule) { 68 | assert(!err); 69 | assert(testModule._options.color === 'red'); 70 | return done(); 71 | }); 72 | }); 73 | 74 | it('should create a subclass with overrides of default options in localModules folder and npm', function(done) { 75 | synth = require('../../index.js')({ 76 | localModules: __dirname + '/../project_modules', 77 | root: module 78 | }); 79 | 80 | synth.define({ 81 | // testModuleTwo is defined in ./../project_modules and 82 | // ./node_modules 83 | 'testModuleTwo': { } 84 | } 85 | ); 86 | 87 | synth.create('testModuleTwo', {}, function(err, testModuleTwo) { 88 | assert(!err); 89 | assert(testModuleTwo._options.color === 'red'); 90 | return done(); 91 | }); 92 | }); 93 | 94 | it('should create a subclass with overrides of default options at runtime', function(done) { 95 | synth = require('../../index.js')({ 96 | localModules: __dirname + '/../project_modules', 97 | root: module 98 | }); 99 | 100 | synth.define({ 101 | 'testModule': { } 102 | }); 103 | 104 | synth.create('testModule', { color: 'purple' }, function(err, testModule) { 105 | assert(!err); 106 | assert(testModule._options.color === 'purple'); 107 | return done(); 108 | }); 109 | }); 110 | 111 | it('should create a subclass with a new name using the `extend` property', function(done) { 112 | synth = require('../../index.js')({ 113 | localModules: __dirname + '/../project_modules', 114 | root: module 115 | }); 116 | 117 | synth.define({ 118 | 'myTestModuleExtend': { 119 | extend: 'testModule', 120 | color: 'red' 121 | } 122 | }); 123 | 124 | synth.create('myTestModuleExtend', {}, function(err, myTestModule) { 125 | if (err) { 126 | console.error(err); 127 | } 128 | assert(!err); 129 | assert(myTestModule); 130 | assert(myTestModule._options.color === 'red'); 131 | return done(); 132 | }); 133 | }); 134 | 135 | it('should create a subclass with a new name by extending a module defined in localModules', function(done) { 136 | synth = require('../../index.js')({ 137 | localModules: __dirname + '/../project_modules', 138 | root: module 139 | }); 140 | 141 | synth.define({ 142 | 'myTestModule': { 143 | extend: 'testModuleLocalOnly', 144 | newProperty: 42 145 | } 146 | }); 147 | 148 | synth.create('myTestModule', {}, function(err, myTestModule) { 149 | assert(!err); 150 | assert(myTestModule); 151 | assert(myTestModule._options.color === 'purple'); 152 | assert(myTestModule._options.newProperty === 42); 153 | return done(); 154 | }); 155 | }); 156 | 157 | it('should create a subclass of a subclass', function(done) { 158 | synth = require('../../index.js')({ 159 | localModules: __dirname + '/../project_modules', 160 | root: module 161 | }); 162 | 163 | synth.define({ 164 | 'myTestModule': { 165 | extend: 'testModule' 166 | }, 167 | 'mySubTestModule': { 168 | extend: 'myTestModule', 169 | color: 'orange' 170 | } 171 | }); 172 | 173 | synth.create('myTestModule', {}, function(err, myTestModule) { 174 | assert(!err); 175 | assert(myTestModule); 176 | assert(myTestModule._options.color === 'blue'); 177 | synth.create('mySubTestModule', {}, function(err, mySubTestModule) { 178 | assert(!err); 179 | assert(mySubTestModule); 180 | assert(mySubTestModule._options.color === 'orange'); 181 | return done(); 182 | }); 183 | }); 184 | }); 185 | 186 | it('should create a subclass when both parent and subclass are in npm', function(done) { 187 | synth = require('../../index.js')({ 188 | localModules: __dirname + '/../project_modules', 189 | root: module 190 | }); 191 | 192 | synth.define({ 193 | 'testModuleThree': {} 194 | }); 195 | 196 | synth.create('testModuleThree', {}, function(err, testModuleThree) { 197 | if (err) { 198 | console.error(err); 199 | } 200 | assert(!err); 201 | assert(testModuleThree); 202 | assert(testModuleThree._options.age === 30); 203 | return done(); 204 | }); 205 | }); 206 | 207 | }); 208 | 209 | 210 | describe('synth.createAll', function() { 211 | var synth; 212 | 213 | it('should create two subclasses', function(done) { 214 | synth = require('../../index.js')({ 215 | localModules: __dirname + '/../project_modules', 216 | root: module 217 | }); 218 | 219 | synth.define({ 220 | 'testModule': { }, 221 | 'testModuleTwo': { } 222 | }); 223 | 224 | synth.createAll({}, {}, function(err, modules) { 225 | assert(!err); 226 | assert(modules.testModule); 227 | assert(modules.testModuleTwo); 228 | return done(); 229 | }); 230 | }); 231 | 232 | it('should create two subclasses with runtime options passed using `specific` options', function(done) { 233 | synth = require('../../index.js')({ 234 | localModules: __dirname + '/../project_modules', 235 | root: module 236 | }); 237 | 238 | synth.define({ 239 | 'testModule': { }, 240 | 'testModuleTwo': { } 241 | }); 242 | 243 | synth.createAll({}, { 244 | testModule: { color: 'green' }, 245 | testModuleTwo: { color: 'green' } 246 | }, function(err, modules) { 247 | assert(!err); 248 | assert(modules.testModule); 249 | assert(modules.testModule._options.color === 'green'); 250 | assert(modules.testModuleTwo); 251 | assert(modules.testModuleTwo._options.color === 'green'); 252 | return done(); 253 | }); 254 | }); 255 | 256 | it('should create two subclasses with runtime options passed using `global` options', function(done) { 257 | synth = require('../../index.js')({ 258 | localModules: __dirname + '/../project_modules', 259 | root: module 260 | }); 261 | 262 | synth.define({ 263 | 'testModule': { }, 264 | 'testModuleTwo': { } 265 | }); 266 | 267 | synth.createAll({ color: 'green' }, { }, function(err, modules) { 268 | assert(!err); 269 | assert(modules.testModule); 270 | assert(modules.testModule._options.color === 'green'); 271 | assert(modules.testModuleTwo); 272 | assert(modules.testModuleTwo._options.color === 'green'); 273 | return done(); 274 | }); 275 | }); 276 | }); 277 | 278 | describe('synth.bridge', function() { 279 | it('should run successfully', function(done) { 280 | var synth = require('../../index.js')({ 281 | localModules: __dirname + '/../project_modules', 282 | root: module 283 | }); 284 | 285 | synth.define({ 286 | 'testModule': { }, 287 | 'testModuleTwo': { } 288 | }); 289 | 290 | synth.createAll({ }, { }, function(err, modules) { 291 | synth.bridge(modules); 292 | assert(!err); 293 | assert(modules.testModule); 294 | assert(modules.testModuleTwo); 295 | return done(); 296 | }); 297 | }); 298 | 299 | it('should pass modules to each other', function(done) { 300 | var synth = require('../../index.js')({ 301 | localModules: __dirname + '/../project_modules', 302 | root: module 303 | }); 304 | 305 | synth.define({ 306 | 'testModule': { 307 | construct: function(self, options) { 308 | self.setBridge = function(modules) { 309 | self.otherModule = modules.testModuleTwo; 310 | }; 311 | } 312 | }, 313 | 'testModuleTwo': { 314 | construct: function(self, options) { 315 | self.setBridge = function(modules) { 316 | self.otherModule = modules.testModule; 317 | }; 318 | } 319 | } 320 | }); 321 | 322 | synth.createAll({ }, { }, function(err, modules) { 323 | assert(!err); 324 | synth.bridge(modules); 325 | assert(modules.testModule.otherModule); 326 | assert(modules.testModuleTwo.otherModule); 327 | return done(); 328 | }); 329 | }); 330 | }); 331 | 332 | describe('module structure', function() { 333 | 334 | it('should accept a `defaultBaseClass` that is inherited by empty definitions', function(done) { 335 | var synth = require('../../index.js')({ 336 | localModules: __dirname + '/../project_modules', 337 | defaultBaseClass: 'testModule', 338 | root: module 339 | }); 340 | 341 | synth.define({ 342 | 'newModule': { } 343 | }); 344 | 345 | synth.createAll({ }, { }, function(err, modules) { 346 | assert(!err); 347 | assert(modules.newModule); 348 | assert(modules.newModule._options.color === 'blue'); 349 | return done(); 350 | }); 351 | }); 352 | 353 | // ================================================================= 354 | // PASSING 355 | // ================================================================= 356 | 357 | it('should accept a synchronous `construct` method', function(done) { 358 | var synth = require('../../index.js')({ 359 | localModules: __dirname + '/../project_modules', 360 | root: module 361 | }); 362 | 363 | synth.define({ 364 | 'testModule': { } 365 | }); 366 | 367 | synth.createAll({ }, { }, function(err, modules) { 368 | assert(!err); 369 | assert(modules.testModule); 370 | return done(); 371 | }); 372 | }); 373 | 374 | it('should accept an asynchronous `construct` method', function(done) { 375 | var synth = require('../../index.js')({ 376 | localModules: __dirname + '/../project_modules', 377 | root: module 378 | }); 379 | 380 | synth.define({ 381 | 'testModuleTwo': { } 382 | }); 383 | 384 | synth.createAll({ }, { }, function(err, modules) { 385 | assert(!err); 386 | assert(modules.testModuleTwo); 387 | return done(); 388 | }); 389 | }); 390 | 391 | it('should accept a synchronous `beforeConstruct` method', function(done) { 392 | var synth = require('../../index.js')({ 393 | localModules: __dirname + '/../project_modules', 394 | root: module 395 | }); 396 | 397 | synth.define({ 398 | 'testModule': { } 399 | }); 400 | 401 | synth.createAll({ }, { }, function(err, modules) { 402 | assert(!err); 403 | assert(modules.testModule); 404 | return done(); 405 | }); 406 | }); 407 | 408 | it('should accept an asynchronous `beforeConstruct` method', function(done) { 409 | var synth = require('../../index.js')({ 410 | localModules: __dirname + '/../project_modules', 411 | root: module 412 | }); 413 | 414 | synth.define({ 415 | 'testBeforeConstructAsync': { } 416 | }); 417 | 418 | synth.createAll({ }, { }, function(err, modules) { 419 | assert(!err); 420 | assert(modules.testBeforeConstructAsync); 421 | return done(); 422 | }); 423 | }); 424 | 425 | // ================================================================= 426 | // FAILING 427 | // ================================================================= 428 | 429 | it('should catch a synchronous Error during `construct`', function(done) { 430 | var synth = require('../../index.js')({ 431 | localModules: __dirname + '/../project_modules', 432 | root: module 433 | }); 434 | 435 | synth.define({ 436 | 'failingModuleSync': { } 437 | }); 438 | 439 | synth.createAll({ }, { }, function(err, modules) { 440 | assert(err); 441 | assert(err.message === 'I have failed.'); 442 | return done(); 443 | }); 444 | }); 445 | 446 | it('should catch an asynchronous Error during `construct`', function(done) { 447 | var synth = require('../../index.js')({ 448 | localModules: __dirname + '/../project_modules', 449 | root: module 450 | }); 451 | 452 | synth.define({ 453 | 'failingModuleAsync': { } 454 | }); 455 | 456 | synth.createAll({ }, { }, function(err, modules) { 457 | assert(err); 458 | assert(err.message === 'I have failed.'); 459 | return done(); 460 | }); 461 | }); 462 | 463 | it('should catch a synchronous Error during `beforeConstruct`', function(done) { 464 | var synth = require('../../index.js')({ 465 | localModules: __dirname + '/../project_modules', 466 | root: module 467 | }); 468 | 469 | synth.define({ 470 | 'failingBeforeConstructSync': { } 471 | }); 472 | 473 | synth.createAll({ }, { }, function(err, modules) { 474 | assert(err); 475 | assert(err.message === 'I have failed.'); 476 | return done(); 477 | }); 478 | }); 479 | 480 | it('should catch an asynchronous Error during `beforeConstruct`', function(done) { 481 | var synth = require('../../index.js')({ 482 | localModules: __dirname + '/../project_modules', 483 | root: module 484 | }); 485 | 486 | synth.define({ 487 | 'failingBeforeConstructAsync': { } 488 | }); 489 | 490 | synth.createAll({ }, { }, function(err, modules) { 491 | assert(err); 492 | assert(err.message === 'I have failed.'); 493 | return done(); 494 | }); 495 | }); 496 | }); 497 | 498 | describe('order of operations', function() { 499 | 500 | // ================================================================= 501 | // MULTIPLE `construct`s AND `beforeConstruct`s 502 | // ================================================================= 503 | 504 | it('should call both the project-level `construct` and the npm module\'s `construct`', function(done) { 505 | var synth = require('../../index.js')({ 506 | localModules: __dirname + '/../project_modules', 507 | root: module 508 | }); 509 | 510 | synth.define({ 511 | 'testDifferentConstruct': { 512 | extend: 'testModule' 513 | } 514 | }); 515 | 516 | synth.createAll({ }, { }, function(err, modules) { 517 | assert(!err); 518 | assert(modules.testDifferentConstruct._options); 519 | assert(modules.testDifferentConstruct._differentOptions); 520 | return done(); 521 | }); 522 | }); 523 | 524 | it('should call both the project-level `beforeConstruct` and the npm module\'s `beforeConstruct`', function(done) { 525 | var synth = require('../../index.js')({ 526 | localModules: __dirname + '/../project_modules', 527 | root: module 528 | }); 529 | 530 | synth.define({ 531 | 'testDifferentConstruct': { 532 | extend: 'testModule' 533 | } 534 | }); 535 | 536 | synth.createAll({ }, { }, function(err, modules) { 537 | assert(!err); 538 | assert(modules.testDifferentConstruct._bcOptions); 539 | assert(modules.testDifferentConstruct._bcDifferentOptions); 540 | return done(); 541 | }); 542 | }); 543 | 544 | it('should override the project-level `construct` using a definitions-level `construct`', function(done) { 545 | var synth = require('../../index.js')({ 546 | localModules: __dirname + '/../project_modules', 547 | root: module 548 | }); 549 | 550 | synth.define({ 551 | 'testDifferentConstruct': { 552 | extend: 'testModule', 553 | construct: function(self, options) { 554 | self._definitionsLevelOptions = options; 555 | } 556 | } 557 | }); 558 | 559 | synth.createAll({ }, { }, function(err, modules) { 560 | assert(!err); 561 | assert(modules.testDifferentConstruct._options); 562 | assert(!modules.testDifferentConstruct._differentOptions); 563 | assert(modules.testDifferentConstruct._definitionsLevelOptions); 564 | return done(); 565 | }); 566 | }); 567 | 568 | it('should override the project-level `beforeConstruct` using a definitions-level `beforeConstruct`', function(done) { 569 | var synth = require('../../index.js')({ 570 | localModules: __dirname + '/../project_modules', 571 | root: module 572 | }); 573 | 574 | synth.define({ 575 | 'testDifferentConstruct': { 576 | extend: 'testModule', 577 | beforeConstruct: function(self, options) { 578 | self._bcDefinitionsLevelOptions = options; 579 | } 580 | } 581 | }); 582 | 583 | synth.createAll({ }, { }, function(err, modules) { 584 | assert(!err); 585 | assert(modules.testDifferentConstruct._bcOptions); 586 | assert(!modules.testDifferentConstruct._bcDifferentOptions); 587 | assert(modules.testDifferentConstruct._bcDefinitionsLevelOptions); 588 | return done(); 589 | }); 590 | }); 591 | 592 | // ================================================================= 593 | // ORDER OF OPERATIONS 594 | // ================================================================= 595 | 596 | it('should respect baseClass-first order-of-operations for `beforeConstruct` and `construct`', function(done) { 597 | var synth = require('../../index.js')({ 598 | localModules: __dirname + '/../project_modules', 599 | root: module 600 | }); 601 | 602 | synth.define({ 603 | 'testOrderOfOperations': { } 604 | }); 605 | 606 | synth.createAll({ }, { }, function(err, modules) { 607 | assert(!err); 608 | assert(modules.testOrderOfOperations._bcOrderOfOperations[0] === 'notlast'); 609 | assert(modules.testOrderOfOperations._bcOrderOfOperations[1] === 'last'); 610 | assert(modules.testOrderOfOperations._orderOfOperations[0] === 'first'); 611 | assert(modules.testOrderOfOperations._orderOfOperations[1] === 'second'); 612 | return done(); 613 | }); 614 | }); 615 | 616 | it('should respect baseClass-first order-of-operations for `beforeConstruct` and `construct` with subclassing', function(done) { 617 | var synth = require('../../index.js')({ 618 | localModules: __dirname + '/../project_modules', 619 | root: module 620 | }); 621 | 622 | synth.define({ 623 | 'subTestOrderOfOperations': { 624 | extend: 'testOrderOfOperations', 625 | beforeConstruct: function(self, options) { 626 | self._bcOrderOfOperations = (self._bcOrderOfOperations || []).concat('first'); 627 | }, 628 | construct: function(self, options) { 629 | self._orderOfOperations = (self._orderOfOperations || []).concat('third'); 630 | } 631 | } 632 | }); 633 | 634 | synth.createAll({ }, { }, function(err, modules) { 635 | assert(!err); 636 | assert(modules.subTestOrderOfOperations._bcOrderOfOperations[0] === 'first'); 637 | assert(modules.subTestOrderOfOperations._bcOrderOfOperations[1] === 'notlast'); 638 | assert(modules.subTestOrderOfOperations._bcOrderOfOperations[2] === 'last'); 639 | assert(modules.subTestOrderOfOperations._orderOfOperations[0] === 'first'); 640 | assert(modules.subTestOrderOfOperations._orderOfOperations[1] === 'second'); 641 | assert(modules.subTestOrderOfOperations._orderOfOperations[2] === 'third'); 642 | return done(); 643 | }); 644 | }); 645 | }); 646 | 647 | describe('bundles', function() { 648 | it('should expose two new modules via a bundle', function(done) { 649 | var synth = require('../../index.js')({ 650 | localModules: __dirname + '/../project_modules', 651 | root: module, 652 | bundles: ['testBundle'] 653 | }); 654 | 655 | synth.define({ 656 | 'bundleModuleOne': { }, 657 | 'bundleModuleTwo': { } 658 | }); 659 | 660 | synth.createAll({ }, { }, function(err, modules) { 661 | assert(!err); 662 | assert(modules.bundleModuleOne); 663 | assert(modules.bundleModuleOne._options.color === 'blue'); 664 | assert(modules.bundleModuleTwo); 665 | assert(modules.bundleModuleTwo._options.color === 'blue'); 666 | return done(); 667 | }); 668 | }); 669 | }); 670 | 671 | describe('metadata', function() { 672 | it('should expose correct dirname metadata for npm, project level, and explicitly defined classes in the chain', function(done) { 673 | var synth = require('../../index.js')({ 674 | localModules: __dirname + '/../project_modules', 675 | root: module 676 | }); 677 | 678 | synth.define('metadataExplicit', { 679 | color: 'red', 680 | extend: 'metadataProject' 681 | }); 682 | 683 | synth.create('metadataExplicit', { }, function(err, module) { 684 | if (err) { 685 | console.error(err); 686 | } 687 | assert(!err); 688 | assert(module); 689 | assert(module.__meta); 690 | assert(module.__meta.chain); 691 | assert(module.__meta.chain[0]); 692 | assert(module.__meta.chain[0].dirname === path.dirname(__dirname) + '/node_modules/metadataNpm'); 693 | assert(module.__meta.chain[1]); 694 | assert(module.__meta.chain[1].dirname === __dirname + '/../project_modules/metadataNpm'); 695 | assert(module.__meta.chain[2]); 696 | assert(module.__meta.chain[2].dirname === __dirname + '/../project_modules/metadataProject'); 697 | assert(module.__meta.chain[3]); 698 | assert(module.__meta.chain[3].dirname === __dirname + '/../project_modules/metadataExplicit'); 699 | return done(); 700 | }); 701 | }); 702 | }); 703 | 704 | describe('error handling', function() { 705 | it('should prevent cyclical module definitions', function(done) { 706 | var synth = require('../../index.js')({ 707 | localModules: __dirname + '/../project_modules', 708 | root: module 709 | }); 710 | 711 | synth.define({ 712 | 'myNewModuleOne': { 713 | extend: 'myNewModuleTwo' 714 | }, 715 | 'myNewModuleTwo': { 716 | extend: 'myNewModuleOne' 717 | } 718 | }); 719 | 720 | synth.createAll({ }, { }, function(err, modules) { 721 | assert(err); 722 | return done(); 723 | }); 724 | }); 725 | }); 726 | 727 | describe('replace option', function() { 728 | it('should substitute a replacement type when replace option is used', function() { 729 | var synth = require('../../index.js')({ 730 | localModules: __dirname + '/../project_modules', 731 | root: module 732 | }); 733 | synth.define('replaceTestOriginal'); 734 | synth.define('replaceTestReplacement'); 735 | var instance = synth.create('replaceTestOriginal', {}); 736 | assert(instance._options); 737 | assert(!instance._options.color); 738 | assert(instance._options.size === 'large'); 739 | }); 740 | }); 741 | 742 | describe('improve option', function() { 743 | it('should substitute an implicit subclass when improve option is used', function() { 744 | var synth = require('../../index.js')({ 745 | localModules: __dirname + '/../project_modules', 746 | root: module 747 | }); 748 | synth.define('improveTestOriginal'); 749 | synth.define('improveTestReplacement'); 750 | var instance = synth.create('improveTestOriginal', {}); 751 | assert(instance._options); 752 | assert(instance._options.color === 'red'); 753 | assert(instance._options.size === 'large'); 754 | }); 755 | it('should require the original for you if needed', function() { 756 | var synth = require('../../index.js')({ 757 | localModules: __dirname + '/../project_modules', 758 | root: module 759 | }); 760 | synth.define('improveTestReplacement'); 761 | var instance = synth.create('improveTestOriginal', {}); 762 | assert(instance._options); 763 | assert(instance._options.color === 'red'); 764 | assert(instance._options.size === 'large'); 765 | }); 766 | }); 767 | 768 | describe('nestedModuleSubdirs option', function() { 769 | it('should load a module from a regular folder without the nesting feature enabled', function() { 770 | var synth = require('../../index.js')({ 771 | localModules: __dirname + '/../project_modules', 772 | root: module 773 | }); 774 | synth.define('testModuleSimple'); 775 | var instance = synth.create('testModuleSimple', {}); 776 | assert(instance._options); 777 | assert(instance._options.color === 'red'); 778 | }); 779 | it('should load a module from a nested or non-nested folder with the nesting option enabled', function() { 780 | var synth = require('../../index.js')({ 781 | localModules: __dirname + '/../project_modules', 782 | nestedModuleSubdirs: true, 783 | root: module 784 | }); 785 | synth.define('testModuleSimple'); 786 | var instance = synth.create('testModuleSimple', {}); 787 | assert(instance._options); 788 | assert(instance._options.color === 'red'); 789 | synth.define('nestedModule'); 790 | var instance = synth.create('nestedModule', {}); 791 | assert(instance._options); 792 | assert(instance._options.color === 'green'); 793 | }); 794 | }); 795 | it('should load a project level module properly when a transitive dependency not in package.json nevertheless has the same name and appears in node_modules', function() { 796 | var synth = require('../../index.js')({ 797 | localModules: __dirname + '/../project_modules', 798 | root: module 799 | }); 800 | synth.define('sameNameAsTransitiveDependency'); 801 | var instance = synth.create('sameNameAsTransitiveDependency', {}); 802 | assert(instance.confirm === 'loaded'); 803 | }); 804 | }); 805 | --------------------------------------------------------------------------------