riot/compiler

View on GitHub
src/generators/javascript/utils.js

Summary

Maintainability
A
1 hr
Test Coverage
import {
  RIOT_INTERFACE_WRAPPER_NAME,
  RIOT_MODULE_ID,
  RIOT_TAG_INTERFACE_NAME,
  TAG_LOGIC_PROPERTY,
} from '../../constants.js'
import { builders, types } from '../../utils/build-types.js'
import {
  isExportDefaultStatement,
  isExportNamedDeclaration,
  isImportDeclaration,
  isInterfaceDeclaration,
  isThisExpressionStatement,
  isTypeAliasDeclaration,
} from '../../utils/ast-nodes-checks.js'
import compose from 'cumpa'

/**
 * Find the export default statement
 * @param   { Array } body - tree structure containing the program code
 * @returns { Object } node containing only the code of the export default statement
 */
export function findExportDefaultStatement(body) {
  return body.find(isExportDefaultStatement)
}

/**
 * Find all import declarations
 * @param   { Array } body - tree structure containing the program code
 * @returns { Array } array containing all the import declarations detected
 */
export function findAllImportDeclarations(body) {
  return body.filter(isImportDeclaration)
}

/**
 * Find all the named export declarations
 * @param   { Array } body - tree structure containing the program code
 * @returns { Array } array containing all the named export declarations detected
 */
export function findAllExportNamedDeclarations(body) {
  return body.filter(isExportNamedDeclaration)
}

/**
 * Filter all the import declarations
 * @param   { Array } body - tree structure containing the program code
 * @returns { Array } array containing all the ast expressions without the import declarations
 */
export function filterOutAllImportDeclarations(body) {
  return body.filter((n) => !isImportDeclaration(n))
}

/**
 * Filter all the export declarations
 * @param   { Array } body - tree structure containing the program code
 * @returns { Array } array containing all the ast expressions without the export declarations
 */
export function filterOutAllExportDeclarations(body) {
  return body.filter(
    (n) => !isExportNamedDeclaration(n) || isExportDefaultStatement(n),
  )
}

/**
 * Find the component interface exported
 * @param   { Array } body - tree structure containing the program code
 * @returns { Object|null } the object referencing the component interface if found
 */
export function findComponentInterface(body) {
  const exportNamedDeclarations = body
    .filter(isExportNamedDeclaration)
    .map((n) => n.declaration)
  const types = exportNamedDeclarations.filter(isTypeAliasDeclaration)
  const interfaces = exportNamedDeclarations.filter(isInterfaceDeclaration)
  const isRiotComponentTypeName = ({ typeName }) =>
    typeName && typeName.name
      ? typeName.name === RIOT_TAG_INTERFACE_NAME
      : false
  const extendsRiotComponent = ({ expression }) =>
    expression.name === RIOT_TAG_INTERFACE_NAME

  return (
    types.find(
      (node) =>
        (node.typeAnnotation.types &&
          node.typeAnnotation.types.some(isRiotComponentTypeName)) ||
        isRiotComponentTypeName(node.typeAnnotation),
    ) ||
    interfaces.find(
      (node) => node.extends && node.extends.some(extendsRiotComponent),
    )
  )
}

/**
 * Add the component interface to the export declaration
 * @param   { Object } ast - ast object generated by recast
 * @param   { Object } componentInterface - the component typescript interface
 * @returns { Object } the component object exported combined with the riot typescript interfaces
 */
export function addComponentInterfaceToExportedObject(ast, componentInterface) {
  const body = getProgramBody(ast)
  const RiotComponentWrapperImportSpecifier = builders.importSpecifier(
    builders.identifier(RIOT_INTERFACE_WRAPPER_NAME),
  )
  const componentInterfaceName = componentInterface.id.name
  const riotImportDeclaration = findAllImportDeclarations(body).find(
    (node) => node.source.value === RIOT_MODULE_ID,
  )
  const exportDefaultStatement = body.find(isExportDefaultStatement)
  const objectExport = exportDefaultStatement.declaration

  // add the RiotComponentWrapper to this component imports
  if (riotImportDeclaration) {
    riotImportDeclaration.specifiers.push(RiotComponentWrapperImportSpecifier)
  } else {
    // otherwise create the whole import statement from riot
    body.unshift(
      0,
      builders.importDeclaration(
        [RiotComponentWrapperImportSpecifier],
        builders.stringLiteral(RIOT_MODULE_ID),
      ),
    )
  }

  // override the object export adding the types detected
  exportDefaultStatement.declaration = builders.tsAsExpression(
    objectExport,
    builders.tsTypeReference(
      builders.identifier(RIOT_INTERFACE_WRAPPER_NAME),
      builders.tsTypeParameterInstantiation([
        builders.tsTypeReference(builders.identifier(componentInterfaceName)),
      ]),
    ),
  )

  return ast
}

/**
 * Create the default export declaration interpreting the old riot syntax relying on "this" statements
 * @param   { Array } body - tree structure containing the program code
 * @returns { Object } ExportDefaultDeclaration
 */
export function createDefaultExportFromLegacySyntax(body) {
  return builders.exportDefaultDeclaration(
    builders.functionDeclaration(
      builders.identifier(TAG_LOGIC_PROPERTY),
      [],
      builders.blockStatement([
        ...compose(
          filterOutAllImportDeclarations,
          filterOutAllExportDeclarations,
        )(body),
        builders.returnStatement(builders.thisExpression()),
      ]),
    ),
  )
}

/**
 * Find all the code in an ast program except for the export default statements
 * @param   { Array } body - tree structure containing the program code
 * @returns { Array } array containing all the program code except the export default expressions
 */
export function filterNonExportDefaultStatements(body) {
  return body.filter(
    (node) =>
      !isExportDefaultStatement(node) && !isThisExpressionStatement(node),
  )
}

/**
 * Get the body of the AST structure
 * @param   { Object } ast - ast object generated by recast
 * @returns { Array } array containing the program code
 */
export function getProgramBody(ast) {
  return ast.body || ast.program.body
}

/**
 * Extend the AST adding the new tag method containing our tag sourcecode
 * @param   { Object } ast - current output ast
 * @param   { Object } exportDefaultNode - tag export default node
 * @returns { Object } the output ast having the "tag" key extended with the content of the export default
 */
export function extendTagProperty(ast, exportDefaultNode) {
  types.visit(ast, {
    visitProperty(path) {
      if (path.value.key.name === TAG_LOGIC_PROPERTY) {
        path.value.value = exportDefaultNode.declaration
        return false
      }

      this.traverse(path)
    },
  })

  return ast
}