cbillowes/gatsby-remark-interactive-gifs

View on GitHub
src/gatsby-node.js

Summary

Maintainability
A
1 hr
Test Coverage
const path = require(`path`);
const fs = require(`fs-extra`);
const gifFrames = require(`gif-frames`);
const sizeOf = require(`image-size`);

let Reporter = console;

/**
 * @typedef {object} PluginOptions
 * @property {string} root Project's working directory. Absolute path.
 * @property {string} src Where all the interactive gifs are stored. Absolute path.
 * @property {string} dest A directory in public where the gifs should be copied to. Absolute path.
 * @property {string} play The image that indicates that the gif can be interacted with. Absolute path.
 * @property {string} placeholder The image which shows when the gif is missing in action. Absolute path.
 * @property {string} loading The image which shows when the gif is downloading. Absolute path.
 */

/**
 * Verifies if the path exists.
 * @param {string} option
 * @param {string} path
 * @returns {Promise}
 */
async function verifyPathExists(option, path) {
  return fs.pathExists(path, (err, exists) => {
    if (err || !exists)
      Reporter.error(`Path does not exist [${option}]: ${path}`);
    return exists;
  });
}

/**
 * Validate if all required paths exist.
 * @param {PluginOptions} pluginOptions
 * @returns {boolean}
 */
const validate = (pluginOptions) => {
  const verifies = [
    verifyPathExists(`root`, pluginOptions.root),
    verifyPathExists(`src`, pluginOptions.src),
    verifyPathExists(`play`, pluginOptions.play),
    verifyPathExists(`placeholder`, pluginOptions.placeholder),
  ];
  return verifies.every(Boolean);
};

/**
 * Gets that bas64 contents of a file.
 * @param {string} pathAndfilename
 * @returns {string}
 */
const getBase64 = (pathAndfilename) => {
  return Buffer.from(fs.readFileSync(pathAndfilename)).toString(`base64`);
};

/**
 * Copy gifs, play and placeholder images from src to dest.
 * @param {string[]} files A list of all gif file names to be copied.
 * @param {PluginOptions} pluginOptions
 * @returns {void}
 */
const copyFiles = (files, pluginOptions) => {
  let copy = files.map((filename) => path.join(pluginOptions.src, filename));
  copy.push(pluginOptions.play);
  copy.push(pluginOptions.placeholder);
  copy.push(pluginOptions.loading);
  copy.map((src) => {
    const dest = path.join(pluginOptions.dest, path.basename(src));
    fs.copyFile(src, dest);
  });
};

/**
 * Create the still image from a gif.
 * @param {string} file A file name for the gif that will generate a still image from src to dest.
 * @param {PluginOptions} pluginOptions
 * @returns {void}
 */
const createStill = (file, pluginOptions) => {
  const src = path.join(pluginOptions.src, file);
  const dest = path.join(pluginOptions.dest, `still-${path.basename(src)}`);

  // @es-ignore
  gifFrames({ url: src, frames: 0 })
    // @ts-ignore
    .then((frameData) => {
      frameData[0].getImage().pipe(fs.createWriteStream(dest));
    });
};

/**
 * Gets the relative path of a file from its absolute path.
 * @param {string} absolutePath
 * @param {string} filePath
 * @returns {string}
 */
const getRelativePath = (absolutePath, filePath) => {
  return path.relative(absolutePath, filePath).replace(/public/gi, ``);
};

/**
 * Create the node data.
 * @param {string} filename
 * @param {string} base64
 * @param {PluginOptions} pluginOptions
 * @returns {object}
 */
const createNodeData = (filename, base64, pluginOptions) => {
  const src = path.join(pluginOptions.src, filename);
  const dest = path.join(pluginOptions.dest, filename);
  const still = path.join(pluginOptions.dest, `still-${filename}`);
  const { width, height } = sizeOf.imageSize(src);
  const root = pluginOptions.root;
  return {
    absolutePath: dest,
    sourcePath: src,
    relativePath: getRelativePath(root, dest),
    stillRelativePath: getRelativePath(root, still),
    base64,
    width,
    height,
  };
};

/**
 * @param {string} filename
 * @param {string} base64
 * @param {Function} createNodeId
 * @param {Function} createContentDigest
 * @returns {object}
 */
const createNodeMeta = (
  filename,
  base64,
  createNodeId,
  createContentDigest,
) => {
  return {
    id: createNodeId(`interactive-gif-${filename}`),
    parent: null,
    children: [],
    internal: {
      type: `InteractiveGif`,
      mediaType: `image/gif`,
      content: filename,
      contentDigest: createContentDigest(base64),
    },
  };
};

/**
 * Creates the source node.
 * @param {string} filename Of the gif to be added to GraphQL.
 * @param {object} options
 * @param {PluginOptions} pluginOptions
 * @returns {void}
 */
const createSourceNode = (filename, options, pluginOptions) => {
  const { actions, createNodeId, createContentDigest } = options;
  const { createNode } = actions;
  const base64 = getBase64(path.join(pluginOptions.src, filename));
  const data = createNodeData(filename, base64, pluginOptions);
  const meta = createNodeMeta(
    filename,
    base64,
    createNodeId,
    createContentDigest,
  );
  const node = Object.assign(data, meta);
  createNode(node);
};

/**
 * @param {{ reporter: object; }} options
 * @param {PluginOptions} pluginOptions
 * @returns {void}
 */
exports.sourceNodes = (options, pluginOptions) => {
  const { reporter } = options;
  Reporter = reporter;

  if (validate(pluginOptions)) {
    fs.mkdirp(pluginOptions.dest, (err) => {
      if (err)
        Reporter.error(
          `Cannot make directory [dest]: ${pluginOptions.dest} -> ${err}`,
        );
    });

    fs.readdir(pluginOptions.src, (err, files) => {
      if (err) {
        Reporter.error(
          `Cannot read directory [src]: ${pluginOptions.src} -> ${err}`,
        );
        return;
      }

      const gifFiles = files.filter(function (file) {
        return path.extname(file).toLowerCase() === '.gif';
      });

      copyFiles(gifFiles, pluginOptions);
      gifFiles.forEach((filename) => {
        createStill(filename, pluginOptions);
        createSourceNode(filename, options, pluginOptions);
      });
    });
  }
};