1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | var path = require('path');
|
7 | var _ = require('@sailshq/lodash');
|
8 | var argv = require('yargs').argv;
|
9 | var fs = require('fs');
|
10 | var async = require('async');
|
11 | var npmResolve = require('resolve');
|
12 | var defaults = require('./defaults.js');
|
13 | var glob = require('glob');
|
14 |
|
15 | module.exports = function(options) {
|
16 |
|
17 | traceStartup('begin');
|
18 |
|
19 |
|
20 |
|
21 | var self = {
|
22 | __meta: {
|
23 | name: 'apostrophe'
|
24 | }
|
25 | };
|
26 |
|
27 |
|
28 |
|
29 | self.apos = self;
|
30 |
|
31 | require('./lib/modules/apostrophe-module/lib/events.js')(self, options);
|
32 |
|
33 | try {
|
34 |
|
35 | self.root = options.root || getRoot();
|
36 | self.rootDir = options.rootDir || path.dirname(self.root.filename);
|
37 | self.npmRootDir = options.npmRootDir || self.rootDir;
|
38 |
|
39 | testModule();
|
40 |
|
41 | self.options = mergeConfiguration(options, defaults);
|
42 | autodetectBundles();
|
43 | acceptGlobalOptions();
|
44 |
|
45 |
|
46 | self.handlers = {};
|
47 |
|
48 |
|
49 | self.eventHandlers = {};
|
50 |
|
51 | traceStartup('defineModules');
|
52 | defineModules();
|
53 | } catch (err) {
|
54 | if (options.initFailed) {
|
55 |
|
56 | return options.initFailed(err);
|
57 | } else {
|
58 | throw err;
|
59 | }
|
60 | }
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | async.series([
|
66 | instantiateModules,
|
67 | modulesReady,
|
68 | modulesAfterInit,
|
69 | lintModules,
|
70 | migrate,
|
71 | afterInit
|
72 | ], function(err) {
|
73 | if (err) {
|
74 | if (options.initFailed) {
|
75 |
|
76 | return options.initFailed(err);
|
77 | } else {
|
78 | throw err;
|
79 | }
|
80 | }
|
81 | traceStartup('startup end');
|
82 | if (self.argv._.length) {
|
83 | self.emit('runTask');
|
84 | } else {
|
85 |
|
86 | self.listen();
|
87 | }
|
88 | });
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 | self.emit = function(eventName /* ,arg1, arg2, arg3... */) {
|
103 | var handlers = self.handlers[eventName];
|
104 | if (!handlers) {
|
105 | return;
|
106 | }
|
107 | var args = Array.prototype.slice.call(arguments, 1);
|
108 | var i;
|
109 | for (i = 0; (i < handlers.length); i++) {
|
110 | handlers[i].apply(self, args);
|
111 | }
|
112 | };
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 | self.on = function(eventName, fn) {
|
123 | self.handlers[eventName] = (self.handlers[eventName] || []).concat([ fn ]);
|
124 | };
|
125 |
|
126 |
|
127 |
|
128 | self.off = function(eventName, fn) {
|
129 | if (!fn) {
|
130 | delete self.handlers[eventName];
|
131 | return;
|
132 | }
|
133 | self.handlers[eventName] = _.filter(self.handlers[eventName], function(_fn) {
|
134 | return fn !== _fn;
|
135 | });
|
136 | };
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 | self.callAll = function(method, /* argument, ... */ callback) {
|
149 | var args = Array.prototype.slice.call(arguments);
|
150 | var extraArgs = args.slice(1, args.length - 1);
|
151 | callback = args[args.length - 1];
|
152 | return async.eachSeries(_.keys(self.modules), function(name, callback) {
|
153 | return invoke(name, method, extraArgs, callback);
|
154 | }, function(err) {
|
155 | if (err) {
|
156 | return callback(err);
|
157 | }
|
158 | return callback(null);
|
159 | });
|
160 | };
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 | self.callOne = function(moduleName, method, /* argument, ... */ callback) {
|
167 | var args = Array.prototype.slice.call(arguments);
|
168 | var extraArgs = args.slice(2, args.length - 1);
|
169 | callback = args[args.length - 1];
|
170 | return invoke(moduleName, method, extraArgs, callback);
|
171 | };
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | self.destroy = function(callback) {
|
184 | return self.callAllAndEmit('apostropheDestroy', 'destroy', callback);
|
185 | };
|
186 |
|
187 |
|
188 |
|
189 | self.isTask = function() {
|
190 | return !!self.argv._.length;
|
191 | };
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | self.instancesOf = function(name) {
|
201 | return _.filter(self.modules, function(module) {
|
202 | return self.synth.instanceOf(module, name);
|
203 | });
|
204 | };
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 | self.instanceOf = function(object, name) {
|
211 | return self.synth.instanceOf(object, name);
|
212 | };
|
213 |
|
214 |
|
215 |
|
216 | return self;
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | function mergeConfiguration(options, defaults) {
|
222 | var config = {};
|
223 | var local = {};
|
224 | var localPath = options.__localPath || '/data/local.js';
|
225 | var reallyLocalPath = self.rootDir + localPath;
|
226 |
|
227 | if (fs.existsSync(reallyLocalPath)) {
|
228 | local = require(reallyLocalPath);
|
229 | }
|
230 |
|
231 |
|
232 |
|
233 |
|
234 | config = _.cloneDeep(options.__testDefaults || defaults);
|
235 |
|
236 | _.merge(config, options);
|
237 |
|
238 | if (typeof (local) === 'function') {
|
239 | if (local.length === 1) {
|
240 | _.merge(config, local(self));
|
241 | } else if (local.length === 2) {
|
242 | local(self, config);
|
243 | } else {
|
244 | throw new Error('data/local.js may export an object, a function that takes apos as an argument and returns an object, OR a function that takes apos and config as objects and directly modifies config');
|
245 | }
|
246 | } else {
|
247 | _.merge(config, local || {});
|
248 | }
|
249 |
|
250 | return config;
|
251 | }
|
252 |
|
253 | function getRoot() {
|
254 | var _module = module;
|
255 | var m = _module;
|
256 | while (m.parent) {
|
257 |
|
258 |
|
259 | if (m.parent.filename.match(/\/node_modules\/mocha\//)) {
|
260 | return m;
|
261 | }
|
262 | m = m.parent;
|
263 | _module = m;
|
264 | }
|
265 | return _module;
|
266 | }
|
267 |
|
268 | function nestedModuleSubdirs() {
|
269 | if (!options.nestedModuleSubdirs) {
|
270 | return;
|
271 | }
|
272 | var configs = glob.sync(self.moogOptions.localModules + '/**/modules.js');
|
273 | _.each(configs, function(config) {
|
274 | try {
|
275 | _.merge(self.options.modules, require(config));
|
276 | } catch (e) {
|
277 | console.error('When nestedModuleSubdirs is active, any modules.js file beneath ' + self.moogOptions.localModules + '\nmust export an object containing configuration for Apostrophe modules.\nThe file ' + config + ' did not parse.');
|
278 | throw e;
|
279 | }
|
280 | });
|
281 | }
|
282 |
|
283 | function autodetectBundles() {
|
284 | var modules = _.keys(self.options.modules);
|
285 | _.each(modules, function(name) {
|
286 | var path = getNpmPath(name);
|
287 | if (!path) {
|
288 | return;
|
289 | }
|
290 | var module = require(path);
|
291 | if (module.moogBundle) {
|
292 | self.options.bundles = (self.options.bundles || []).concat(name);
|
293 | _.each(module.moogBundle.modules, function(name) {
|
294 | if (!_.has(self.options.modules, name)) {
|
295 | var bundledModule = require(require('path').dirname(path) + '/' + module.moogBundle.directory + '/' + name);
|
296 | if (bundledModule.improve) {
|
297 | self.options.modules[name] = {};
|
298 | }
|
299 | }
|
300 | });
|
301 | }
|
302 | });
|
303 | }
|
304 |
|
305 | function getNpmPath(name) {
|
306 | var parentPath = path.resolve(self.npmRootDir);
|
307 | try {
|
308 | return npmResolve.sync(name, { basedir: parentPath });
|
309 | } catch (e) {
|
310 |
|
311 |
|
312 | return null;
|
313 | }
|
314 | }
|
315 |
|
316 | function acceptGlobalOptions() {
|
317 |
|
318 | if (options.testModule) {
|
319 |
|
320 |
|
321 | self.argv = {
|
322 | _: []
|
323 | };
|
324 | self.options.shortName = self.options.shortName || 'test';
|
325 | } else if (options.argv) {
|
326 |
|
327 |
|
328 | self.argv = options.argv;
|
329 | } else {
|
330 | self.argv = argv;
|
331 | }
|
332 |
|
333 | self.shortName = self.options.shortName;
|
334 | if (!self.shortName) {
|
335 | throw "Specify the `shortName` option and set it to the name of your project's repository or folder";
|
336 | }
|
337 | self.title = self.options.title;
|
338 | self.baseUrl = self.options.baseUrl;
|
339 | self.prefix = self.options.prefix || '';
|
340 | }
|
341 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 | function testModule() {
|
353 | if (!options.testModule) {
|
354 | return;
|
355 | }
|
356 | if (!options.shortName) {
|
357 | options.shortName = 'test';
|
358 | }
|
359 | defaults = _.cloneDeep(defaults);
|
360 | _.defaults(defaults, {
|
361 | 'apostrophe-express': {}
|
362 | });
|
363 | _.defaults(defaults['apostrophe-express'], {
|
364 | port: 7900,
|
365 | secret: 'irrelevant'
|
366 | });
|
367 | var m = findTestModule();
|
368 |
|
369 | var testDir = require('path').dirname(m.filename);
|
370 | var testRegex;
|
371 | if (process.platform === "win32") {
|
372 | testRegex = /\\tests?$/;
|
373 | } else {
|
374 | testRegex = /\/tests?$/;
|
375 | }
|
376 | var moduleDir = testDir.replace(testRegex, '');
|
377 | if (testDir === moduleDir) {
|
378 | throw new Error('Test file must be in test/ or tests/ subdirectory of module');
|
379 | }
|
380 | var moduleName = require('path').basename(moduleDir);
|
381 | try {
|
382 |
|
383 | var packageName = JSON.parse(fs.readFileSync(path.resolve(moduleDir, 'package.json'), 'utf8')).name;
|
384 | if (typeof packageName === 'string') {
|
385 | moduleName = packageName;
|
386 | }
|
387 | } catch (e) {}
|
388 | var testDependenciesDir = testDir + require("path").normalize('/node_modules/');
|
389 | if (!fs.existsSync(testDependenciesDir + moduleName)) {
|
390 |
|
391 | if (!fs.existsSync(testDependenciesDir)) {
|
392 | fs.mkdirSync(testDependenciesDir);
|
393 | }
|
394 |
|
395 | if (moduleName.charAt(0) === '@' && moduleName.includes(require("path").sep)) {
|
396 | var scope = moduleName.split(require("path").sep)[0];
|
397 | var scopeDir = testDependenciesDir + scope;
|
398 | if (!fs.existsSync(scopeDir)) {
|
399 | fs.mkdirSync(scopeDir);
|
400 | }
|
401 | }
|
402 |
|
403 |
|
404 | var type;
|
405 | if (process.platform === "win32") {
|
406 | type = "junction";
|
407 | } else {
|
408 | type = "dir";
|
409 | }
|
410 | fs.symlinkSync(moduleDir, testDependenciesDir + moduleName, type);
|
411 | }
|
412 |
|
413 |
|
414 |
|
415 |
|
416 | function findTestModule() {
|
417 | var m = module;
|
418 | var nodeModuleRegex;
|
419 | if (process.platform === "win32") {
|
420 | nodeModuleRegex = /node_modules\\mocha/;
|
421 | } else {
|
422 | nodeModuleRegex = /node_modules\/mocha/;
|
423 | }
|
424 | while (m) {
|
425 | if (m.parent && m.parent.filename.match(nodeModuleRegex)) {
|
426 | return m;
|
427 | }
|
428 | m = m.parent;
|
429 | if (!m) {
|
430 | throw new Error('mocha does not seem to be running, is this really a test?');
|
431 | }
|
432 | }
|
433 | }
|
434 | }
|
435 |
|
436 | function defineModules() {
|
437 |
|
438 |
|
439 | self.moogOptions = {
|
440 | root: self.root,
|
441 | bundles: [ 'apostrophe' ].concat(self.options.bundles || []),
|
442 | localModules: self.options.modulesSubdir || self.options.__testLocalModules || (self.rootDir + '/lib/modules'),
|
443 | defaultBaseClass: 'apostrophe-module',
|
444 | nestedModuleSubdirs: self.options.nestedModuleSubdirs
|
445 | };
|
446 | var synth = require('moog-require')(self.moogOptions);
|
447 |
|
448 | self.synth = synth;
|
449 |
|
450 |
|
451 |
|
452 | self.define = self.synth.define;
|
453 | self.redefine = self.synth.redefine;
|
454 | self.create = self.synth.create;
|
455 |
|
456 | nestedModuleSubdirs();
|
457 |
|
458 | _.each(self.options.modules, function(options, name) {
|
459 | synth.define(name, options);
|
460 | });
|
461 |
|
462 | return synth;
|
463 | }
|
464 |
|
465 | function instantiateModules(callback) {
|
466 | traceStartup('instantiateModules');
|
467 | self.modules = {};
|
468 | return async.eachSeries(_.keys(self.options.modules), function(item, callback) {
|
469 | traceStartup('Instantiating module ' + item);
|
470 | var improvement = self.synth.isImprovement(item);
|
471 | if (self.options.modules[item] && (improvement || self.options.modules[item].instantiate === false)) {
|
472 |
|
473 |
|
474 |
|
475 |
|
476 | return setImmediate(callback);
|
477 | }
|
478 | return self.synth.create(item, { apos: self }, function(err, obj) {
|
479 | if (err) {
|
480 | return callback(err);
|
481 | }
|
482 | return callback(null);
|
483 | });
|
484 | }, function(err) {
|
485 | return setImmediate(function() {
|
486 | return callback(err);
|
487 | });
|
488 | });
|
489 | }
|
490 |
|
491 | function modulesReady(callback) {
|
492 | traceStartup('modulesReady');
|
493 | return self.callAllAndEmit('modulesReady', 'modulesReady', callback);
|
494 | }
|
495 |
|
496 | function modulesAfterInit(callback) {
|
497 | traceStartup('modulesAfterInit');
|
498 | return self.callAllAndEmit('afterInit', 'afterInit', callback);
|
499 | }
|
500 |
|
501 | function lintModules(callback) {
|
502 | traceStartup('lintModules');
|
503 | _.each(self.modules, function(module, name) {
|
504 | if (module.options.extends && ((typeof module.options.extends) === 'string')) {
|
505 | lint('The module ' + name + ' contains an "extends" option. This is probably a\nmistake. In Apostrophe "extend" is used to extend other modules.');
|
506 | }
|
507 | if (module.options.singletonWarningIfNot && (name !== module.options.singletonWarningIfNot)) {
|
508 | lint('The module ' + name + ' extends ' + module.options.singletonWarningIfNot + ', which is normally\na singleton (Apostrophe creates only one instance of it). Two competing\ninstances will lead to problems. If you are adding project-level code to it,\njust use lib/modules/' + module.options.singletonWarningIfNot + '/index.js and do not use "extend".\nIf you are improving it via an npm module, use "improve" rather than "extend".\nIf neither situation applies you should probably just make a new module that does\nnot extend anything.\n\nIf you are sure you know what you are doing, you can set the\nsingletonWarningIfNot: false option for this module.');
|
509 | }
|
510 | if (name.match(/-widgets$/) && (!extending(module)) && (!module.options.ignoreNoExtendWarning)) {
|
511 | lint('The module ' + name + ' does not extend anything.\n\nA `-widgets` module usually extends `apostrophe-widgets` or\n`apostrophe-pieces-widgets`. Or possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\n`ignoreNoExtendWarning` option to `true` for this module.');
|
512 | } else if (name.match(/-pages$/) && (name !== 'apostrophe-pages') && (!extending(module)) && (!module.options.ignoreNoExtendWarning)) {
|
513 | lint('The module ' + name + ' does not extend anything.\n\nA `-pages` module usually extends `apostrophe-custom-pages` or\n`apostrophe-pieces-pages`. Or possibly you forgot to npm install something.\n\nIf you are sure you are doing the right thing, set the\n`ignoreNoExtendWarning` option to `true` for this module.');
|
514 | } else if ((!extending(module)) && (!hasConstruct(name)) && (!isMoogBundle(name)) && (!module.options.ignoreNoCodeWarning)) {
|
515 | lint('The module ' + name + ' does not extend anything and does not have a\n`beforeConstruct`, `construct` or `afterConstruct` function. This usually means that you:\n\n1. Forgot to `extend` another module\n2. Configured a module that comes from npm without npm installing it\n3. Simply haven\'t written your `index.js` yet\n\nIf you really want a module with no code, set the `ignoreNoCodeWarning` option\nto `true` for this module.');
|
516 | }
|
517 | });
|
518 | function hasConstruct(name) {
|
519 | var d = self.synth.definitions[name];
|
520 | if (d.construct) {
|
521 |
|
522 | return true;
|
523 | }
|
524 | if (self.synth.isMy(d.__meta.name)) {
|
525 |
|
526 | d = d.extend;
|
527 | }
|
528 |
|
529 |
|
530 | if (d.__meta.name.match(/apostrophe-module$/)) {
|
531 | return false;
|
532 | }
|
533 | return d.beforeConstruct || d.construct || d.afterConstruct;
|
534 | }
|
535 | function isMoogBundle(name) {
|
536 | var d = self.synth.definitions[name];
|
537 | return d.moogBundle || (d.extend && d.extend.moogBundle);
|
538 | }
|
539 | function extending(module) {
|
540 |
|
541 |
|
542 |
|
543 |
|
544 | return module.__meta.chain.length > 4;
|
545 | }
|
546 | return callback(null);
|
547 | }
|
548 |
|
549 | function migrate(callback) {
|
550 | traceStartup('migrate');
|
551 | if (self.argv._[0] === 'apostrophe-migrations:migrate') {
|
552 |
|
553 |
|
554 | return callback(null);
|
555 | }
|
556 |
|
557 |
|
558 |
|
559 |
|
560 | if (process.env.APOS_NO_MIGRATE || (self.options.migrate === false)) {
|
561 | return callback(null);
|
562 | }
|
563 |
|
564 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 | return self.promiseEmit('migrate', {}).then(function() {
|
570 | return callback(null);
|
571 | }).catch(callback);
|
572 | }
|
573 |
|
574 | function lint(s) {
|
575 | self.utils.warnDev('\n⚠️ It looks like you may have made a mistake in your code:\n\n' + s + '\n');
|
576 | }
|
577 |
|
578 | function afterInit(callback) {
|
579 | traceStartup('afterInit');
|
580 |
|
581 |
|
582 | if (!self.options.afterInit) {
|
583 | return setImmediate(callback);
|
584 | }
|
585 | return self.options.afterInit(callback);
|
586 | }
|
587 |
|
588 |
|
589 | function invoke(moduleName, method, extraArgs, callback) {
|
590 | var module = self.modules[moduleName];
|
591 | var invoke = module[method];
|
592 | if (invoke) {
|
593 | if (invoke.length === (1 + extraArgs.length)) {
|
594 | return invoke.apply(module, extraArgs.concat([callback]));
|
595 | } else if (invoke.length === extraArgs.length) {
|
596 | return setImmediate(function () {
|
597 | try {
|
598 | invoke.apply(module, extraArgs);
|
599 | } catch (e) {
|
600 | return callback(e);
|
601 | }
|
602 | return callback(null);
|
603 | });
|
604 | } else {
|
605 | return callback(moduleName + ' module: your ' + method + ' method must take ' + extraArgs.length + ' arguments, plus an optional callback.');
|
606 | }
|
607 | } else {
|
608 | return setImmediate(callback);
|
609 | }
|
610 | }
|
611 |
|
612 | };
|
613 |
|
614 | var abstractClasses = [ 'apostrophe-module', 'apostrophe-widgets', 'apostrophe-custom-pages', 'apostrophe-pieces', 'apostrophe-pieces-pages', 'apostrophe-pieces-widgets', 'apostrophe-doc-type-manager' ];
|
615 |
|
616 | module.exports.moogBundle = {
|
617 | modules: abstractClasses.concat(_.keys(defaults.modules)),
|
618 | directory: 'lib/modules'
|
619 | };
|
620 |
|
621 | function traceStartup(message) {
|
622 | if (process.env.APOS_TRACE_STARTUP) {
|
623 |
|
624 | console.debug('⌁ startup ' + message);
|
625 | }
|
626 | }
|