477 lines
11 KiB
JavaScript
Raw Normal View History

2015-10-01 00:30:16 +02:00
'use strict';
2017-07-24 19:58:03 +02:00
// Dependencies.
2017-07-25 17:12:18 +02:00
const http = require('http');
const path = require('path');
2018-05-04 17:02:27 +02:00
const { EventEmitter } = require('events');
const fse = require('fs-extra');
2018-05-04 17:02:27 +02:00
const Koa = require('koa');
2019-08-21 11:05:33 +02:00
const Router = require('koa-router');
2019-04-05 16:11:09 +02:00
const _ = require('lodash');
2017-09-29 14:26:28 +02:00
const { logger, models } = require('strapi-utils');
const chalk = require('chalk');
const CLITable = require('cli-table3');
2018-05-04 17:02:27 +02:00
const utils = require('./utils');
2019-04-05 16:11:09 +02:00
const {
loadConfig,
2019-04-05 16:11:09 +02:00
loadApis,
loadAdmin,
loadPlugins,
2019-04-05 16:11:09 +02:00
loadMiddlewares,
loadHooks,
2019-04-09 15:29:17 +02:00
bootstrap,
loadExtensions,
2019-04-09 21:51:28 +02:00
initCoreStore,
2019-06-03 21:00:03 +02:00
loadGroups,
2019-04-05 16:11:09 +02:00
} = require('./core');
2017-07-24 19:58:03 +02:00
const initializeMiddlewares = require('./middlewares');
const initializeHooks = require('./hooks');
const createStrapiFs = require('./core/fs');
const getPrefixedDeps = require('./utils/get-prefixed-dependencies');
2019-09-20 12:44:24 +02:00
const { createDatabaseManager } = require('strapi-dbal');
2019-07-15 15:33:42 +02:00
2015-10-01 00:30:16 +02:00
/**
* Construct an Strapi instance.
*
* @constructor
*/
2016-07-26 11:57:50 +02:00
class Strapi extends EventEmitter {
constructor({ dir, autoReload = false, serveAdminPanel = true } = {}) {
2016-07-06 15:51:52 +02:00
super();
this.setMaxListeners(100);
2016-07-06 15:51:52 +02:00
2017-08-02 13:17:40 +02:00
this.reload = this.reload();
2016-07-06 15:51:52 +02:00
// Expose `koa`.
2016-11-04 16:00:19 +01:00
this.app = new Koa();
2019-08-21 11:05:33 +02:00
this.router = new Router();
2016-07-06 15:51:52 +02:00
// Mount the HTTP server.
2017-07-31 11:35:57 +02:00
this.server = http.createServer(this.app.callback());
2017-08-10 15:38:08 +02:00
// Logger.
this.log = logger;
2017-09-12 17:58:31 +02:00
// Utils.
this.utils = {
2018-05-18 14:22:24 +02:00
models,
2017-09-12 17:58:31 +02:00
};
2017-07-31 11:35:57 +02:00
// Exclude EventEmitter, Koa and HTTP server to be freezed.
this.propertiesToNotFreeze = Object.keys(this);
// Expose `admin`.
this.admin = {};
// Expose `plugin`.
this.plugins = {};
2019-05-17 11:37:35 +02:00
this.dir = dir || process.cwd();
const pkgJSON = require(path.resolve(this.dir, 'package.json'));
2017-07-24 19:58:03 +02:00
// Default configurations.
this.config = {
serveAdminPanel,
launchedAt: Date.now(),
appPath: this.dir,
2019-05-13 17:32:52 +02:00
autoReload,
2017-07-24 19:58:03 +02:00
host: process.env.HOST || process.env.HOSTNAME || 'localhost',
port: process.env.PORT || 1337,
2019-04-05 16:11:09 +02:00
environment: _.toLower(process.env.NODE_ENV) || 'development',
2017-07-25 17:12:18 +02:00
environments: {},
2018-09-13 16:44:42 +02:00
admin: {},
2017-07-24 19:58:03 +02:00
paths: {
2017-07-25 17:12:18 +02:00
admin: 'admin',
2017-07-24 19:58:03 +02:00
api: 'api',
2017-07-25 17:12:18 +02:00
config: 'config',
2017-07-24 19:58:03 +02:00
controllers: 'controllers',
models: 'models',
plugins: 'plugins',
2017-07-25 17:12:18 +02:00
policies: 'policies',
tmp: '.tmp',
services: 'services',
static: 'public',
2017-07-24 19:58:03 +02:00
validators: 'validators',
2018-05-18 14:22:24 +02:00
views: 'views',
2017-07-25 17:12:18 +02:00
},
2017-07-26 18:53:48 +02:00
middleware: {},
hook: {},
functions: {},
2018-05-18 14:22:24 +02:00
routes: {},
info: pkgJSON,
installedPlugins: getPrefixedDeps('strapi-plugin', pkgJSON),
installedMiddlewares: getPrefixedDeps('strapi-middleware', pkgJSON),
installedHooks: getPrefixedDeps('strapi-hook', pkgJSON),
2017-07-24 19:58:03 +02:00
};
this.fs = createStrapiFs(this);
2019-06-19 08:45:10 +02:00
this.requireProjectBootstrap();
2016-07-06 15:51:52 +02:00
}
requireProjectBootstrap() {
const bootstrapPath = path.resolve(
this.dir,
'config/functions/bootstrap.js'
);
if (fse.existsSync(bootstrapPath)) {
require(bootstrapPath);
}
}
logStats() {
const columns = Math.min(process.stderr.columns, 80) - 2;
console.log();
console.log(chalk.black.bgWhite(_.padEnd(' Project information', columns)));
console.log();
const infoTable = new CLITable({
colWidths: [20, 50],
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
});
infoTable.push(
[chalk.blue('Time'), `${new Date()}`],
[chalk.blue('Launched in'), Date.now() - this.config.launchedAt + ' ms'],
[chalk.blue('Environment'), this.config.environment],
[chalk.blue('Process PID'), process.pid],
[
chalk.blue('Version'),
`${this.config.info.strapi} (node v${this.config.info.node})`,
]
);
console.log(infoTable.toString());
console.log();
console.log(chalk.black.bgWhite(_.padEnd(' Actions available', columns)));
console.log();
}
logFirstStartupMessage() {
this.logStats();
console.log(chalk.bold('One more thing...'));
console.log(
chalk.grey(
'Create your first administrator 💻 by going to the administration panel at:'
)
);
console.log();
const addressTable = new CLITable();
addressTable.push([chalk.bold(this.config.admin.url)]);
console.log(`${addressTable.toString()}`);
console.log();
}
logStartupMessage() {
this.logStats();
console.log(chalk.bold('Welcome back!'));
if (this.config.serveAdminPanel === true) {
console.log(
chalk.grey(
'To manage your project 🚀, go to the administration panel at:'
)
);
console.log(chalk.bold(this.config.admin.url));
console.log();
}
console.log(chalk.grey('To access the server ⚡️, go to:'));
console.log(chalk.bold(this.config.url));
console.log();
}
2019-04-26 16:19:08 +02:00
async start(cb) {
2017-07-24 19:58:03 +02:00
try {
2018-03-16 11:42:17 +01:00
// Emit starting event.
this.emit('server:starting');
2019-04-30 14:47:49 +02:00
2017-07-24 19:58:03 +02:00
await this.load();
2019-09-20 12:44:24 +02:00
2017-07-25 17:12:18 +02:00
// Run bootstrap function.
2019-04-09 15:29:17 +02:00
await this.runBootstrapFunctions();
2017-07-31 11:35:57 +02:00
// Freeze object.
await this.freeze();
2018-11-08 21:07:33 +01:00
// Init first start
2019-04-30 14:47:49 +02:00
utils.init(this.config);
2019-08-21 11:05:33 +02:00
this.app.use(this.router.routes()).use(this.router.allowedMethods());
2017-07-25 17:12:18 +02:00
// Launch server.
this.server.listen(this.config.port, async err => {
2019-04-05 16:11:09 +02:00
if (err) return this.stopWithError(err);
2017-07-24 19:58:03 +02:00
if (this.config.init) {
this.logFirstStartupMessage();
} else {
this.logStartupMessage();
}
2017-07-25 17:12:18 +02:00
2018-03-16 11:42:17 +01:00
// Emit started event.
this.emit('server:started');
2017-07-25 17:12:18 +02:00
if (cb && typeof cb === 'function') {
cb();
}
if (
(this.config.environment === 'development' &&
2019-04-05 16:11:09 +02:00
_.get(
this.config.currentEnvironment,
'server.admin.autoOpen',
true
) !== false) ||
this.config.init
) {
await utils.openBrowser.call(this);
}
2017-07-24 19:58:03 +02:00
});
2018-03-16 11:42:17 +01:00
} catch (err) {
2019-04-05 16:11:09 +02:00
this.stopWithError(err);
2017-07-24 19:58:03 +02:00
}
2016-07-26 11:57:50 +02:00
}
2019-04-05 16:11:09 +02:00
/**
* Add behaviors to the server
*/
2017-07-25 17:12:18 +02:00
async enhancer() {
2019-04-05 16:11:09 +02:00
// handle port in use cleanly
this.server.on('error', err => {
if (err.code === 'EADDRINUSE') {
return this.stopWithError(
`The port ${err.port} is already used by another application.`
);
}
this.log.error(err);
});
// Close current connections to fully destroy the server
2017-07-25 17:12:18 +02:00
const connections = {};
2017-07-25 17:12:18 +02:00
this.server.on('connection', conn => {
const key = conn.remoteAddress + ':' + conn.remotePort;
connections[key] = conn;
2016-07-26 11:57:50 +02:00
conn.on('close', function() {
2018-05-04 17:02:27 +02:00
delete connections[key];
2017-07-25 17:12:18 +02:00
});
});
2016-07-26 11:57:50 +02:00
2017-07-25 17:12:18 +02:00
this.server.destroy = cb => {
this.server.close(cb);
2016-07-26 11:57:50 +02:00
2017-07-25 17:12:18 +02:00
for (let key in connections) {
connections[key].destroy();
2018-05-04 17:02:27 +02:00
}
2017-07-25 17:12:18 +02:00
};
2016-07-26 11:57:50 +02:00
}
2019-04-05 16:11:09 +02:00
stopWithError(err) {
this.log.debug(`⛔️ Server wasn't able to start properly.`);
this.log.error(err);
return this.stop();
}
2016-07-26 11:57:50 +02:00
stop() {
2017-07-25 17:12:18 +02:00
// Destroy server and available connections.
2017-07-24 19:58:03 +02:00
this.server.destroy();
2017-09-04 15:38:29 +02:00
2019-05-13 17:32:52 +02:00
if (this.config.autoReload) {
process.send('stop');
}
2017-09-04 15:38:29 +02:00
2017-07-25 17:12:18 +02:00
// Kill process.
process.exit(1);
2016-07-26 11:57:50 +02:00
}
2017-07-24 19:58:03 +02:00
async load() {
await this.enhancer();
this.app.use(async (ctx, next) => {
2017-09-07 16:35:12 +02:00
if (ctx.request.url === '/_health' && ctx.request.method === 'HEAD') {
ctx.set('strapi', 'You are so French!');
ctx.status = 204;
2017-09-06 11:06:18 +02:00
} else {
await next();
}
});
const [
config,
api,
admin,
plugins,
2019-06-08 18:50:07 +02:00
middlewares,
hook,
extensions,
2019-06-03 21:00:03 +02:00
groups,
] = await Promise.all([
loadConfig(this),
loadApis(this),
loadAdmin(this),
loadPlugins(this),
loadMiddlewares(this),
loadHooks(this.config),
loadExtensions(this.config),
2019-06-03 21:00:03 +02:00
loadGroups(this),
]);
_.merge(this.config, config);
this.api = api;
this.admin = admin;
2019-06-03 21:00:03 +02:00
this.groups = groups;
this.plugins = plugins;
2019-04-05 16:11:09 +02:00
this.middleware = middlewares;
this.hook = hook;
2017-07-24 19:58:03 +02:00
/**
* Handle plugin extensions
*/
// merge extensions config folders
_.mergeWith(this.plugins, extensions.merges, (objValue, srcValue, key) => {
// concat routes
if (_.isArray(srcValue) && _.isArray(objValue) && key === 'routes') {
return srcValue.concat(objValue);
}
});
// overwrite plugins with extensions overwrites
extensions.overwrites.forEach(({ path, mod }) => {
_.assign(_.get(this.plugins, path), mod);
});
2017-07-25 17:12:18 +02:00
// Populate AST with configurations.
2019-04-11 09:32:16 +02:00
await bootstrap(this);
// Usage.
2019-04-05 16:11:09 +02:00
await utils.usage(this.config);
2018-04-24 12:30:43 +02:00
// Init core store
2019-04-09 21:51:28 +02:00
initCoreStore(this);
2019-09-20 12:44:24 +02:00
this.db = createDatabaseManager(this);
await this.db.initialize();
2017-07-25 17:12:18 +02:00
// Initialize hooks and middlewares.
2019-08-21 11:05:33 +02:00
await initializeMiddlewares.call(this);
await initializeHooks.call(this);
2016-07-26 11:57:50 +02:00
}
2017-08-02 11:25:18 +02:00
reload() {
2018-03-28 20:13:09 +02:00
const state = {
shouldReload: 0,
2018-03-28 20:13:09 +02:00
};
const reload = function() {
2018-08-31 13:47:10 +02:00
if (state.shouldReload > 0) {
// Reset the reloading state
2018-08-31 13:47:10 +02:00
state.shouldReload -= 1;
reload.isReloading = false;
2018-03-28 20:13:09 +02:00
return;
}
2019-05-13 17:32:52 +02:00
if (this.config.autoReload) {
2019-03-11 10:42:43 +01:00
this.server.close();
2018-01-04 16:03:34 +01:00
process.send('reload');
}
2017-08-02 11:25:18 +02:00
};
2018-03-28 20:13:09 +02:00
Object.defineProperty(reload, 'isWatching', {
configurable: true,
enumerable: true,
2018-05-18 14:22:24 +02:00
set: value => {
2018-03-28 20:13:09 +02:00
// Special state when the reloader is disabled temporarly (see GraphQL plugin example).
2018-08-31 13:47:10 +02:00
if (state.isWatching === false && value === true) {
state.shouldReload += 1;
}
2018-03-28 20:13:09 +02:00
state.isWatching = value;
},
get: () => {
return state.isWatching;
2018-05-18 14:22:24 +02:00
},
2018-03-28 20:13:09 +02:00
});
2017-08-02 11:25:18 +02:00
reload.isReloading = false;
reload.isWatching = true;
return reload;
2016-07-26 11:57:50 +02:00
}
2019-04-09 15:29:17 +02:00
async runBootstrapFunctions() {
2019-08-12 15:35:40 +02:00
const timeoutMs = this.config.bootstrapTimeout || 3500;
const warnOnTimeout = () =>
setTimeout(() => {
this.log.warn(
`The bootstrap function is taking unusually long to execute (${timeoutMs} miliseconds).`
);
this.log.warn('Make sure you call it?');
}, timeoutMs);
async function execBootstrap(fn) {
if (!fn) return;
const timer = warnOnTimeout();
try {
await fn();
} finally {
clearTimeout(timer);
}
}
const pluginBoostraps = Object.keys(this.plugins).map(plugin => {
return execBootstrap(
_.get(this.plugins[plugin], 'config.functions.bootstrap')
).catch(err => {
strapi.log.error(`Bootstrap function in plugin "${plugin}" failed`);
strapi.log.error(err);
strapi.stop();
2019-04-05 16:11:09 +02:00
});
2019-08-12 15:35:40 +02:00
});
await Promise.all(pluginBoostraps);
2019-04-05 16:11:09 +02:00
2019-08-12 15:35:40 +02:00
return execBootstrap(_.get(this.config, ['functions', 'bootstrap']));
2016-07-26 11:57:50 +02:00
}
2017-07-31 11:35:57 +02:00
async freeze() {
const propertiesToNotFreeze = this.propertiesToNotFreeze || [];
// Remove object from tree.
delete this.propertiesToNotFreeze;
2018-05-18 14:22:24 +02:00
return Object.keys(this)
2019-04-05 16:11:09 +02:00
.filter(x => !_.includes(propertiesToNotFreeze, x))
2018-05-18 14:22:24 +02:00
.forEach(key => {
Object.freeze(this[key]);
});
2016-07-26 11:57:50 +02:00
}
getModel(modelKey, plugin) {
2019-09-20 12:44:24 +02:00
return this.db.getModel(modelKey, plugin)
}
2019-05-16 21:37:45 +02:00
/**
* Binds queries with a specific model
* @param {string} entity - entity name
* @param {string} plugin - plugin name or null
*/
2019-07-15 15:33:42 +02:00
query(entity, plugin) {
2019-09-20 12:44:24 +02:00
return this.db.query(entity, plugin);
}
2016-07-26 11:57:50 +02:00
}
2019-04-11 09:32:16 +02:00
module.exports = options => {
const strapi = new Strapi(options);
global.strapi = strapi;
return strapi;
};