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.
(click to expand)
- Install
- How it works
- Usage
- Options
- Examples
- Creating an Import Statement
- Basic ES6 Statement via createModule
- Basic CJS Statement via createModule
- Basic Dynamic Import Statement via createModule
- Manual Statement creation via addCode
- Creating an Import Statement, appended after another statement
- Creating an Import Statement, prepended before another statement
- Creating an Import Statement by replacing another statement
- Removing an Import Statement
- Moving an Import Statement (cut and paste)
- Modifying the module
- Addressing the (default) members
- Applying RegExp for module matching
- Using rawModule for module matching
- Creating an Import Statement
- General Hints
- Debugging
- License
Using npm:
npm install rollup-plugin-import-manager --save-dev
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.)
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.
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.
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.
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.
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).
Type: Boolean
Default: true
Set to false
to prevent displaying warning messages.
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:
Type: String
| Object
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. If the value is a String
the provided value will get searched anywhere in the module name. You can skip the extension or even bigger parts if you like and if this doesn't lead to multiple matches. However, if you need more control, you can always use a Regular Expression Object. (If you need access to the full path you should use the rawModule
matching method.)
Absolute imports are directly assigned as the name attribute. So, the following example can be matched with module: "bar"
import foo from "bar";
To match to an exact module name like react
but exclude react-table
for example, you can provide a RegExp
:
module: /^react$/
.
Also see this example of matching a module and changing it.
Type: String
| Object
Default: null
Selects a unit by its raw module name. rawModule
works exactly the same as module
. The only difference is, that is using the raw full module path. Consider the example from above:
import foo from "./path/bar.js";
The raw module stores the value "./path/bar.js"
including the quotation marks, which can be matched as shown before via String
or RegExp
. See this example.
If any other matching option is set, rawModule
gets ignored.
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.
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.
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: 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
Type: String
| Function
(when targeting the module)
Default: null
This option is used to rename a selected specific part (defaultMember
, member
, module
). The value is a string of the new name of the selected part.
Examples:
If the selected part is `module`, the value could alternatively be a function. The function must return a raw full module name (eg. `() => '"./new-module-name"'`) as a `String`. The original raw name is getting passed to the function and can be accessed by passing a variable to the function: `oldRawName => oldRawName.replace("foo", "bar")`.
When passing a function the modType
is getting ignored. Always make sure the return value includes quotation marks if the import statement requires it.
See this example.
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).
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.
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.
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.
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.
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:
import "foobar";
import "bar as pub" from "baz";
plugins: [
importManager({
units: {
file: "**/my-file.js",
createModule: "./path/to/foo.js",
actions: [
{
"select": "defaultMembers",
"add": "bar"
},
{
"select": "members",
"add": "baz as qux"
}
]
}
})
]
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.)
/**
* This is my description.
*/
const foobar = require("foobar");
plugins: [
importManager({
units: {
file: "**/my-file.js",
createModule: "./path/to/foo.js",
type: "cjs",
const: "foo",
insert: "top"
}
})
]
/**
* 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:
import "foobar";
import "bar as pub" from "baz";
plugins: [
importManager({
units: {
file: "**/my-file.js",
createModule: "./path/to/foo.js",
type: "dynamic",
let: "foo"
}
})
]
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.
import "bar as pub" from "baz";
const customImport = `
let foobar;
import("fs").then(fs => fs.readFileSync("./path/to/foobar.txt"));
`;
plugins: [
importManager({
units: {
file: "**/my-file.js",
addCode: customImport,
}
})
]
import "bar as pub" from "baz";
let foobar; // <--
import("fs").then(fs => foobar = fs.readFileSync("./path/to/foobar.txt")); // <--
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.
import { foo } from "bar";
plugins: [
importManager({
units: {
file: "**/my-file.js",
createModule: "./path/to/baz.js",
actions: {
"select": "defaultMembers",
"add": "* as qux"
},
append: {
module: "bar"
}
}
})
]
import { foo } from "bar";
import * as qux from "./path/to/baz.js"; // <--
import { foo } from "foobar";
plugins: [
importManager({
units: {
file: "**/my-file.js",
createModule: "./path/to/baz.js",
actions: {
"select": "defaultMembers",
"add": "* as qux"
},
prepend: {
module: "foobar"
}
}
})
]
import * as qux from "./path/to/baz.js"; // <--
import { foo } from "foobar";
import { foo } from "bar";
plugins: [
importManager({
units: {
file: "**/my-file.js",
createModule: "./path/to/baz.js",
actions: {
"select": "defaultMembers",
"add": "* as qux"
},
replace: {
module: "bar"
}
}
})
]
import * as qux from "./path/to/baz.js";
import { foo } from "bar";
import * as qux from "./path/to/baz.js";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "bar",
actions: [
{
remove: null,
}
]
}
})
]
import * as qux from "./path/to/baz.js";
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"
}
})
]
import "foobar";
import { foo } from "bar";
import baz from "quz";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "quz",
actions: "cut",
insert: "top"
}
})
]
import baz from "quz"; // <----
import "foobar"; // |
import { foo } from "bar"; // |
// -----------------------------
To change a module in any way, first it must be select
ed and then rename
ed, which can be fed with a String
or a Function
for module manipulation.
Changing a relative path to an absolute path (passing a String
to rename
)
In this example there is a relative path, that should be changed to a non relative module. This can be achieved like this:
import foo from "./path/to/bar.js";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "bar.js",
actions: {
select: "module",
rename: "bar"
}
}
})
]
import foo from "bar";
Changing a relative path to different directory (making use of a rename
function)
In this example there is a relative path, that should be changed to a sub-directory. This time a function is used for the goal, also a little help of an external function from path, which must be available (imported) in the rollup config file.
(keep in mind, that a function in rename
is only valid for modules)
import foo from "./path/to/bar.js";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "bar.js",
actions: {
select: "module",
rename: moduleSourceRaw => {
// Get rid of the quotes
const importPath = moduleSourceRaw.slice(1, -1);
// Parse the path into its parts (path must be imported for this example)
const importInfo = path.parse(importPath);
// Build the new import path with the sub-directory
const newPath = [importInfo.dir, "build-temp", importInfo.base].join("/");
// Remember to add quotes again
return `"${newPath}"`;
}
}
}
})
]
import foo from "./path/to/build-temp/bar.js";
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"
.
import foo from "bar";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "bar",
actions: {
select: "defaultMembers",
add: "* as baz"
}
}
})
]
import foo, * as baz from "bar";
Adding multiple members, again for the same example:
import foo from "bar";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "bar",
actions: {
select: "members",
add: [
"baz",
"qux"
]
}
}
})
]
import foo, { baz, qux } from "bar";
import { foo, bar, baz } from "qux";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "qux",
actions: {
select: "member",
name: "bar",
remove: null
}
}
})
]
import { foo, baz } from "qux";
import foo, { bar, baz } from "qux";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "qux",
actions: {
select: "members",
remove: null
}
}
})
]
import foo from "qux";
import foo from "bar";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "bar",
actions: {
select: "defaultMember",
name: "foo",
rename: "baz"
}
}
})
]
import baz from "bar";
By default the alias gets overwritten, but this can be prevented.
import { foo as bar } from "baz";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: "bar",
actions: {
select: "member",
name: "foo",
rename: "qux",
keepAlias: true
}
}
})
]
import { qux as bar } from "baz";
Aliases can also be addressed (set, renamed and removed). All possibilities demonstrated at once via chaining.
import { foo as bar, baz as qux, quux } from "quuz";
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)
import { foo, baz as corge, quux as grault } from "quuz";
Applying RegExp for module matching
This example demonstrates a case, where matching the module via a regular expression is necessary. Exemplary the first import statement of the following source code should be matched and removed. Searching for 'bar' or 'bar.js' is not an option, as this matches both statements. RegExp to the rescue:
import foo from "./path/to/bar.js";
import baz from "./path/to/foobar.js";
plugins: [
importManager({
units: {
file: "**/my-file.js",
module: /[^foo]bar\.js/,
actions: "remove"
}
})
]
import baz from "./path/to/foobar.js";
Using rawModule
for module matching
This example demonstrates a case, where it is desired to match a module by its raw module-name part.
(Be aware of the quotation marks, which are also part of the raw string in this case and might introduce problems when applying RegExp without taken them into account.)
import foo from "./path/to/bar.js";
import baz from "./path/to/foobar.js";
plugins: [
importManager({
units: {
file: "**/my-file.js",
rawModule: "./path/to/bar.js", // or eg. /\/bar.js/
actions: "remove"
}
})
]
import baz from "./path/to/foobar.js";
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.
import foo, { bar } from "baz";
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"
}
]
}
})
]
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.
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.
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.
plugins: [
importManager({
showDiff: null,
units: {
//...
}
})
]
This will log the performed changes to the console.
To visualize the properties of a specific file, it can help to stop the building process and throw a DebuggingError
.
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).
Also a single unit can be debugged. The keyword can be added to the existing list in an actions object.
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"
}
})
]
Copyright (c) 2022-2023, UmamiAppearance