riot/create-riot

View on GitHub
src/utils.js

Summary

Maintainability
A
45 mins
Test Coverage
import { askCustomTemplatePath, askProjectTemplate } from './prompts.js'
import { unlink, createWriteStream } from 'node:fs'
import { CUSTOM_PROJECT_KEY } from './constants.js'
import { URL } from 'url'
import extractZip from 'extract-zip'
import { join } from 'node:path'
import { merge } from 'lodash-es'
import { promisify } from 'util'
import { render } from 'ejs'
import rimraf from 'rimraf'
import through from 'through2'
import spawn from '@npmcli/promise-spawn'
import fetch from 'node-fetch'

const removeFile = promisify(unlink)

// validators for the prompts
export const validateEmptyString = (message) => (value) =>
  value.length ? true : message
export const validateWrongUrl = (message) => (value) =>
  isValidUrl(value) ? true : message

/**
 * Terminate the process with a critical error
 * @param  {string} message - error message
 * @param  {Error} error - error object
 * @return {undefined}
 */
/* istanbul ignore next */
export function panic(message, error) {
  console.log('\n')
  console.error(message)

  // exit with 1 after the error dispatching
  setImmediate(() => {
    process.exit(1)
  })

  console.log('Error details:\n')
  throw new Error(error)
}

/**
 * Get the template zip file path by a template type
 * @param  {string} type - template type id
 * @return {string} path to the template zip file to download
 */
export const getTemplateZipPathByTemplateType = (type) =>
  `https://github.com/riot/${type}-template/archive/main.zip`

/**
 * Check if the url passed is valid
 * @param  {string} url - url to check
 * @return {boolean} true if it's a valid url
 */
export const isValidUrl = (url) => {
  try {
    new URL(url)
    return true
  } catch (err) {
    return false
  }
}

/**
 * Download a remote file and copy it in a local system folder
 * @param {string} remoteFileUrl - remote file uri
 * @param {string} destinationFolder - local system file location where the file will be copied
 * @return {Promise<string>} path to the local file
 */
export async function downloadFile(remoteFileUrl, destinationFolder) {
  const fileName = 'template.zip'
  const destinationFile = join(destinationFolder, fileName)

  try {
    const response = await fetch(remoteFileUrl)
    if (!response.ok) {
      throw new Error('Failed to download template zip file')
    }

    const dest = createWriteStream(destinationFile)
    await new Promise((resolve, reject) => {
      response.body.pipe(dest)
      response.body.on('error', reject)
      dest.on('finish', resolve)
    })
  } catch (error) {
    /* istanbul ignore next */
    panic('It was not possible to download the template zip file', error)
  }

  return destinationFile
}

/**
 * Delete a folder recursively from the file system
 * @param  {string} path - path to the folder to delete
 * @return {Promise<undefined>} IO operation
 */
export function deleteFolder(path) {
  return new Promise((resolve) => {
    try {
      rimraf(path, {}, resolve)
    } catch (error) {
      /* istanbul ignore next */
      panic(`It was not possible to delete the "${path}" folder`, error)
    }
  })
}

/**
 * Delete a file from the file system
 * @param  {string} path - path to the file to delete
 * @return {Promise<undefined>} IO operation
 */
export function deleteFile(path) {
  try {
    return removeFile(path)
  } catch (error) {
    /* istanbul ignore next */
    panic(`It was not possible to delete the "${path}" file`, error)
  }
}

/**
 * Transform the files template files interpolating the package.json values to their content
 * @param  {Object} pkg - package.json content
 * @return {Function} - function returning a through stream
 */
export const transformFiles = (pkg) => (src) => {
  return through((chunk, enc, done) => {
    const originalFileContent = chunk.toString()

    // if it's a package.json file we merge it with the one just created
    if (src.includes('package.json')) {
      done(null, JSON.stringify(merge(pkg, JSON.parse(originalFileContent))))
    } else {
      // otherwise we interpolate the file content with the package values
      try {
        const fileContent = render(originalFileContent, pkg)

        done(null, fileContent)
      } catch (error) {
        console.error('It was not possible to interpolate the values in', src)
        done(null, originalFileContent)
      }
    }
  })
}

/**
 * Unzip a file
 * @param  {string} path - zip file path
 * @param  {Object} options - extractZip options
 * @return {Promise<undefined>} IO operation
 */
export function unzip(path, options) {
  try {
    return extractZip(path, options)
  } catch (error) {
    /* istanbul ignore next */
    panic(`It was not possible to unzip the "${path}" file`, error)
  }
}

/**
 * Run `npm init` in the current directory
 * @param  {string} pkgManager - npm or yarn
 * @return {ChildProcess} - child process instance
 */
/* istanbul ignore next */
export const initPackage = (pkgManager) => {
  const args = process.argv.slice(2).filter((arg) => arg.startsWith('-'))

  return spawn(pkgManager, ['init', ...args], {
    stdio: 'inherit',
  })
}

/**
 * Get the template info depending on the user feedback
 * @return {Promise<Object>} an object containing the "templateZipURL" property
 */
/* istanbul ignore next */
export const getTemplateInfo = async () => {
  const { templateType } = await askProjectTemplate()

  if (templateType === CUSTOM_PROJECT_KEY) {
    return {
      ...(await askCustomTemplatePath()),
      templateType,
    }
  }

  return {
    templateZipURL: getTemplateZipPathByTemplateType(templateType),
    templateType,
  }
}