sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
464 lines (402 loc) • 18.6 kB
JavaScript
/**
* Module dependencies
*/
var fs = require('fs');
var path = require('path');
var _ = require('@sailshq/lodash');
var chalk = require('chalk');
var machine = require('machine');
var loadHelpers = require('./private/load-helpers');
var iterateHelpers = require('./private/iterate-helpers');
/**
* Helpers hook
*/
module.exports = function(sails) {
// Private variable used below to keep track of whether a compatibility
// warning message might need to be displayed.
var _prereleaseCompatWarning;
return {
defaults: {
helpers: {
// Custom usage/miscellaneous options:
usageOpts: {
arginStyle: 'serial',
execStyle: 'natural'
},
// Experimental: Programmatically provide a dictionary of helpers.
moduleDefinitions: undefined,
}
},
configure: function() {
// Check SVR of the `sails` dep in this app's package.json file.
//
// If it points at a 1.0 prerelease less than `1.0.0-44`, then log a warning
// explaining what's going on with helpers (we needed to make a breaking
// change in a prerelease), and that the old way can be achieved through
// configuration. Also mention that, to avoid breaking this app, we've
// applied the old configuration automatically, but that if the SVR in the
// package.json is changed, then this protection will disappear, and this
// app will potentially break.
//
// To resolve this, either:
// • (A) configure `sails.config.helpers.usageOpts` as `{arginStyle: 'named', execStyle: 'deferred'}`
// • or (B) change any code in this app that invokes helpers to take advantage of the new default style
// of usage, then upgrade to the latest version of Sails v1.0 and update
// your package.json file accordingly to make this warning go away.
if (sails.config.helpers.usageOpts.arginStyle !== 'named' || sails.config.helpers.usageOpts.execStyle !== 'deferred') {
var localSailsSVR = (function _gettingSVROfLocalSailsDep(){
var pjPath = path.resolve(sails.config.appPath, 'package.json');
var rawPjText;
try {
rawPjText = fs.readFileSync(pjPath, 'utf8');
} catch (unusedErr) {}
var appPj;
try {
appPj = JSON.parse(rawPjText);
} catch (unusedErr) {}
return appPj && appPj.dependencies && appPj.dependencies.sails;
})();//†
var prerelease = localSailsSVR && localSailsSVR.match(/1\.0\.0\-(.+)$/);
prerelease = prerelease && prerelease[1];
var isMinSVRPointedAtSensitiveV1Prerelease = prerelease && Number(prerelease) < 44;
if (isMinSVRPointedAtSensitiveV1Prerelease) {
sails.config.helpers.usageOpts.arginStyle = 'named';
sails.config.helpers.usageOpts.execStyle = 'deferred';
// Note that we don't actually log the warning right now-- we won't do that
// until a bit later, in .initialize(). Even then, we'll only actually log
// the warning if there are helpers defined in the app. (Because if there
// aren't any helpers, logging a warning would just be annoying!)
_prereleaseCompatWarning =
'---------------------------------------------------------------------\n'+
'Based on its package.json file, it looks like this app was built\n'+
'using the Sails beta, but with a version prior to v1.0.0-44.\n'+
'(This app depends on sails@'+localSailsSVR+'.)\n'+
'\n'+
'In the 1.0.0-44 prerelease of Sails, changes were introduced. By\n'+
'default, helpers now expect serial arguments instead of a dictionary\n'+
'of named parameters. In other words, you\'d now call:\n'+
' await sails.helpers.passwords.changePassword(\'abc123\')\n'+
'Instead of:\n'+
' await sails.helpers.passwords.changePassword({password:\'abc123\'})\n'+
'\n'+
'Additionally, it is no longer necessary to call .now() or .execSync()\n'+
'for synchronous helpers-- by default they are invoked automatically.\n'+
'(Not a fan? Sorry about the inconvenience! And don\'t worry, it\'s\n'+
'easy to change.)\n'+
'\n'+
'To avoid breaking this app, some special settings that make Sails\n'+
'backwards-compatible have been set automatically for you. But please\n'+
'be sure to take the steps below to resolve this as soon as possible.\n'+
'(What if you forgot about this and changed your package.json file?\n'+
'You might inadvertently remove this compatibility check... And if\n'+
'that were to happen, the next time you tried to lift your app, your\n'+
'helpers would no longer work!)\n'+
'\n'+
'To resolve this, use one of the following solutions:\n'+
'\n'+
' (A) <<<<Quick & dirty>>>>\n'+
' If you need a quick fix, or you just prefer to call helpers the old\n'+
' way, no problem: just nestle this in your .sailsrc file:\n'+
' "helpers": {\n'+
' "usageOpts": {\n'+
' "arginStyle": "named",\n'+
' "execStyle": "deferred"\n'+
' }\n'+
' }\n'+
' ^^That will make helpers behave exactly like they did before.\n'+
'\n'+
' (B) <<<<Recommended>>>>\n'+
' Change any relevant code in this app (e.g. `sails.helpers.x({…})`)\n'+
' to take advantage of serial usage, or chain on .with({…}). Then, \n'+
' update the `sails` dependency in your package.json file so that it\n'+
' satisfies ^1.0.0-44 or higher.\n'+
'\n'+
' Note: If you go with this approach, it\'s not all or nothing. You\n'+
' you can always use .with() to call a helper with named parameters\n'+
' on a one-off basis. For example:\n'+
' await sails.helpers.changePassword.with({password:\'abc123\'});\n'+
'\n'+
'\n'+
'(To hide this message, apply one of the solutions suggested above.)\n'+
'\n'+
' [?] If you\'re unsure, visit https://sailsjs.com/support\n'+
'---------------------------------------------------------------------\n';
}//fi
}//fi
// Define `sails.helpers` here so that it can potentially be used by other hooks.
// > NOTE: This is NOT `sails.config.helpers`-- this is `sails.helpers`!
// > (As for sails.config.helpers, it's set automatically based on our `defaults above)
sails.helpers = {};
Object.defineProperty(sails.helpers, Symbol.for('nodejs.util.inspect.custom'), {
enumerable: false,
configurable: false,
writable: true,
value: function inspect(){
// Tree diagram:
// ```
// .
// ├── …
// │ ├── …
// │ │ └── …
// │ ├── …
// │ └── …
// │
// ├── …
// │ ├── …
// │ │ └── …
// │ ├── …
// │ │ ├── …
// │ │ ├── …
// │ │ └── …
// │ ├── …
// │ └── …
// │
// ├── …
// │ └── …
// │
// ├── …
// ├── …
// └── …
// ```
var treeDiagram = (function(){
var OFFSET = ' ';
var TAB = ' ';
var SYMBOL_INITIAL = '. ';
var SYMBOL_NO_BRANCH = '│ ';
var SYMBOL_MID_BRANCH = '├── ';
var SYMBOL_LAST_BRANCH = '└── ';
var treeDiagram = '';
treeDiagram += OFFSET + SYMBOL_INITIAL + '\n';
iterateHelpers(
sails.helpers,
function _onBeforeStartingPack(pack, key, depth, isFirst, isLast, lastnessPerAncestor){
var indentation = _.reduce(lastnessPerAncestor, function(indentation, wasLast){
if (wasLast) {
indentation += TAB;
} else {
indentation += SYMBOL_NO_BRANCH;
}
return indentation;
}, '');
if (isLast) {
treeDiagram += OFFSET + indentation + SYMBOL_LAST_BRANCH + key + '\n';
} else {
treeDiagram += OFFSET + indentation + SYMBOL_MID_BRANCH + key + '\n';
}
},
undefined,// « no need for an _onAfterFinishingPack notifier here, so we omit it
function _onHelper(callable, methodName, depth, isFirst, isLast, lastnessPerAncestor){
var indentation = _.reduce(lastnessPerAncestor, function(indentation, wasLast){
if (wasLast) {
indentation += TAB;
} else {
indentation += SYMBOL_NO_BRANCH;
}
return indentation;
}, '');
if (isLast) {
treeDiagram += OFFSET + indentation + SYMBOL_LAST_BRANCH + (callable.toJSON()._fromLocalSailsApp ? chalk.bold.cyan(methodName) : chalk.italic(methodName)) + chalk.gray('()')+'\n';
if (depth === 2) {
treeDiagram += OFFSET + indentation + '\n';
}
} else {
treeDiagram += OFFSET + indentation + SYMBOL_MID_BRANCH + (callable.toJSON()._fromLocalSailsApp ? chalk.bold.cyan(methodName) : chalk.italic(methodName)) + chalk.gray('()')+'\n';
}
}
);
return treeDiagram;
})();//†
// Examples (asynchronous and synchronous)
var example1 = (function(){
var exampleArginPhrase = '';
if (sails.config.helpers.usageOpts.arginStyle === 'named') {
exampleArginPhrase = '{dir: \'./colorado/\'}';
} else if (sails.config.helpers.usageOpts.arginStyle === 'serial') {
exampleArginPhrase = '\'./colorado/\'';
}
return 'var contents = await sails.helpers.fs.ls('+exampleArginPhrase+');';
})();//†
var example2 = (function(){
var exampleArginPhrase = '';
if (sails.config.helpers.usageOpts.arginStyle === 'named') {
exampleArginPhrase = '{style: \'url-friendly\'}';
} else if (sails.config.helpers.usageOpts.arginStyle === 'serial') {
exampleArginPhrase = '\'url-friendly\'';
}
if (sails.config.helpers.usageOpts.execStyle === 'deferred') {
return 'var name = sails.helpers.strings.random('+exampleArginPhrase+').now();';
} else if (sails.config.helpers.usageOpts.execStyle === 'immediate' || sails.config.helpers.usageOpts.execStyle === 'natural') {
return 'var name = sails.helpers.strings.random('+exampleArginPhrase+');';
}
throw new Error('Consistency violation: Unrecognized arginStyle/execStyle in sails.config.helpers.usageOpts (This should never happen, since it should have already been validated and prevented from being built- please report at https://sailsjs.com/bugs)');
})();//†
return ''+
'-------------------------------------------------------\n'+
' sails.helpers\n'+
'\n'+
' Available methods:\n'+
treeDiagram+'\n'+
'\n'+
' Example usage:\n'+
' '+example1+'\n'+
' '+example2+'\n'+
'\n'+
' More info:\n'+
' https://sailsjs.com/support\n'+
'-------------------------------------------------------\n';
}//ƒ
});//…)
},
initialize: function(done) {
// Load helpers from the appropriate folder.
loadHelpers(sails, function(err) {
if (err) { return done(err); }
// If deemed relevant, log the prerelease compatibility warning that
// we built above. (Then clear it out, since we don't want to ever
// display it again during this "lift" cycle-- even if the experimental
// .reload() method is in use.)
if (_prereleaseCompatWarning && _.keys(sails.helpers).length > 0) {
sails.log.warn(_prereleaseCompatWarning);
_prereleaseCompatWarning = '';
}
return done();
});//_∏_
},
/**
* @experimental
* (This might change at any time, without a major version release!)
*/
furnishPack: function(slug, packInfo){
packInfo = packInfo || {};
slug = _.map(slug.split('.'), _.kebabCase).join('.');
var slugKeyPath = _.map(slug.split('.'), _.camelCase).join('.');
var chunks = slugKeyPath.split('.');
if (chunks.length > 1) {
sails.log.verbose(
'Watch out! Nesting helpers more than one sub-folder deep can be a liability. '+
'It also means that you\'ll need to type more every time you want to use '+
'your helper. Instead, try keeping your directory structure as flat as possible; '+
'i.e. in general, having more explicit filenames is better than having deep, '+
'complicated folder hierarchies.'
);
}
// If pack already exists, avast.
if (_.get(sails.helpers, slugKeyPath)) {
return;
}
// Ancestor packs:
var thisKeyPath;
var theseChunks;
var parentKeyPath;
var parentPackOrRoot;
for (var i = 0; i < chunks.length - 1; i++) {
theseChunks = chunks.slice(0,i+1);
thisKeyPath = theseChunks.join('.');
parentKeyPath = theseChunks.slice(0, -1).join('.');
if (!_.get(sails.helpers, thisKeyPath)) {
parentPackOrRoot = parentKeyPath ? _.get(sails.helpers, parentKeyPath) : sails.helpers;
parentPackOrRoot[chunks[i]] = machine.pack({
name: 'sails.helpers.'+chunks.slice(0,i+1).join('.'),
defs: {},
customize: _.extend({}, sails.config.helpers.usageOpts, {
implementationSniffingTactic: sails.config.implementationSniffingTactic||undefined
})
});
}
}//∞
// Main pack:
parentKeyPath = chunks.slice(0, -1).join('.');
parentPackOrRoot = parentKeyPath ? _.get(sails.helpers, parentKeyPath) : sails.helpers;
parentPackOrRoot[chunks[chunks.length - 1]] = machine.pack(_.extend({}, packInfo, {
name: 'sails.helpers.'+slugKeyPath,
customize: _.extend({}, sails.config.helpers.usageOpts, {
implementationSniffingTactic: sails.config.implementationSniffingTactic||undefined
})
}));
},
/**
* @experimental
* (This might change at any time, without a major version release!)
*/
furnishHelper: function(identityPlusMaybeSlug, nmDef){
// Ensure we're starting off with dot-delimited, kebab-cased hops.
identityPlusMaybeSlug = _.map(identityPlusMaybeSlug.split('.'), _.kebabCase).join('.');
var chunks = identityPlusMaybeSlug.split('.');
// slug ('foo-bar.baz-bing.beep.boop')
// identity ('do-something')
var slug = chunks.length >= 2 ? chunks.slice(0, -1).join('.') : undefined;
var identity = _.last(chunks);
// Camel-case every part of the file path, and join with dots
// e.g. admin-stuff.foo.do-something => adminStuff.foo.doSomething
var slugKeyPath = slug ? _.map(slug.split('.'), _.camelCase).join('.') : undefined;
var fullKeyPath = slug ? slugKeyPath + '.' + machine.getMethodName(identity) : machine.getMethodName(identity);
if (!_.get(sails.helpers, fullKeyPath)) {
// Work our way down
if (slug && !_.get(sails.helpers, slugKeyPath)) {
this.furnishPack(slug, {
name: 'sails.helpers.'+slugKeyPath,
defs: {}
});
}//fi
// And then build the helper last
// > (can't do it first! We'd confuse `_.get()`!)
// Use provided `identity` if no explicit identity was set.
// (Otherwise, as of machine@v15, this could fail with an ImplementationError.)
if (!nmDef.identity) {
nmDef.identity = identity;
}
// Attach new method to the appropriate pack.
// e.g. sails.helpers.userHelpers.foo.myHelper
if (slug) {
var parentPack = _.get(sails.helpers, slugKeyPath);
parentPack.registerDefs(
(function(){
var defs = {};
defs[identity] = nmDef;
return defs;
})()//†
);
} else {
sails.helpers[machine.getMethodName(identity)] = machine.buildWithCustomUsage(_.extend(
{},
sails.config.helpers.usageOpts,
{
def: nmDef,
implementationSniffingTactic: sails.config.implementationSniffingTactic
}
));
}
}//fi
},
/**
* sails.hooks.helpers.reload()
*
* @param {Dictionary?} helpers [if specified, these helpers will replace all existing helpers. Otherwise, if omitted, helpers will be freshly reloaded from disk, and old helpers will be thrown away.]
* @param {Function} done [optional callback]
*
* @experimental
* (This might change at any time, without a major version release!)
*/
reload: function(helpers, done) {
// Handle variadic usage
if (typeof helpers === 'function') {
done = helpers;
helpers = undefined;
}
// Handle optional callback
done = done || function _noopCb(err){
if (err) {
sails.log.error('Could not reload helpers due to an error:', err, '\n(continuing anyway...)');
}
};//ƒ
// If we received an explicit set of helpers to load, use them.
// Otherwise reload helpers from the appropriate folder.
if (helpers) {
sails.helpers = helpers;
return done();
} else {
return loadHelpers(sails, done);
}
}//ƒ
};
};