UNPKG

sails

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

632 lines (548 loc) 24.3 kB
module.exports = function(sails) { /** * Module dependencies */ var path = require('path'); var fs = require('fs'); var async = require('async'); var _ = require('@sailshq/lodash'); var includeAll = require('include-all'); var mergeDictionaries = require('merge-dictionaries'); var COMMON_JS_FILE_EXTENSIONS = require('common-js-file-extensions'); /** * Module constants */ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Supported file extensions for imperative code files such as hooks: // • 'js' (.js) // • 'ts' (.ts) // • 'es6' (.es6) // • ...etc. // // > For full list, see: // > https://github.com/luislobo/common-js-file-extensions/blob/210fd15d89690c7aaa35dba35478cb91c693dfa8/README.md#code-file-extensions // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var BASIC_SUPPORTED_FILE_EXTENSIONS = COMMON_JS_FILE_EXTENSIONS.code; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Supported file extensions, ONLY for configuration files: // • All of the normal supported extensions like 'js', plus // • 'json' (.json) // • 'json5' (.json5) // • 'json.ls' (.json.ls) // • ...etc. // // > For full list, see: // > https://github.com/luislobo/common-js-file-extensions/blob/210fd15d89690c7aaa35dba35478cb91c693dfa8/README.md#configobject-file-extensions // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG = COMMON_JS_FILE_EXTENSIONS.config.concat(BASIC_SUPPORTED_FILE_EXTENSIONS); /** * Module loader * * Load code files from a Sails app into memory; modules like controllers, * models, services, config, etc. */ return { defaults: function (config) { var localConfig = { // The path to the application appPath: config.appPath ? path.resolve(config.appPath) : process.cwd(), // Paths for application modules and key files // If `paths.app` not specified, use process.cwd() // (the directory where this Sails process is being initiated from) paths: { // Configuration // // For `userconfig` hook config: path.resolve(config.appPath, 'config'), // Server-Side Code // // For `controllers` hook controllers: path.resolve(config.appPath, 'api/controllers'), // For `policies` hook policies: path.resolve(config.appPath, 'api/policies'), // For `services` hook services: path.resolve(config.appPath, 'api/services'), // For `orm` hook adapters: path.resolve(config.appPath, 'api/adapters'), models: path.resolve(config.appPath, 'api/models'), // For `userhooks` hook hooks: path.resolve(config.appPath, 'api/hooks'), // For `blueprints` hook blueprints: path.resolve(config.appPath, 'api/blueprints'), // For `responses` hook responses: path.resolve(config.appPath, 'api/responses'), // For `helpers` hook helpers: path.resolve(config.appPath, 'api/helpers'), // Server-Side View templates // // For `views` hook views: path.resolve(config.appPath, 'views'), layout: path.resolve(config.appPath, 'views/layout.ejs') } }; return localConfig; }, initialize: function(cb) { // Expose self as `sails.modules`. sails.modules = sails.hooks.moduleloader; // | // |_Note that, in the future, the moduleloader's methods will be federated // | out to the places where they're being used, instead of relying on // | having those other modules call the appropriate method on `sails.modules.*()`. return cb(); }, configure: function() { // Default to process.cwd() sails.config.appPath = sails.config.appPath ? path.resolve(sails.config.appPath) : process.cwd(); _.extend(sails.config.paths, { // Configuration // // For `userconfig` hook config: path.resolve(sails.config.appPath, sails.config.paths.config), // Server-Side Code // // For `controllers` hook controllers: path.resolve(sails.config.appPath, sails.config.paths.controllers), // For `policies` hook policies: path.resolve(sails.config.appPath, sails.config.paths.policies), // For `services` hook services: path.resolve(sails.config.appPath, sails.config.paths.services), // For `orm` hook adapters: path.resolve(sails.config.appPath, sails.config.paths.adapters), models: path.resolve(sails.config.appPath, sails.config.paths.models), // For `userhooks` hook hooks: path.resolve(sails.config.appPath, sails.config.paths.hooks), // For `blueprints` hook blueprints: path.resolve(sails.config.appPath, sails.config.paths.blueprints), // For `responses` hook responses: path.resolve(sails.config.appPath, sails.config.paths.responses), // Server-Side HTML // // For `views` hook views: path.resolve(sails.config.appPath, sails.config.paths.views), layout: path.resolve(sails.config.appPath, sails.config.paths.layout) }); }, /** * Load user config from app * * @param {Object} options * @param {Function} cb */ loadUserConfig: function (cb) { async.auto({ 'config/*': function loadOtherConfigFiles (cb) { includeAll.aggregate({ dirname : sails.config.paths.config, exclude : ['locales', /local\..+/], excludeDirs: /(locales|env)$/, filter : new RegExp('^(.+)\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), flatten : true, keepDirectoryPath: true, identity : false }, cb); }, 'config/local' : function loadLocalOverrideFile (cb) { includeAll.aggregate({ dirname : sails.config.paths.config, filter : new RegExp('^local\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), identity : false }, cb); }, // Load environment-specific config folder, e.g. config/env/development/* 'config/env/**': ['config/local', function loadEnvConfigFolder (asyncData, cb) { // If there's an environment already set in sails.config, then it came from the environment // or the command line, so that takes precedence. Otherwise, check the config/local.js file // for an environment setting. Lastly, default to development. var env = sails.config.environment || asyncData['config/local'].environment || 'development'; includeAll.aggregate({ dirname : path.resolve( sails.config.paths.config, 'env', env ), filter : new RegExp('^(.+)\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), optional : true, flatten : true, keepDirectoryPath: true, identity : false }, cb); }], // Load environment-specific config file, e.g. config/env/development.js 'config/env/*' : ['config/local', function loadEnvConfigFile (asyncData, cb) { // If there's an environment already set in sails.config, then it came from the environment // or the command line, so that takes precedence. Otherwise, check the config/local.js file // for an environment setting. Lastly, default to development. var env = sails.config.environment || asyncData['config/local'].environment || 'development'; includeAll.aggregate({ dirname : path.resolve( sails.config.paths.config, 'env' ), filter : new RegExp('^' + _.escapeRegExp(env) + '\\.(' + SUPPORTED_FILE_EXTENSIONS_FOR_CONFIG.join('|') + ')$'), optional : true, flatten : true, keepDirectoryPath: true, identity : false }, cb); }] }, function (err, asyncData) { if (err) { return cb(err); } // Save the environment override, if any. var env = sails.config.environment; // Merge the configs, with env/*.js files taking precedence over others, and local.js // taking precedence over everything. var config = mergeDictionaries( asyncData['config/*'], asyncData['config/env/**'], asyncData['config/env/*'], asyncData['config/local'] ); // Set the environment, but don't allow env/* files to change it; that'd be weird. config.environment = env || asyncData['config/local'].environment || 'development'; // Return the user config cb(undefined, config); }); }, /** * Load adapters * * @param {Object} options * @param {Function} cb */ loadAdapters: function (cb) { // Load things like `api/adapters/FooAdapter.js` includeAll.optional({ dirname: sails.config.paths.adapters, filter: /^(.+Adapter)\..+$/, replaceExpr: /Adapter/, flatten: true, depth: 1 }, function(err, classicStyleAdapters) { if (err) { return cb(err); } // Load things like `api/adapters/foo/index.js` fs.readdir(sails.config.paths.adapters, function(err, contents) { if (err) { if (err.code === 'ENOENT') { return cb(undefined, classicStyleAdapters); } return cb(err); } var folderStyleAdapters = {}; try { _.each(contents, function(filename) { var absPath = path.join(sails.config.paths.adapters, filename); // Exclude things that aren't directories, and directories that start with dots. if (_.startsWith(filename, '.')) { return; } var stats = fs.statSync(absPath); if (!stats.isDirectory()) { return; } // But otherwise, if we see a directory in here, try to require it. // (this follows the rules of the package.json file if there is one-- // or otherwise uses index.js by convention) var adapterDef = require(absPath); // Use the name of the folder as the identity. folderStyleAdapters[filename] = adapterDef; }); //</_.each()> } catch (e) { return cb(e); } // Finally, send back the merged-together set of adapters. return cb(undefined, _.extend(classicStyleAdapters, folderStyleAdapters)); }); //</fs.readdir> }); //</includeall.optional> }, /** * Load app's model definitions * * @param {Object} options * @param {Function} cb */ loadModels: function (cb) { // Get the main model files includeAll.optional({ dirname : sails.config.paths.models, filter : /^(.+)\.(?:(?!md|txt).)+$/, replaceExpr : /^.*\//, flatten: true }, function(err, models) { if (err) { return cb(err); } // --------------------------------------------------------- // Get any supplemental files (BACKWARDS-COMPAT.) includeAll.optional({ dirname : sails.config.paths.models, filter : /(.+)\.attributes.json$/, replaceExpr : /^.*\//, flatten: true }, bindToSails(function(err, supplements) { if (err) { return cb(err); } if (_.keys(supplements).length > 0) { sails.log.debug('The use of `.attributes.json` files is deprecated, and support will be removed in a future release of Sails.'); } return cb(undefined, _.merge(models, supplements)); })); // --------------------------------------------------------- }); }, /** * Load app services * * @param {Object} options * @param {Function} cb */ loadServices: function (cb) { includeAll.optional({ dirname : sails.config.paths.services, filter : /^(.+)\.(?:(?!md|txt).)+$/, depth : 1, caseSensitive : true }, bindToSails(cb)); }, /** * Check for the existence of views in the app * * @param {Object} options * @param {Function} cb */ statViews: function (cb) { includeAll.optional({ dirname: sails.config.paths.views, filter: /^(.+)\.(?:(?!md|txt).)+$/, replaceExpr: null, dontLoad: true }, cb); }, /** * Load app policies * * @param {Object} options * @param {Function} cb */ loadPolicies: function (cb) { includeAll.optional({ dirname: sails.config.paths.policies, filter: /^(.+)\.(?:(?!md|txt).)+$/, replaceExpr: null, flatten: true, keepDirectoryPath: true }, bindToSails(cb)); }, /** * Load app hooks * * > Note that, while `sails.config.hooks` is respected here in this * > function, the `sails.config.loadHooks` setting in regards to * > user hooks is taken care of in the initialize() method of the * > userhooks hook itself. * * @param {Object} options * @param {Function} cb */ loadUserHooks: function (cb) { var defaultInstalledHooks = _.filter(_.values(require('../../app/configuration/default-hooks')), function(val) {return val !== true;}); // Get the current app's package.json file (defaulting to an empty dictionary) var appPackageJson; try { appPackageJson = require(path.resolve(sails.config.appPath, 'package.json')); } catch (unusedErr) { appPackageJson = {}; } async.auto({ // Load user hooks from the "api/hooks" folder hooksFolder: function(cb) { includeAll.optional({ dirname: sails.config.paths.hooks, filter: new RegExp('^(.+)\\.(' + BASIC_SUPPORTED_FILE_EXTENSIONS.join('|') + ')$'), // Hooks should be defined as either single files as a function // OR (better yet) a subfolder with an index.js file // (like a standard node module) depth: 2 }, cb); }, // Load package.json files from node_modules to check for hooks nodeModulesFolder: function(cb) { includeAll.optional({ dirname: path.resolve(sails.config.appPath, 'node_modules'), filter: /^(package\.json)$/, excludeDirs: /^\./, // Look inside namespaced folders e.g. node_modules/@sailsjs/sails-hook-foo depth: 3, // Don't actually load the files, since malformed ones would cause a crash. // Just keep track of where they are and we'll load them carefully below. dontLoad: true }, function(err, modules) { if (err) { return cb(err); } // Now that we have a map of where the package.json files are, flatten that // map and load the files carefully. Map might look something like: // { angular2: // { animate: {}, // bundles: { web_worker: undefined }, // es6: { dev: undefined, prod: undefined }, // examples: { router: undefined }, // http: {}, // 'package.json': true, // etc... modules = (function _flatten(modules, installedHooks, currentPath, level) { installedHooks = installedHooks || {}; currentPath = currentPath || ''; level = level || 0; // Loop through the keys in the current map object Object.keys(modules).forEach(function(identity) { // If it represents a package.json file, attempt to load it and, if // successful, save it in our set of found files. If unsuccessful, // just ignore it. if (identity === 'package.json' && modules[identity] === true) { var filePath = path.resolve(sails.config.appPath, 'node_modules', currentPath, identity); try { // Attempt to load the package.json file var packageJson = require(filePath); // If the module isn't declared as a Sails hook, ignore it. if (!packageJson.sails || !packageJson.sails.isHook) { return; } // If the module isn't saved in this app's package.json, ignore it. if (!_.get(appPackageJson, 'dependencies.' + packageJson.name) && !_.get(appPackageJson, 'devDependencies.' + packageJson.name) && !_.get(appPackageJson, 'optionalDependencies.' + packageJson.name)) { sails.log.debug('Ignoring hook `' + packageJson.name + '` because it isn\'t saved as any kind of dependency in your package.json file.'); sails.log.debug('(You could try installing it with `npm install ' + packageJson.name +' --save`. Or if you aren\'t using this hook,'); sails.log.debug('just remove it from the node_modules/ folder and this message will stop appearing.)'); sails.log.debug(); return; } // If it's one of our default hooks, ignore it so that it can be safely overridden. if (_.contains(defaultInstalledHooks, packageJson.name)) { return; } // Save a reference to this installed hook, which we'll use to require // the full module below. installedHooks[currentPath] = packageJson; } catch(e) { sails.log.verbose('While searching for installable hooks, found invalid package.json file at `'+filePath+'`. Details:',e.stack); return; } } // If the key represents an object, recursively search within it, but only if it's directly // under node_modules or under a node_modules/@something (namespaced) folder if (_.isObject(modules[identity]) && level === 0 || (level === 1 && currentPath[0] === '@')) { var nextPath; if (currentPath) { nextPath = path.join(currentPath,identity); } else { nextPath = identity; } _flatten(modules[identity], installedHooks, nextPath, level + 1 ); } });//</forEach() :: key in `modules`> // Return the dictionary of installed hooks we found. return installedHooks; })(modules);//</ invoked self-calling recursive function :: _flatten()> return cb(undefined, modules); });//</includeAll.optional() :: loading package.json files from the node_modules folder to check for hooks> } }, function(err, results) { if (err) {return cb(err);} // Marshall the hooks by checking that they are valid. The ones from the // api/hooks folder are assumed to be okay, as long as they aren't explicitly turned off. var hooks = _.reduce(results.hooksFolder, function(memo, module, identity) { if (sails.config.hooks[identity] !== false && sails.config.hooks[identity] !== 'false') { memo[identity] = module; } return memo; }, {}); try { // Loop through the package.json files of the hooks we found in the node_modules folder. _.extend(hooks, _.reduce(results.nodeModulesFolder, function(memo, modulePackageJson, identity) { // Any special config for this hook will be under the `sails` key in the package.json file. var hookConfig = modulePackageJson.sails; // Determine the name the hook should be added as var hookName; if (!_.isEmpty(hookConfig.hookName)) { hookName = hookConfig.hookName; } // If an identity was specified in sails.config.installedHooks, use that else if (sails.config.installedHooks && sails.config.installedHooks[identity] && sails.config.installedHooks[identity].name) { hookName = sails.config.installedHooks[identity].name; } // Otherwise use the module name, with namespacing and initial "sails-hook-" stripped off if it exists else { // Strip off any NPM namespacing and/or sails-hook- prefix hookName = identity.replace(/^(@.+?[\/\\])?(sails-hook-)?/, ''); } if (sails.config.hooks[hookName] === false || sails.config.hooks[hookName] === 'false') { return memo; } // Allow overriding core hooks if (sails.hooks[hookName]) { sails.log.verbose('Found hook: `'+hookName+'` in `node_modules/`. Overriding core hook w/ the same identity...'); } // If we have a hook in api/hooks with this name, throw an error if (hooks[hookName]) { var err = (function (){ var msg = 'Found hook: `' + hookName + '`, in `node_modules/`, but a hook with that identity already exists in `api/hooks/`. '+ 'The hook defined in your `api/hooks/` folder will take precedence.'; var err = new Error(msg); err.code = 'E_INVALID_HOOK_NAME'; return err; })(); sails.log.warn(err); return memo; } // Load the hook code var hook = require(path.resolve(sails.config.appPath, 'node_modules', identity)); // Set its config key (defaults to the hook name) hook.configKey = (sails.config.installedHooks && sails.config.installedHooks[identity] && sails.config.installedHooks[identity].configKey) || hookName; // Add this to the list of hooks to load memo[hookName] = hook; return memo; }, {}));//</_.reduce() + _.extend()> return bindToSails(cb)(null, hooks); } catch (e) { return cb(e); } });//</after async.auto> },//<loadUserHooks> /** * Load custom blueprint actions. * * @param {Object} options * @param {Function} cb */ loadBlueprints: function (cb) { includeAll.optional({ dirname: sails.config.paths.blueprints, filter: /^(.+)\.(?:(?!md|txt).)+$/, useGlobalIdForKeyName: true }, cb); }, /** * Load custom API responses. * * @param {Object} options * @param {Function} cb */ loadResponses: function (cb) { includeAll.optional({ dirname: sails.config.paths.responses, filter: /^(.+)\.(?:(?!md|txt).)+$/, useGlobalIdForKeyName: true }, bindToSails(cb)); }, optional: includeAll.optional, required: includeAll.required, aggregate: includeAll.aggregate, exists: includeAll.exists }; /** * Private helper function used above. * * @param {Function} cb [description] * @return {Function} * @param {Error?} err * @param {Dictionary} modules */ function bindToSails(cb) { return function(err, modules) { if (err) {return cb(err);} _.each(modules, function(moduleDef) { // Add a reference to the Sails app that loaded the module moduleDef.sails = sails; // Bind all methods to the module context _.bindAll(moduleDef); }); return cb(undefined, modules); }; }//</bindToSails definition (private helper function)> };