Home Manual Reference Source Test Repository

lib/server/server.js

import path from 'path';
import Hapi from 'hapi';
import Boom from 'boom';
import inert from 'inert';
import chokidar from 'chokidar';
import Constants from '../constants';
import log from '../log';
import prunePrivateProperties from './prune-private-properties';
import api from './api';

function debounceFunction(fn) {
  return (...args) => {
    if (fn.running) {
      return;
    }
    fn.running = true;
    fn(...args).then(() => {
      fn.running = false;
    });
  };
}

export default class Server {
  constructor(reptar) {
    this.reptar = reptar;

    this.server = new Hapi.Server();

    this.server.connection({
      host: reptar.config.get('server.host'),
      port: reptar.config.get('server.port'),
    });
  }

  /**
   * Starts the Hapi server.
   * @return {Promise}
   */
  async start() {
    await this.server.register([
      inert,
      { register: api, options: { reptar: this.reptar } },
    ]);

    this.server.route({
      method: 'GET',
      path: '/{p*}',
      config: {
        // If an error occurs when parsing a cookie don't error, just log it.
        state: {
          failAction: 'log',
        },
      },
      handler: (request, reply) => {
        this.routeHandler(request, reply).catch(e => {
          reply(Boom.badData(e.message));
        });
      },
    });

    this.createFsWatchers();

    // Start the server
    return this.server.start();
  }

  /**
   * Get File/CollectionPage based on request.path.
   * @param {string} requestPath Request path to server.
   * @return {File|CollectionPage}
   */
  getFile(requestPath) {
    let file = this.reptar.destination[requestPath];
    if (!file) {
      file = this.reptar.destination[path.join(requestPath, 'index.html')];
    }
    return file;
  }

  /**
   * Our default route handler for every request.
   * @param {Object} request Hapi Request object.
   * @param {Object} reply Hapi Response object.
   * @return {Promise}
   */
  routeHandler = async (request, reply) => {
    const isDebug = request.query.debug != null;

    const file = this.getFile(request.path);

    if (!file) {
      return reply(Boom.resourceGone('No file found to render.'));
    }

    if (file.assetProcessor) {
      const content = await file.render();
      const contentType = request.server.mime.path(request.path).type;
      return reply(content).type(contentType);
    }

    // If this File does not require any processing then it's a static asset
    // and we can just render it.
    if (file.skipProcessing) {
      return reply.file(file.path);
    }

    // Update the File/CollectionPage from disk.
    await file.update(this.reptar.metadata.get());

    // We need to make sure we run all middleware and lifecycle hooks on
    // every render to ensure you get an accurate representation of your site.
    await this.reptar.update({ skipFiles: true });

    // Render the File/CollectionPage.
    const content = await file.render(this.reptar.metadata.get());

    // If we want debug information then render the JSON version.
    if (isDebug) {
      // Exclude private fields from being returned.
      const debugFile = prunePrivateProperties(file);
      return reply(JSON.stringify(debugFile)).type('application/json');
    }

    log.info(`Rendering ${file.id}`);

    return reply(content);
  };

  /**
   * Create file system watchers to update Reptar state according to when a
   * user updates files.
   */
  createFsWatchers() {
    chokidar
      .watch([path.join(this.reptar.config.root, Constants.ConfigFilename)])
      .on(
        'change',
        debounceFunction(async changePath => {
          log.info(`${Constants.ConfigFilename} updated at ${changePath}`);

          try {
            await this.reptar.update();
          } catch (e) {
            log.error(e);
          }
        })
      );

    chokidar.watch([this.reptar.config.get('path.data')]).on(
      'change',
      debounceFunction(async changePath => {
        log.info(`Data updated at ${changePath}`);

        try {
          await this.reptar.update();
        } catch (e) {
          log.error(e);
        }
      })
    );
  }
}