diff --git a/src/collections/ImportDeclaration.js b/src/collections/ImportDeclaration.js new file mode 100644 index 00000000..58d9a639 --- /dev/null +++ b/src/collections/ImportDeclaration.js @@ -0,0 +1,113 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +"use strict"; + +const Collection = require("../Collection"); +const NodeCollection = require("./Node"); + +const assert = require("assert"); +const once = require("../utils/once"); +const recast = require("recast"); + +const types = recast.types.namedTypes; + +const globalMethods = { + /** + * Inserts an ImportDeclaration at the top of the AST + * + * @param {string} sourcePath + * @param {Array} specifiers + */ + insertImportDeclaration: function (sourcePath, specifiers) { + assert.ok( + sourcePath && typeof sourcePath === "string", + "insertImportDeclaration(...) needs a source path" + ); + + assert.ok( + specifiers && Array.isArray(specifiers), + "insertImportDeclaration(...) needs an array of specifiers" + ); + + if (this.hasImportDeclaration(sourcePath)) { + return this; + } + + const importDeclaration = recast.types.builders.importDeclaration( + specifiers, + recast.types.builders.stringLiteral(sourcePath) + ); + + return this.forEach((path) => { + if (path.value.type === "Program") { + path.value.body.unshift(importDeclaration); + } + }); + }, + /** + * Finds all ImportDeclarations optionally filtered by name + * + * @param {string} sourcePath + * @return {Collection} + */ + findImportDeclarations: function (sourcePath) { + assert.ok( + sourcePath && typeof sourcePath === "string", + "findImportDeclarations(...) needs a source path" + ); + + return this.find(types.ImportDeclaration, { + source: { value: sourcePath }, + }); + }, + + /** + * Determines if the collection has an ImportDeclaration with the given sourcePath + * + * @param {string} sourcePath + * @returns {boolean} + */ + hasImportDeclaration: function (sourcePath) { + assert.ok( + sourcePath && typeof sourcePath === "string", + "findImportDeclarations(...) needs a source path" + ); + + return this.findImportDeclarations(sourcePath).length > 0; + }, + + /** + * Renames all ImportDeclarations with the given name + * + * @param {string} sourcePath + * @param {string} newSourcePath + * @return {Collection} + */ + renameImportDeclaration: function (sourcePath, newSourcePath) { + assert.ok( + sourcePath && typeof sourcePath === "string", + "renameImportDeclaration(...) needs a name to look for" + ); + + assert.ok( + newSourcePath && typeof newSourcePath === "string", + "renameImportDeclaration(...) needs a new name to rename to" + ); + + return this.findImportDeclarations(sourcePath).forEach((path) => { + path.value.source.value = newSourcePath; + }); + }, +}; + +function register() { + NodeCollection.register(); + Collection.registerMethods(globalMethods, types.Node); +} + +exports.register = once(register); diff --git a/src/collections/__tests__/ImportDeclaration-test.js b/src/collections/__tests__/ImportDeclaration-test.js new file mode 100644 index 00000000..d9d06f97 --- /dev/null +++ b/src/collections/__tests__/ImportDeclaration-test.js @@ -0,0 +1,147 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +"use strict"; + +const getParser = require("./../../getParser"); + +describe("ImportDeclaration API", function () { + let nodes; + let Collection; + let ImportDeclarationCollection; + let recast; + let types; + let b; + + beforeEach(function () { + jest.resetModules(); + + Collection = require("../../Collection"); + ImportDeclarationCollection = require("../ImportDeclaration"); + recast = require("recast"); + types = recast.types.namedTypes; + b = recast.types.builders; + + ImportDeclarationCollection.register(); + + nodes = [ + recast.parse( + [ + 'import FooBar from "XYZ";', + 'import Foo, { Bar, Baz } from "@meta/foo";', + 'import { Bar as Burger } from "@meta/bar";', + ].join("\n"), + { parser: getParser() } + ).program, + ]; + }); + + describe("Traversal", function () { + describe("hasImportDeclaration", function () { + it("returns true if an ImportDeclaration exists", function () { + const hasImport = + Collection.fromNodes(nodes).hasImportDeclaration("XYZ"); + + expect(hasImport).toBe(true); + }); + + it("returns false if an ImportDeclaration does not exist", function () { + const hasImport = + Collection.fromNodes(nodes).hasImportDeclaration("ABC"); + + expect(hasImport).toBe(false); + }); + }); + + describe("findImportDeclarations", function () { + it("lets us find ImportDeclarations by source path conveniently", function () { + const imports = + Collection.fromNodes(nodes).findImportDeclarations("XYZ"); + + expect(imports.length).toBe(1); + }); + + it("returns an empty ImportDeclarationCollection if no ImportDeclarations are found", function () { + const imports = + Collection.fromNodes(nodes).findImportDeclarations("Foo"); + + expect(imports.length).toBe(0); + }); + }); + + describe("renameImportDeclaration", function () { + it("renames an ImportDeclaration with the given sourcePath", function () { + Collection.fromNodes(nodes).renameImportDeclaration("XYZ", "ABC"); + + { + const imports = + Collection.fromNodes(nodes).findImportDeclarations("ABC"); + + expect(imports.length).toBe(1); + } + { + const imports = + Collection.fromNodes(nodes).findImportDeclarations("XYZ"); + expect(imports.length).toBe(0); + } + }); + + it("throws if sourcePath is not provided", function () { + expect(function () { + Collection.fromNodes(nodes).renameImportDeclaration(); + }).toThrow(); + }); + + it("throws if newSourcePath is not provided", function () { + expect(function () { + Collection.fromNodes(nodes).renameImportDeclaration("XYZ"); + }).toThrow(); + }); + }); + + describe("insertImportDeclaration", function () { + it("inserts an ImportDeclaration into the AST", function () { + Collection.fromNodes(nodes).insertImportDeclaration("@foo/bar", [ + b.importDefaultSpecifier(b.identifier("Foo")), + b.importSpecifier(b.identifier("ABC")), + b.importSpecifier(b.identifier("123")), + ]); + + const imports = + Collection.fromNodes(nodes).findImportDeclarations("@foo/bar"); + + expect(imports.length).toBe(1); + + const importSpecifiers = imports.paths()[0].value.specifiers; + expect(importSpecifiers.length).toBe(3); + }); + + it("does not insert duplicate ImportDeclarations", function () { + Collection.fromNodes(nodes).insertImportDeclaration("@foo/baz", [ + b.importDefaultSpecifier(b.identifier("Foo")), + b.importSpecifier(b.identifier("ABC")), + ]); + + Collection.fromNodes(nodes).insertImportDeclaration("@foo/baz", [ + b.importDefaultSpecifier(b.identifier("Foo")), + b.importSpecifier(b.identifier("ABC")), + ]); + + const imports = + Collection.fromNodes(nodes).findImportDeclarations("@foo/baz"); + + expect(imports.length).toBe(1); + }); + + it("throws if importDeclaration is not provided", function () { + expect(function () { + Collection.fromNodes(nodes).insertImportDeclaration(); + }).toThrow(); + }); + }); + }); +}); diff --git a/src/collections/index.js b/src/collections/index.js index 12c77008..e030770c 100644 --- a/src/collections/index.js +++ b/src/collections/index.js @@ -10,4 +10,5 @@ module.exports = { Node: require('./Node'), JSXElement: require('./JSXElement'), VariableDeclarator: require('./VariableDeclarator'), + ImportDeclaration: require('./ImportDeclaration'), };