Skip to content

Latest commit

 

History

History
1292 lines (1023 loc) · 43.1 KB

README.md

File metadata and controls

1292 lines (1023 loc) · 43.1 KB

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

(click to expand)

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 | 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.

rawModule [option for units]

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.

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 | 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.

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 pastemove 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";   // |
// -----------------------------

Modifying the module

To change a module in any way, first it must be selected and then renameed, 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:

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";

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)

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: 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}"`;
                }
            }
        }
    })
]
Bundle Code
import foo from "./path/to/build-temp/bar.js";

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";

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:

Source Code
import foo from "./path/to/bar.js";
import baz from "./path/to/foobar.js";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            module: /[^foo]bar\.js/,
            actions: "remove"
        }
    })
]
Bundle Code
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.)

Source Code
import foo from "./path/to/bar.js";
import baz from "./path/to/foobar.js";
Rollup Config
plugins: [
    importManager({
        units: {
            file: "**/my-file.js",
            rawModule: "./path/to/bar.js",     // or eg. /\/bar.js/
            actions: "remove"
        }
    })
]
Bundle Code
import baz from "./path/to/foobar.js";

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