2.2.0 • Published 8 months ago

transform-imports v2.2.0

Weekly downloads
1,084
License
MIT
Repository
github
Last release
8 months ago

transform-imports

An API that makes it easy to transform imports and require calls.

Usage

import transformImports from "transform-imports";

const code = `
  import Foo from "foo";
  import Bar from "bar";
`;

const newCode = transformImports(code, (importDefs) => {
  importDefs.forEach((importDef) => {
    if (importDef.source === "bar") {
      importDef.source = "something-new";
    }
  });
});

console.log(newCode);
// Logs:
// import Foo from "foo";
// import Foo from "something-new";

transformImports calls its callback with an Array of ImportDefinition objects, which have this shape:

class ImportDefinition {
  variableName: ?string;
  source: ?string;
  importedExport: {
    name: string,
    isImportedAsCJS: boolean,
  };
  kind: "value" | "type" | "typeof",
  isDynamicImport: boolean,
  path: NodePath;

  remove(): void;
  fork(?{ insert: "before" | "after" }): void;
}

You can change the values of the properties on the ImportDefinition or call the methods on it to change the underlying import/require statement.

Each ImportDefinition is associated with a single import; that is, import { red, blue } from "colors" contains two ImportDefinitions (one for red and one for blue).

Here's an example of what some different imports/requires parse into:

import * as React from "react";
// Becomes...
ImportDefinition {
  variableName: "React",
  source: "react",
  importedExport: {
    name: "*",
    isImportedAsCJS: false,
  },
  kind: "value",
  isDynamicImport: false,
};

import traverse from "babel-traverse";
// Becomes...
ImportDefinition {
  variableName: "traverse",
  source: "babel-traverse",
  importedExport: {
    name: "default",
    isImportedAsCJS: false,
  },
  kind: "value",
  isDynamicImport: false,
};

import MyClass, { SOME_CONSTANT } from "my-library";
// Becomes...
ImportDefinition {
  variableName: "MyClass",
  source: "my-library",
  importedExport: {
    name: "default",
    isImportedAsCJS: false,
  },
  kind: "value",
  isDynamicImport: false,
};
// and
ImportDefinition {
  variableName: "SOME_CONSTANT",
  source: "my-library",
  importedExport: {
    name: "SOME_CONSTANT",
    isImportedAsCJS: false,
  },
  kind: "value",
  isDynamicImport: false,
};

const PropTypes = require("prop-types");
// Becomes...
ImportDefinition {
  variableName: "PropTypes",
  source: "prop-types",
  importedExport: {
    name: "*",
    isImportedAsCJS: true,
  },
  kind: "value",
  isDynamicImport: false,
};

const { darken, lighten } = require("polished");
// Becomes...
ImportDefinition {
  variableName: "darken",
  source: "polished",
  importedExport: {
    name: "darken",
    isImportedAsCJS: true,
  },
  kind: "value",
  isDynamicImport: false,
};
// and
ImportDefinition {
  variableName: "lighten",
  source: "polished",
  importedExport: {
    name: "lighten",
    isImportedAsCJS: true,
  },
  kind: "value",
  isDynamicImport: false,
};

import type {Node} from "./node";
// Becomes...
ImportDefinition {
  variableName: "Node",
  source: "./node",
  importedExport: {
    name: "Node",
    isImportedAsCJS: false,
  },
  kind: "type",
  isDynamicImport: false,
};

import "something";
// Becomes...
ImportDefinition {
  variableName: null,
  source: "something",
  importedExport: {
    name: "*",
    isImportedAsCJS: false,
  },
  kind: "value",
  isDynamicImport: false,
};

import("something").then((somethingModule) => somethingModule.default());
// Becomes...
ImportDefinition {
  variableName: null,
  source: "something",
  importedExport: {
    name: "*",
    isImportedAsCJS: false,
  },
  kind: "value",
  isDynamicImport: true,
};

Here's some more in-depth documentation explaining what each property means/does. In each example, importDef refers to an ImportDefinition object as passed to the transformImports callback.

variableName

This refers to the name of the variable that was created by the import. For example, in import MyThing from "./overThere";, it would be MyThing.

If you change this string, then the variable name in the code will change. So doing this:

importDef.variableName = "MyOtherThing";

would change the code into:

import MyOtherThing from "./overThere";

When dealing with named imports, this only refers to the name of the local variable created. To also change the name of the variable that is being imported, change importedExport.name:

// Given this code:
import { red } from "./colors";

// Doing this:
importDef.variableName = "blue";

// Would only change the code to this:
import { red as blue } from "./colors";

// But doing this:
importDef.variableName = "blue";
importDef.importedExport.name = "blue";

// Would change the code to this:
import { blue } from "blue";

Changing variableName does not rename references to the variable:

// Given this code:
import MyThing from "./overThere";

MyThing.isCool();

// Doing this:
importDef.variableName = "MyOtherThing";

// Would change the code to this:
import MyOtherThing from "./overThere";

MyThing.isCool();

If you want to also change references to the variable, you can use the babel Scope object found at importDef.path.scope.

NOTE: An ImportDefinition referring to a dynamic import (import("foo")) or bare import (import "foo";) has no variableName, and attempting to set the variableName will throw an Error.

source

This refers to the string indicating which file or package this import was obtained from. So for const Theme = require("../theme"), it would be "../theme".

If you change this string, then the import's source will change.

// Given this code:
import Something from "../somewhere";

// Doing this:
importDef.source = "../somewhereElse";

// Would change the code to this:
import Something from "../somewhereElse";

Note that the source string always reflects the source string found in the source code. If you would like to find the absolute path to a given imported file, you will need to use another package, like eslint-import-resolver-node or eslint-import-resolver-webpack:

const fileLocation = "/Users/suchipi/Code/my-project/something.js"; // Get this from babel, jscodeshift, etc

const { resolve } = require("eslint-import-resolver-webpack");
const { found, path } = resolve(importDef.source, fileLocation, {
  /* import resolver config goes here */
});

For more info, see the eslint-plugin-import Resolver spec.

importedExport.name

This refers to the name of the export that is being imported into the file where the code associated with this ImportDefinition is.

For example, given two files one.js and two.js:

// one.js

const Chunky = "chunky";
const Bacon = "bacon";

export { Chunky, Bacon };
// two.js

import { Chunky as BaconStyle } from "./one";

console.log(BaconStyle);

The value of importedExport.name for the import in two.js would be Chunky.

If you change this string, then which export you pull in from two.js would change:

// If you did this:
importDef.importedExport.name = "default";

// Then it would change into a default import:
import BaconStyle from "./one";

// If you did this:
importDef.importedExport.name = "*";

// Then it would change into a namespace import:
import * as BaconStyle from "./one";

// If you did anything else:
importDef.importedExport.name = "Bacon";

// Then it would change which named export is imported:
import { Bacon as BaconStyle } from "./one";

NOTE: Attempting to change importedExport.name on an ImportDefinition referring to a dynamic import (import("foo")) or bare import (import "foo";) will throw an Error.

importedExport.isImportedAsCJS

This refers to the whether the import is using CommonJS or not. Changing this to true will change an import statement into a require call, and changing this to false will change a require call into an import statement.

// Given this code:
import Default from "somewhere";
import * as Star from "somewhere-else";
import { Named } from "somewhere-else-else";

// If you did this to each ImportDefinition:
importDef.importedExport.isImportedAsCJS = true;

// Then the code would change into this:
const { default: Default } = require("somewhere");
const Star = require("somewhere-else");
const { Named } = require("somewhere-else-else");

// Likewise, given this code:
const All = require("all");
const { Some, Members } = require("members");

// If you did this to each ImportDefinition:
importDef.importedExport.isImportedAsCJS = false;

// Then the code would change into this:
import * as All from "all";
import { Some, Members } from "members";

Note that in const Foo = require("foo"), importedExport.name is "*", not "default" like might be expected. This is because "*" is the most accurate representation of the way CommonJS imports work in most compilation pipelines.

NOTE: Attempting to change importedExport.isImportedAsCJS on an ImportDefinition referring to a dynamic import (import("foo")) or bare import (import "foo";) will throw an Error.

kind

This value indicates whether the import is a flow type/typeof import, or if it is a normal import (a value import). Possible values are "type", "typeof", and "value". Changing this value will change the import into a type, typeof, or value import.

// Given this code:
import Default from "somewhere";
import * as Star from "somewhere-else";
import { Named } from "somewhere-else-else";

// If you did this to each ImportDefinition:
importDef.kind = "type";

// Then the code would change into this:
import type Default from "somewhere";
import type * as Star from "somewhere-else";
import type { Named } from "somewhere-else-else";

// Likewise, if you did this to each ImportDefinition:
importDef.kind = "typeof";

// Then the code would change into this:
import typeof Default from "somewhere";
import typeof * as Star from "somewhere-else";
import typeof { Named } from "somewhere-else-else";

If you change a require call's kind into type or typeof, then it will turn into an import statement according to the rules established when changing importedExport.isImportedAsCJS:

// Given this code:
const All = require("all");
const { Some, Members } = require("members");

// If you did this to each ImportDefinition:
importDef.kind = "type";

// Then the code would change into this:
import type * as All from "all";
import type { Some, Members } from "members";

NOTE: Attempting to change kind on an ImportDefinition referring to a dynamic import (import("foo")) or bare import (import "foo";) will throw an Error.

isDynamicImport

Whether the import is a dynamic import (import("something")).

This property is not writable.

remove()

Calling this method removes the import specifier associated with this ImportDefinition from the source code.

// Given this code:
import * as Everything from "everything";

Everything.isAwesome();

// Doing this:
importDef.remove();

// Would change the code to this:
Everything.isAwesome();

If the ImportDefinition refers to an import that is not alone in its import/require statement, then only it will be removed:

// Given this code:
import { One, Two } from "every-number";

console.log(One < Two);

// Doing this to the ImportDefinition referring to `One`:
importDef.remove();

// Would change the code to this:
import { Two } from "every-number";

console.log(One < Two);

NOTE: Attempting to call remove() on an ImportDefinition referring to a dynamic import (import()) will throw an Error.

fork()

Calling this method will split an import specifier away from its related specifiers into its own statement:

// Given this code:
import { One, Two } from "every-number";

// Doing this to the ImportDefinition referring to `One`:
importDef.fork();

// Would change the code to this:
import { Two } from "every-number";
import { One } from "every-number";

Note that the new import/require declaration is inserted after the existing one by default. To change this behavior, call fork with { insert: "before" }:

// Given this code:
import { One, Two } from "every-number";

// Doing this to the ImportDefinition referring to `One`:
importDef.fork({ insert: "before" });

// Would change the code to this:
import { One } from "every-number";
import { Two } from "every-number";

fork() is automatically called when you change properties of an ImportDefinition such that it cannot remain with its sibling specifiers in the same statement anymore:

// Given this code:
import { One, Two } from "every-number";

// Doing this to the ImportDefinition referring to `One`:
importDef.source = "most-numbers";

// Would change the code to this:
import { Two } from "every-number";
import { One } from "most-numbers";

Usage with jscodeshift

transform-imports uses recast, so untouched source styling is preserved. This means that it's suitable for usage in jscodeshift:

// An example jscodeshift transform module that turns
//   import React, { PropTypes } from "react";
// into
//   import * as React from "react";
//   import PropTypes from "prop-types";
const transformImports = require("transform-imports");

module.exports = function (fileInfo, api, options) {
  return transformImports(fileInfo.source, (importDefs) => {
    importDefs.forEach((importDef) => {
      if (importDef.source !== "react") {
        return;
      }

      if (
        importDef.importedExport.name === "default" &&
        importDef.importedExport.isImportedAsCJS === false
      ) {
        importDef.fork({ insert: "before" });
        importDef.importedExport.name = "*";
      }

      if (
        importDef.variableName === "PropTypes" &&
        importDef.importedExport.name === "PropTypes"
      ) {
        importDef.importedExport.name = "default";
        importDef.source = "prop-types";
      }
    });
  });
};