tree-shake-import-namespaces
Tree-shake import namespaces with oxc.
This is a set of plugins that helps you to use TypeScript namespaces, ambient namespaces, barrel files, or modules with many named exports by tree-shaking and rewiring their import declarations into direct imports:
import { User } from "#utils";
const userEmail = User.email(user);
import { email } from "foo/bar/utils/User/email";
const userEmail = email(user);
Features
- Fast, uses the Oxidation Compiler under the hood
- Supports default, named and wildcard imports, including mixed import types
- Allows you to rewire or skip each individual imported module
- No hardcoded assumptions about your file structure, you can roll your own module resolution
- Optionally supports recursive destructuring for nested namespaces
- Supports JSX
- Sourcemap generation
- Unplugin for convenient integration with modern bundlers
How it works
Given the example:
import { User } from "#utils";
const userEmail = User.email(user);
First, this plugin will collect import declarations (import { User } from "#utils";
), member expressions bound to these imports (User.email
) and their scope information.
This information will be forwarded to a function provided by you:
treeshake(code, "source.ts", {
resolveImport({
filePath = "source.ts",
importName = "User",
importPath = "#utils",
localName = "User",
propertyName = "email",
}) {},
});
You can use this to declare a new import statement, for example:
treeshake(code, "source.ts", {
resolveImport({ propertyName, importPath }) {
// import { email } from "#utils/email"
return `import { ${propertyName} } from "${importPath}/${propertyName}";`;
},
});
The plugin will extract the desired import name, in this example email
.
Following that, the import declarations are rewritten as necessary - in this case replacing the entire import:
import { User } from "#utils";
import { email } from "#utils/email";
Then all relevant member expressions are replaced with the new import name:
const userEmail = User.email(user);
const userEmail = email(user);
Unique import names
In the above example, it is easy to declare an import statement that results with naming conflicts:
import { User } from "#utils";
const email = User.email(user);
import { email } from "#utils/email";
const email = email(user);
The plugin will generate an import name (importAlias
) that is ensured to be unique across all relevant scopes as a suggestion:
treeshake(code, "source.ts", {
resolveImport({ importAlias, propertyName, importPath }) {
// import { email as _email } from "#utils/email"
return `import { ${propertyName} as ${importAlias} } from "${importPath}/${propertyName}";`;
},
});
import { email as _email } from "#utils/email";
const email = _email(user);
And alternatively you can access all used bindings to generate your own if you'd like:
treeshake(code, "source.ts", {
resolveImport({ scopes, propertyName, importPath }) {
let name = propertyName;
while (scopes.has(name)) name += "$";
// import { email as email$ } from "#utils/email"
return `import { ${propertyName} as ${name} } from "${importPath}/${propertyName}";`;
},
});
import { email as email$ } from "#utils/email";
const email = email$(user);
tree-shake-import-namespaces
Installation
npm install -D @monstermann/tree-shake-import-namespaces
pnpm -D add @monstermann/tree-shake-import-namespaces
yarn -D add @monstermann/tree-shake-import-namespaces
bun -D add @monstermann/tree-shake-import-namespaces
Usage
import treeshake from "@monstermann/tree-shake-import-namespaces";
const result = treeshake(code, filePath, options);
// `undefined` if no changes to the code have been made
if (!result) return;
// Transformed code
result.code;
// Sourcemap if needed
result.map;
Options
import treeshake from "@monstermann/tree-shake-import-namespaces";
interface TreeShakeImportData {
// The path of the file being transformed.
filePath: string;
// The generated alias name that is safe to use for the new import.
importAlias: string;
// Bindings that are already in use, useful if
// you want to create your own import aliases.
scope: Set<string>;
// The imported module name, if available:
// import { Foo } from "foo" → "Foo"
// import { Foo as Bar } from "foo" → "Foo"
// import * as Bar from "foo" → "*"
// import Foo from "foo" → undefined
importName: string | undefined;
// The path of the imported module:
// import { Foo } from "foo" → "foo"
importPath: string;
// The local alias of the imported module:
// import { Foo } from "foo" → "Foo"
// import { Foo as Bar } from "foo" → "Bar"
// import * as Bar from "foo" → "Bar"
// import Foo from "foo" → "Foo"
localName: string;
// The property that was used in the member expression:
// Foo.bar; → "bar"
propertyName: string;
}
treeshake(code, filePath, {
// Print detailed information to stdout if needed:
debug?: true,
// Enable destructuring nested properties if needed:
nested?: true,
resolveImport(importData: TreeShakeImportData) {
// Skip tree-shaking this import:
return false;
return null;
return undefined;
// Tree-shake this import by returning an import declaration
// that should be injected - make sure you use `importAlias`!
return `
import { ${propertyName} as ${importAlias} }
from "${importPath}/${propertyName}";
`;
},
});
unplugin-tree-shake-import-namespaces
Installation
npm install -D @monstermann/unplugin-tree-shake-import-namespaces
pnpm -D add @monstermann/unplugin-tree-shake-import-namespaces
yarn -D add @monstermann/unplugin-tree-shake-import-namespaces
bun -D add @monstermann/unplugin-tree-shake-import-namespaces
Usage
// vite.config.ts
import treeshake from "@monstermann/unplugin-tree-shake-import-namespaces/vite";
export default defineConfig({
plugins: [treeshake(options)],
});
// rollup.config.js
import treeshake from "@monstermann/unplugin-tree-shake-import-namespaces/rollup";
export default {
plugins: [treeshake(options)],
};
// rolldown.config.js
import treeshake from "@monstermann/unplugin-tree-shake-import-namespaces/rolldown";
export default {
plugins: [treeshake(options)],
};
// webpack.config.js
module.exports = {
plugins: [
require("@monstermann/unplugin-tree-shake-import-namespaces/webpack")(
options,
),
],
};
// rspack.config.js
module.exports = {
plugins: [
require("@monstermann/unplugin-tree-shake-import-namespaces/rspack")(
options,
),
],
};
// esbuild.config.js
import { build } from "esbuild";
import treeshake from "@monstermann/unplugin-tree-shake-import-namespaces/esbuild";
build({
plugins: [treeshake(options)],
});
Options
import treeshake from "@monstermann/unplugin-tree-shake-import-namespaces";
interface TreeShakeImportData {
// The path of the file being transformed.
filePath: string;
// The generated alias name that is safe to use for the new import.
importAlias: string;
// Bindings that are already in use, useful if
// you want to create your own import aliases.
scope: Set<string>;
// The imported module name, if available:
// import { Foo } from "foo" → "Foo"
// import { Foo as Bar } from "foo" → "Foo"
// import * as Bar from "foo" → "*"
// import Foo from "foo" → undefined
importName: string | undefined;
// The path of the imported module:
// import { Foo } from "foo" → "foo"
importPath: string;
// The local alias of the imported module:
// import { Foo } from "foo" → "Foo"
// import { Foo as Bar } from "foo" → "Bar"
// import * as Bar from "foo" → "Bar"
// import Foo from "foo" → "Foo"
localName: string;
// The property that was used in the member expression:
// Foo.bar; → "bar"
propertyName: string;
}
treeshake({
// Print detailed information to stdout if needed:
// Strings and RegExps can be used to match file paths.
debug?: Boolean | String | RegExp | Array[...String | RegExp],
// Enable destructuring nested properties if needed:
// Strings and RegExps can be used to match file paths.
nested?: Boolean | String | RegExp | Array[...String | RegExp],
// Specify which file paths to include when transforming:
include?: String | RegExp | Array[...String | RegExp],
// Specify which file paths to exclude when transforming:
exclude?: String | RegExp | Array[...String | RegExp],
// Enforce plugin order for bundlers that support this:
enforce?: "post" | "pre" | undefined,
resolveImport(importData: TreeShakeImportData) {
// Skip tree-shaking this import:
return false;
return null;
return undefined;
// Tree-shake this import by returning an import declaration
// that should be injected - make sure you use `importAlias`!
return `
import { ${propertyName} as ${importAlias} }
from "${importPath}/${propertyName}";
`;
},
});
Tips
oxc-resolver
This plugin will report imported paths as-is.
If you need to work with resolved module paths, eg.:
import { Foo } from "#utils/Foo";
{
"compilerOptions": {
"paths": {
"#utils/*": ["./src/utils/*"]
}
}
}
And you would like to resolve #utils/Foo
to $PWD/src/utils/Foo
, you can consider giving oxc-resolver a try.