Skip to content

A rollup plugin to add, modify, and remove imports (cjs/es6/dynamic)

License

Notifications You must be signed in to change notification settings

tjcouch-sil/rollup-plugin-import-manager

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rollup-plugin-import-manager

License npm

A Rollup plugin which makes it possible to manipulate import statements. Features are deleting, adding, changing the members and modules and much more. Supports ES6 Import Statements, CommonJS and Dynamic Imports.

Table of Contents

Install

Using npm:

npm install rollup-plugin-import-manager --save-dev

How it works

rollup-plugin-import-manager analyzes each file (which is used for the rollup building process) for import statements. Those are collected as so called unit objects, on which the user can interact with. Also the creation of new units → import statements is possible.

(The actual work is done by the outsourced program ImportManager which can by used independently from this rollup-plugin.)

Usage

Create a rollup.config.js configuration file and import the plugin.

import { importManager } from "rollup-plugin-import-manager";

export default {
    input: "src/index.js",
    output: {   
        format: "es",
        name: "myBuild",
        file: "./dist/build.js",
    },
    plugins: [
        importManager({
            units: [
                {
                    file: "**/my-file.js",
                    module: "my-module",
                    actions: [
                        // ...
                    ]
                }
            ]
        })
    ]
}

Then call rollup either via the CLI or the API.

Options

include

Type: String | Array[...String]
Default: null

A minimatch pattern, or array of patterns, which specifies the files in the build the plugin should operate on. By default all files are targeted. On top of that each unit has the possibility to target a specific file.

exclude

Type: String | Array[...String]
Default: null

A minimatch pattern, or array of patterns, which specifies the files in the build the plugin should ignore. By default no files are ignored.

showDiff

Type: String
Default: null

A debugging method. If set to anything other than the string "file" a console output of diff is shown. It is modified a little and looks much like the default output of diff from the GNU diffutils, with colors on top. If set to "file" the whole file with insertions and deletions is shown. Either way it only gets logged if there are any changes at all. If this is not the case, there is another (now following) global debugging method available.

debug

Type: String
Default: null

A debugging method. If more than one source file is involved, this really only is useful in combination with include. It stops the building process by throwing an intentional error and lists all units of the first file, that is getting processed. Even more verbose information about all unit objects can be made accessible by passing the strings verbose, object(s) or import(s) (which one to use doesn't matter).

warnings

Type: Boolean
Default: true

Set to false to prevent displaying warning messages.

units

Type: Object | Array[...Object]
Default: null

This is where the plugin comes to life. Here is the place where units are getting selected, created or removed. It has several options by itself. Units are objects, for multiple units pass an array of objects:


module [option for units]

Type: String
Default: null

Selects a unit by its module name. Each import has a name object. This is constructed from the module. Path information are getting removed. Consider this basic es6 import statement:

import foo from "./path/bar.js";

The corresponding unit assigns the module name bar.js which can be matched with: module: "bar.js"
(The matching method is actually a little more generous. You can skip the extension or even bigger parts if you like and if this doesn't lead to multiple matches).

Absolute imports are directly assigned as the name attribute. So, the following example can be matched with module: "bar"

import foo from "bar";

Also see this example of matching a module and changing it.

hash [option for units]

Type: String
Default: null

Selects a unit by its hash. If - for any reason - it is not possible to match via the module name, this is an alternative. If for instance multiple matches are found, by selecting via module, an error is thrown and the corresponding hashes are logged to the console. Also by running a global debugging, the hash can be found.

The hash is generated by the module name, its members and also the filename. If the filename or any of the other properties are changing so is the hash. So, if a module is selected via hash and any of the properties are changed, the build will fail afterwards as the hash is no longer existent. This is why the matching via module name should be preferred.

If the hash option is set, the module option will get ignored.

id [option for units]

Type: Number
Default: null

Internally every unit gets an Id. There are different scopes for the generation:

type scope
es6 1000
dynamic 2000
cjs 3000

The first ES6 Import statement of a file will have the Id 1000, the second 1001 and so forth. For a quick test, you can select via Id (if the filename is specified). But actually this is only an internal method to locate the statements. Testing is the only other reason to use it. If the order or number of import statements changes, this will directly affect the Ids. This selection method should therefore never been used in production.

If the Id option is set, hash and module will get ignored.

file [option for units]

Type: String
Default: null

A minimatch pattern, which specifies the file where the unit is located.

It is always a good idea to set it, even if the files are already limited by include or exclude. The reason for this is, that a the unit is expected to be in the specified file, if the value is set and an error is thrown if it doesn't match. Otherwise it will simply be ignored, if a match is not there.

Also for unit creation this is almost always critical. If there are multiple source files, and no file is specified, the fresh import statement will get created in any file, that is processed (and this is most probably not what you want).

However, it is not mandatory to set it.

type [option for units]

Type: String
Default: null

A possibility to specify the unit type. Valid parameters are:

  • es6
  • cjs
  • dynamic

This argument is mainly necessary when creating new units. Without members or default members the type cannot be guessed and needs to be specified (see this example). But the argument can also be helpful for selecting modules, if there are overlapping matches across the types. For example if es6 and dynamic import share the same module name.

createModule [option for units]

Type: String
Default: null

Creates a new module. Every selection method (id, hash, module) will get ignored if this key is passed to a unit. Set the module (path) as the value (eg: createModule: "./path/to/my-module.js"). The fresh module can be inserted into the code, appended or prepended to another unit or it can replace one. There are examples available for any of the three statement-types.

addCode [option for units]

Type: String
Default: null

This is the manual version of createModule. The value can be any code, provided as a string, which gets inserted into the code, appended or prepended to another unit or it can replace one. This can typically be a manually created import statement or a small function, which replaces an import, but this is completely up to you. See this example.

insert [option for units]

Type: String
Default: "bottom"

Additional parameter for createModule/addCode. This is a very basic approach, to add the import statement. Setting it to "top" will append the statement on top of the file, directly after the the description if present.

If set to "bottom", the new statements gets inserted after the last found import statement same type. Dynamic imports also orient themselves to es6 imports, except none is found. If no statement is found at all it falls back to "top" insertion. See the examples for import creation.

append [option for units]

Type: Object
Default: null

Additional parameter for createModule/addCode. Instead of inserting a fresh statement at the top or bottom of the other statements, appending inserts it it after another import statement. This works by passing a unit as a value. Example.

prepend [option for units]

Type: Object
Default: null

Additional parameter for createModule/addCode. Instead of inserting a fresh statement at the top or bottom of the other statements, prepending inserts it it before another import statement. This works by passing a unit as a value. Example.

replace [option for units]

Type: Object
Default: null

Additional parameter for createModule/addCode. Instead of somehow adding it around another unit, this keyword replaces the according import statement, which is also passed as a unit object. Example.

const [option for units]

Type: String
Default: null

Additional parameter for createModule. Only has an effect if cjs or dynamic modules are getting created. const is the declarator type, the value is the variable name for the import.

let [option for units]

Type: String
Default: null

Additional parameter for createModule. Only has an effect if cjs or dynamic modules are getting created. let is the declarator type, the value is the variable name for the import.

var [option for units]

Type: String
Default: null

Additional parameter for createModule. Only has an effect if cjs or dynamic modules are getting created. var is the declarator type, the value is the variable name for the import.

global [option for units]

Type: String
Default: null

Additional parameter for createModule. Only has an effect if cjs or dynamic modules are getting created. If global is set, there is no declarator type and the variable should be declared before this statement. The value is the variable name for the import.

actions [option for units]

Type: Object | Array[...Object]
Default: null

This is the place where the actual manipulation of a unit (and ultimately a statement) is taking place. Several actions/options can be passed, for a singular option, use an object for multiple an array of objects:


debug [option for actions]

Type: Any
Default: null

A debugging method for a specific unit. This also throws an intentional debugging error, which stops the building process. Verbose information about the specific unit are logged to the console. The value is irrelevant. If this is the only action it can be passed as a string: actions: "debug". See this example.

select [option for actions]

Type: String
Default: null

Select the part you like to modify. This can be specific part (which also needs the option name to be passed):

Or the groups (example):

  • defaultMembers
  • members

Common JS and dynamic imports only have the module available to select.

name [option for actions]

Type: String
Default: null

For the selection of a specific part (defaultMember or member) the name needs to be specified. The name is directly related to the name of a member or default member (without its alias if present).
A member part of { foobar as foo, baz } can be selected with name: "foobar" and name: "baz". See this example.

alias [option for actions]

Type: String
Default: null

An option to target an alias of a selected defaultMember or member. If a value is set, this will change or initially set the alias. Aliases for members can also be removed, by using the remove option (in this case the value for alias will be ignored) and/or by passing null as a value. Examples.

rename [option for actions]

Type: String
Default: null

This option is used to rename a selected specific part (defaultMember, member, module). The value is the new name of the selected part. See this example.

modType [option for actions]

Type: String
Default: "string"|"raw"

If renaming is done with modType "string" there are quotation marks set around the input by default, mode "raw" is not doing that. This can be useful for replacing the module by anything other than a string (which is only valid for cjs and dynamic imports). By default the modType is defined by the existing statement. If it is not a string, type raw is assumed (those are rare occasions).

keepAlias [option for actions]

Type: Boolean
Default: false

This is an extra argument to rename a (default) member. If true, the alias will kept untouched, otherwise it gets overwritten in the renaming process, wether a new alias is set or not. Example.

remove [option for actions]

Type: Any
Default: null

When no part is selected, this removes the entire unit → import statement. The value is irrelevant. If this is the only action it can be passed as a string: actions: "remove". If a part is selected (defaultMembers, members, module or alias) only the according (most specific) part is getting removed. See e.g. this example.

add [option for actions]

Type: String | Array[...String] Default: null

An additional parameter for defaultMembers or members. It adds one or multiple (default) members to the existing ones. The group has to be selected for the add keyword to have an effect. Example.

cut [option for actions]

Type: Any
Default: null

cut and paste → move a unit. Actually it removes an import statement and passes its code snippet to addCode. Therefore a unit with this action, accepts the additional parameters (insert, append, prepend, replace). Example.

Examples

Creating an Import Statement

There are a few options on how to create new import statements. The createModule is working a lot like the the methods for selecting existing statements.

Basic ES6 Statement via createModule

Without specifying insert or append/prepend the following import statement is getting inserted after the last import statement:

Source Code
import "foobar";
import "bar as pub" from "baz";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            createModule: "./path/to/foo.js", 
            actions: [
                {
                    "select": "defaultMembers",
                    "add": "bar"
                },
                {
                    "select": "members",
                    "add": "baz as qux"
                }
            ]
        }
    })
]
Bundle Code
import "foobar";
import bar as pub from "baz";
import bar, { baz as qux } from "./path/to/foo.js"; // <--

Basic CJS Statement via createModule

CJS Imports are also supported. But this time the type needs to be specified. Also a variable name has to be set. In this example the const foo. (Other declaration types are: let, var and global).

(This time the import should be placed at the very top of the file. Therefore insert: "top" gets additionally added to the config file.)

Source Code
/**
 * This is my description.
 */

const foobar = require("foobar");
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            createModule: "./path/to/foo.js", 
            type: "cjs",
            const: "foo",
            insert: "top"
        }
    })
]
Bundle Code
/**
 * This is my description.
 */

const foo = require("./path/to/foo.js"); // <--
const foobar = require("foobar");

Basic Dynamic Import Statement via createModule

Almost exactly the same (only the type differs) goes for dynamic imports:

Source Code
import "foobar";
import "bar as pub" from "baz";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            createModule: "./path/to/foo.js", 
            type: "dynamic",
            let: "foo"
        }
    })
]
Bundle Code
import "foobar";
import "bar as pub" from "baz";
let foo = await import("./path/to/foo.js");  // <--

Manual Statement creation via addCode

If this is all to much predetermination, the addCode method is a very handy feature. It allows to inject a string containing the code snippet (most likely an import statement). Which is very different but behaves exactly the same in other regards (inserting, appending/prepending, replacing).

The addCode value can contain any code you like. You probably should not get too creative. It is designed to add import statements or other short code chunks and it gets appended to existing statements.

Source Code
import "bar as pub" from "baz";
Rollup Config
const customImport = `
let foobar;
import("fs").then(fs => fs.readFileSync("./path/to/foobar.txt"));
`;

plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            addCode: customImport,
        }
    })
]
Bundle Code
import "bar as pub" from "baz";
let foobar;                                                                // <--
import("fs").then(fs => foobar = fs.readFileSync("./path/to/foobar.txt")); // <--

Creating an Import Statement, appended after another statement:

So far statements where created, but they were always appended to the import list or added on top of the file. Now it should be demonstrated how new statements can be appended to any available import statement.

Source Code
import { foo } from "bar";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            createModule: "./path/to/baz.js", 
            actions: {
                "select": "defaultMembers",
                "add": "* as qux"
            },
            append: {
                module: "bar"
            }
        }
    })
]
Bundle Code
import { foo } from "bar";
import * as qux from "./path/to/baz.js"; // <--

Creating an Import Statement, prepended before another statement:

Source Code
import { foo } from "foobar";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            createModule: "./path/to/baz.js", 
            actions: {
                "select": "defaultMembers",
                "add": "* as qux"
            },
            prepend: {
                module: "foobar"
            }
        }
    })
]
Bundle Code
import * as qux from "./path/to/baz.js"; // <--
import { foo } from "foobar";

Creating an Import Statement by replacing another statement:

Source Code
import { foo } from "bar";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            createModule: "./path/to/baz.js", 
            actions: {
                "select": "defaultMembers",
                "add": "* as qux"
            },
            replace: {
                module: "bar"
            }
        }
    })
]
Bundle Code
import * as qux from "./path/to/baz.js";

Removing an Import Statement

Source Code
import { foo } from "bar";
import * as qux from "./path/to/baz.js";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar",
            actions: [
                {
                    remove: null,
                }
            ]
        }
    })
]
Bundle Code
import * as qux from "./path/to/baz.js";

Shorthand Method

The above example can be shortened by a lot as the removal is the only action and the value is not relevant.

plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar",
            actions: "remove"
        }
    })
]

Moving an Import Statement (cut and paste):

Source Code
import "foobar";
import { foo } from "bar";
import baz from "quz";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "quz", 
            actions: "cut",
            insert: "top"
        }
    })
]
Bundle Code
import baz from "quz";  // <----
import "foobar";             // |
import { foo } from "bar";   // |
// -----------------------------

Changing the module

In this example there is a relative path that should be changed to a non relative module. This can be achieved like this:

Source Code
import foo from "./path/to/bar.js";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar.js",
            actions: {
                select: "module",
                rename: "bar"
            }
        }
    })
]
Bundle Code
import foo from "bar";

Addressing the (default) members

defaultMembers and members are using the exact same methods. It is only important to keep in mind to address default members with select: "defaultMembers" or for a specific one select: "defaultMember"; for members select: "members" and select: "member".

Adding a defaultMember

Source Code
import foo from "bar";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar",
            actions: {
                select: "defaultMembers",
                add: "* as baz"
            }
        }
    })
]
Bundle Code
import foo, * as baz from "bar";

Adding multiple members, again for the same example:

Source Code
import foo from "bar";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar",
            actions: {
                select: "members",
                add: [
                    "baz",
                    "qux"
                ]
            }
        }
    })
]
Bundle Code
import foo, { baz, qux } from "bar";

Removing a member

Source Code
import { foo, bar, baz } from "qux";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "qux",
            actions: {
                select: "member",
                name: "bar",
                remove: null
            }
        }
    })
]
Bundle Code
import { foo, baz } from "qux";

Removing a group of members

Source Code
import foo, { bar, baz } from "qux";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "qux",
            actions: {
                select: "members",
                remove: null
            }
        }
    })
]
Bundle Code
import foo from "qux";

Changing a defaultMember name

Source Code
import foo from "bar";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar",
            actions: {
                select: "defaultMember",
                name: "foo",
                rename: "baz"
            }
        }
    })
]
Bundle Code
import baz from "bar";

Renaming but keeping the alias

By default the alias gets overwritten, but this can be prevented.

Source Code
import { foo as bar } from "baz";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar",
            actions: {
                select: "member",
                name: "foo",
                rename: "qux",
                keepAlias: true
            }
        }
    })
]
Bundle Code
import { qux as bar } from "baz";

Addressing an alias

Aliases can also be addressed (set, renamed and removed). All possibilities demonstrated at once via chaining.

Source Code
import { foo as bar, baz as qux, quux } from "quuz";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "bar",
            actions: [
                {
                    select: "member",
                    name: "foo",
                    alias: null,
                    remove: null // redundant **
                },
                {
                    select: "member",
                    name: "baz",
                    alias: "corge"
                },
                {
                    select: "member",
                    name: "quux",
                    alias: "grault"
                },
            ]
        }
    })
]

// ** remove can be set, but if the alias
//    is null, this is redundant
//    (the option is only there to keep the
//    method syntactically consistent)
Bundle Code
import { foo, baz as corge, quux as grault } from "quuz";

General Hints

Chaining

It is possible to address every part of a statement in one go. The order usually doesn't matter. But one part should not be selected twice, which might produce unwanted results. To address every part of a unit with its actions can be as complex as follows.

Source Code
import foo, { bar } from "baz";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "baz", 
            actions: [
                {
                    select: "defaultMember",
                    name: "foo",
                    remove: null
                },
                {
                    select: "defaultMembers",
                    add: "qux"
                },
                {
                    select: "member",
                    name: "bar",
                    alias: "quux"
                },
                {
                    select: "members",
                    add: [
                        "quuz",
                        "corge"
                    ] 
                },
                {
                    select: "module",
                    rename: "grault"
                }
            ]
        }
    })
]
Bundle Code
import qux, { bar as quux, quuz, corge } from "grault";

This is in no way an efficient, but an example to show the complexity modifications are allowed to have.

Array and Object shortening

As a general rule, all arrays can be unpacked if only one member is inside. Objects with meaningless values, can be passed as a string, if syntactically allowed. An example is shown here.

Debugging

Show Diff

A general hint while creating a rollup.config.js configuration file: it is useful to enable diff logging to see how the source file is actually getting manipulated.

Rollup Config
plugins: [
    importManager({
        showDiff: null,
        units: {
            //...
        }
    })
]

This will log the performed changes to the console.

Debugging Files

To visualize the properties of a specific file, it can help to stop the building process and throw a DebuggingError.

Rollup Config
plugins: [
    importManager({
        include: "**/my-file.js"
        debug: null,
        units: {
            //...
        }
    })
]

Or more verbose:

plugins: [
    importManager({
        include: "**/my-file.js"
        debug: "verbose",
        units: {
            //...
        }
    })
]

In both cases the include keyword is also passed. Otherwise the debug key would make the build process stop at the very first file it touches (if there is only one file involved at all, it is not necessary to pass it).

Debugging Units

Also a single unit can be debugged. The keyword can be added to the existing list in an actions object.

Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "foo",
            actions: {
                select: "defaultMember",
                name: "foo",
                rename: "baz"
                debug: null
            }
        }
    })
]

Or as a shorthand, if it is the only option:

plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: "foo",
            actions: "debug"
        }
    })
]

License

MIT

Copyright (c) 2022-2023, UmamiAppearance

About

A rollup plugin to add, modify, and remove imports (cjs/es6/dynamic)

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 100.0%