Galooshi/import-js

View on GitHub
lib/JsModule.js

Summary

Maintainability
A
1 hr
Test Coverage
A
97%
// @flow

import path from 'path';

import Configuration from './Configuration';
import ImportStatement from './ImportStatement';
import forwardSlashes from './forwardSlashes';
import requireResolve from './requireResolve';
import resolveImportPathAndMain from './resolveImportPathAndMain';

// TODO figure out a more holistic solution than stripping node_modules
function stripNodeModules(path: string): string {
  if (path.startsWith('node_modules/')) {
    return path.slice(13);
  }

  return path;
}

// Class that represents a js module found in the file system
export default class JsModule {
  hasNamedExports: ?boolean;

  isType: boolean;

  importPath: string;

  filePath: string;

  variableName: string;

  workingDirectory: string;

  /**
   * @param {Boolean} hasNamedExports
   * @param {Boolean} isType
   * @param {String|null} opts.makeRelativeTo a path to a different file which
   *   the resulting import path should be relative to.
   * @param {String} opts.relativeFilePath a full path to the file, relative to
   *   the project root.
   * @param {Array} opts.stripFileExtensions a list of file extensions to strip,
   *   e.g. ['.js', '.jsx']
   * @param {String} opts.variableName
   * @param {String} opts.workingDirectory
   * @return {JsModule}
   */
  static construct({
    hasNamedExports,
    isType = false,
    makeRelativeTo,
    relativeFilePath,
    stripFileExtensions,
    variableName,
    workingDirectory = process.cwd(),
  }: {
    hasNamedExports?: boolean,
    isType?: boolean,
    makeRelativeTo?: ?string,
    relativeFilePath: string,
    stripFileExtensions: Array<string>,
    variableName: string,
    workingDirectory: string,
  } = {}): ?JsModule {
    const jsModule = new JsModule();
    jsModule.filePath = relativeFilePath;

    const importPathAndMainFile = resolveImportPathAndMain(
      jsModule.filePath,
      stripFileExtensions,
      workingDirectory,
    );
    const importPath = importPathAndMainFile[0];
    const mainFile = importPathAndMainFile[1];

    if (!importPath) {
      return null;
    }

    if (mainFile) {
      jsModule.filePath = forwardSlashes(
        path.normalize(path.join(importPath, mainFile)),
      );
    }

    jsModule.importPath = importPath;
    jsModule.hasNamedExports = hasNamedExports;
    jsModule.isType = isType;
    jsModule.variableName = variableName;
    if (makeRelativeTo) {
      jsModule.makeRelativeTo(makeRelativeTo);
    } else {
      jsModule.importPath = jsModule.importPath.replace(/^\.\//, '');
    }
    return jsModule;
  }

  constructor({
    hasNamedExports,
    isType = false,
    importPath,
    variableName,
  }: {
    hasNamedExports?: boolean,
    isType?: boolean,
    importPath: string,
    variableName: string,
  } = {}) {
    this.hasNamedExports = hasNamedExports;
    this.isType = isType;
    this.importPath = importPath;
    this.variableName = variableName;
  }

  makeRelativeTo(makeRelativeToPath: string) {
    let importPath = path.relative(
      path.dirname(makeRelativeToPath),
      this.importPath,
    );

    importPath = forwardSlashes(importPath);

    // `path.relative` will not add "./" automatically
    if (!importPath.startsWith('.')) {
      importPath = `./${importPath}`;
    }

    this.importPath = importPath;
  }

  resolvedFilePath(
    pathToCurrentFile: string,
    workingDirectory: string = process.cwd(),
  ): string {
    if (this.filePath) {
      return this.filePath;
    }

    // There is no filePath. This likely means that we are working with an
    // alias, so we want to expand it to a full path if we can.
    if (this.importPath.startsWith('.')) {
      // The import path in the alias starts with a ".", which means that it is
      // relative to the current file. In order to open this file, we need to
      // expand it to a full path.
      return forwardSlashes(
        path.resolve(path.dirname(pathToCurrentFile), this.importPath),
      );
    }

    // If all of the above fails to find a path, we fall back to using
    // require.resolve() to find the file path.
    const unresolved = path.join(workingDirectory, this.importPath);
    const resolved = requireResolve(unresolved);
    if (unresolved !== resolved) {
      // We found a location for the import
      return resolved;
    }
    // as a last resort, assume it's a package dependency
    return requireResolve(
      path.join(workingDirectory, 'node_modules', this.importPath),
    );
  }

  _getNamedImports(): Array<Object> {
    if (!this.hasNamedExports) {
      return [];
    }
    return [{ localName: this.variableName, isType: this.isType }];
  }

  _getDefaultImport(): string {
    if (this.hasNamedExports) {
      return '';
    }
    return this.variableName;
  }

  toImportStatement(config: Configuration): ImportStatement {
    const namedImports = this._getNamedImports();
    const defaultImport = this._getDefaultImport();
    // TODO figure out a more holistic solution than stripping node_modules
    const pathToImportedModule = stripNodeModules(
      this.resolvedFilePath(config.pathToCurrentFile, config.workingDirectory),
    );

    return new ImportStatement({
      declarationKeyword: config.get('declarationKeyword', {
        pathToImportedModule,
      }),
      defaultImport,
      hasSideEffects: false,
      importFunction: config.get('importFunction', { pathToImportedModule }),
      namedImports,
      areOnlyTypes: this.isType,
      danglingCommas: config.get('danglingCommas'),
      path: config.get('moduleNameFormatter', {
        pathToImportedModule,
        // TODO figure out a more holistic solution than stripping node_modules
        moduleName: stripNodeModules(this.importPath),
      }),
    });
  }
}