All files / src/helpers exports.js

100% Statements 46/46
97.3% Branches 36/37
100% Functions 7/7
100% Lines 44/44
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125                                          19x 19x 4x           28x 4x   4x         11x 11x 11x 4x     4x 4x     11x   11x 24x                 11x               11x       19x 19x       19x 17x 97x 2x   632x 95x 95x 72x 68x     4x           19x       72x 72x 72x 1x   71x 66x 66x   63x   3x   2x     5x   5x   3x       3x    
 
import { types as t } from "@babel/core";
import * as th from "./templates";
import * as ast from "./ast";
 
/**
 * Collapse named exports onto the default export so that the default export can be returned directly,
 * so that exports can be easily used by code that does not include an import interop.
 * This includes the following:
 *  - Ignore named exports that already exist on the default export and which reference the same identifer.
 *  - If possible, assign any remaining exports to the default export.
 *    - This cannot be done if there is a naming conflict.
 *
 * In order to determine what properties the default export already has, the plugin will scan the
 * top level of the program to find any assignments, including Object.assign() and _extend() calls.
 * It does this recursively in the case of those method calls.
 *
 * If there are any named exports left, they get returned, so the main plugin can check the opts
 * and decide if an error should be thrown.
 */
export function collapseNamedExports(programNode, defaultExport, namedExports, opts) {
  namedExports = filterOutExportsWhichAlreadyMatchPropsOnDefault(programNode, defaultExport, namedExports);
  if (!namedExports.length || opts.noExportExtend) {
    return {
      filteredExports: namedExports,
      newDefaultExportIdentifier: null
    };
  }
 
  if (namedExports.some(exp => exp.conflict)) {
    return {
      filteredExports: namedExports,
      conflictingExports: namedExports.filter(exp => exp.conflict),
      newDefaultExportIdentifier: null
    };
  }
 
  let exportVariableName = getDefaultExportName(defaultExport);
  let newDefaultExportIdentifier = null;
  if (!exportVariableName) { // Something anonymous (literal, anon function, etc...)
    programNode.body.push(th.buildTempExport({
      VALUE: defaultExport
    }));
    newDefaultExportIdentifier = th.exportsIdentifier;
    exportVariableName = newDefaultExportIdentifier.name;
  }
 
  const id = t.identifier(exportVariableName);
 
  for (const namedExport of namedExports) {
    programNode.body.push(
      th.buildAssign({
        OBJECT: id,
        NAME: namedExport.key,
        VALUE: namedExport.value
      })
    );
  }
 
  return {
    filteredExports: [],
    conflictingExports: [],
    newDefaultExportIdentifier: newDefaultExportIdentifier
  };
}
 
export function getDefaultExportName(defaultExport) {
  return defaultExport.name || ast.getIdName(defaultExport);
}
 
function filterOutExportsWhichAlreadyMatchPropsOnDefault(programNode, defaultExport, namedExports) {
  namedExports = [...namedExports]; // Shallow clone for safe splicing
  const defaultObjectProperties = ast.findPropertiesOfNode(programNode, defaultExport);
 
  // Loop through the default object's props and see if it already has a matching definition of the named exports.
  // If the definition matches, we can ignore the named export. If the definition does not match, mark it as a conflict.
  if (defaultObjectProperties) {
    for (const defaultExportProperty of defaultObjectProperties) {
      if (!defaultExportProperty.key) { // i.e. SpreadProperty
        continue;
      }
      const matchingNamedExportIndex = namedExports.findIndex(namedExport => namedExport.key.name === defaultExportProperty.key.name); // get the index for splicing
      const matchingNamedExport = namedExports[matchingNamedExportIndex];
      if (matchingNamedExport && !matchingNamedExport.conflict) {
        if (areSame(defaultExportProperty, matchingNamedExport)) {
          namedExports.splice(matchingNamedExportIndex, 1);
        }
        else {
          matchingNamedExport.conflict = true;
        }
      }
    }
  }
 
  return namedExports;
}
 
function areSame(defaultExportProperty, matchingNamedExport) {
  const defaultExportValue = defaultExportProperty.value;
  const declaration = matchingNamedExport.declaration; // i.e. 'export const a = 1' or 'export let a = b'
  if (!defaultExportValue) { // i.e. object method
    return false; // always a conflict
  }
  else if (t.isIdentifier(defaultExportValue)) {
    const valueName = defaultExportValue.name;
    if (valueName === matchingNamedExport.value.name) {
      // Same identifier reference. Safe to ignore
      return true;
    }
    else if (declaration && t.isIdentifier(declaration.init) && declaration.init.name === valueName) {
      // i.e. 'export let a = b', and default value for a is 'b'.
      return true;
    }
  }
  else Eif (t.isLiteral(defaultExportValue)) { // i.e. { a: 1 } or { a: '1' }
    // See if the original declaration for the matching export was for the same literal
    if (t.isLiteral(declaration.init) && declaration.init.value === defaultExportValue.value) { // i.e. 'export const a = 1'
      // Same literal. Safe to ignore
      return true;
    }
  }
  // Either a conflicting value or no value at all (i.e. method)
  return false;
}