diff --git a/docs/advanced-features/compiler.md b/docs/advanced-features/compiler.md
index 16d415a0a11d9..35acc704d1bad 100644
--- a/docs/advanced-features/compiler.md
+++ b/docs/advanced-features/compiler.md
@@ -233,6 +233,89 @@ module.exports = {
If you have feedback about `swcMinify`, please share it on the [feedback discussion](https://github.com/vercel/next.js/discussions/30237).
+### Modularize Imports
+
+Allows to modularize imports, similar to [babel-plugin-transform-imports](https://www.npmjs.com/package/babel-plugin-transform-imports).
+
+Transforms member style imports:
+
+```js
+import { Row, Grid as MyGrid } from 'react-bootstrap'
+import { merge } from 'lodash'
+```
+
+...into default style imports:
+
+```js
+import Row from 'react-bootstrap/lib/Row'
+import MyGrid from 'react-bootstrap/lib/Grid'
+import merge from 'lodash/merge'
+```
+
+Config for the above transform:
+
+```js
+// next.config.js
+module.exports = {
+ experimental: {
+ modularizeImports: {
+ 'react-bootstrap': {
+ transform: 'react-bootstrap/lib/{{member}}',
+ },
+ lodash: {
+ transform: 'lodash/{{member}}',
+ },
+ },
+ },
+}
+```
+
+Advanced transformations:
+
+- Using regular expressions
+
+Similar to `babel-plugin-transform-imports`, but the transform is templated with [handlebars](https://docs.rs/handlebars) and regular expressions are in Rust [regex](https://docs.rs/regex/latest/regex/) crate's syntax.
+
+The config:
+
+```js
+// next.config.js
+module.exports = {
+ experimental: {
+ modularizeImports: {
+ 'my-library/?(((\\w*)?/?)*)': {
+ transform: 'my-library/{{ matches.[1] }}/{{member}}',
+ },
+ },
+ },
+}
+```
+
+Cause this code:
+
+```js
+import { MyModule } from 'my-library'
+import { App } from 'my-library/components'
+import { Header, Footer } from 'my-library/components/App'
+```
+
+To become:
+
+```js
+import MyModule from 'my-library/MyModule'
+import App from 'my-library/components/App'
+import Header from 'my-library/components/App/Header'
+import Footer from 'my-library/components/App/Footer'
+```
+
+- Handlebars templating
+
+This transform uses [handlebars](https://docs.rs/handlebars) to template the replacement import path in the `transform` field. These variables and helper functions are available:
+
+1. `matches`: Has type `string[]`. All groups matched by the regular expression. `matches.[0]` is the full match.
+2. `member`: Has type `string`. The name of the member import.
+3. `lowerCase`, `upperCase`, `camelCase`: Helper functions to convert a string to lower, upper or camel cases.
+
## Unsupported Features
When your application has a `.babelrc` file, Next.js will automatically fall back to using Babel for transforming individual files. This ensures backwards compatibility with existing applications that leverage custom Babel plugins.
diff --git a/examples/modularize-imports/.gitignore b/examples/modularize-imports/.gitignore
new file mode 100644
index 0000000000000..1437c53f70bc2
--- /dev/null
+++ b/examples/modularize-imports/.gitignore
@@ -0,0 +1,34 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# vercel
+.vercel
diff --git a/examples/modularize-imports/README.md b/examples/modularize-imports/README.md
new file mode 100644
index 0000000000000..c80cdb596798b
--- /dev/null
+++ b/examples/modularize-imports/README.md
@@ -0,0 +1,27 @@
+# Modularize Imports Example
+
+This example shows how to use the `modularizeImports` config option.
+
+## Preview
+
+Preview the example live on [StackBlitz](http://stackblitz.com/):
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/modularize-imports)
+
+## Deploy your own
+
+Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
+
+[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/modularize-imports&project-name=modularize-imports&repository-name=modularize-imports)
+
+## How to use
+
+Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
+
+```bash
+npx create-next-app --example modularize-imports modularize-imports-app
+# or
+yarn create next-app --example modularize-imports modularize-imports-app
+```
+
+Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
diff --git a/examples/modularize-imports/components/halves/LeftHalf.js b/examples/modularize-imports/components/halves/LeftHalf.js
new file mode 100644
index 0000000000000..5f96fe8a0d9a5
--- /dev/null
+++ b/examples/modularize-imports/components/halves/LeftHalf.js
@@ -0,0 +1,3 @@
+export default function LeftHalf() {
+ return Modularize
+}
diff --git a/examples/modularize-imports/components/halves/RightHalf.js b/examples/modularize-imports/components/halves/RightHalf.js
new file mode 100644
index 0000000000000..b1b5d5e0d3b4f
--- /dev/null
+++ b/examples/modularize-imports/components/halves/RightHalf.js
@@ -0,0 +1,3 @@
+export default function RightHalf() {
+ return Imports
+}
diff --git a/examples/modularize-imports/components/halves/index.js b/examples/modularize-imports/components/halves/index.js
new file mode 100644
index 0000000000000..b542f3266a396
--- /dev/null
+++ b/examples/modularize-imports/components/halves/index.js
@@ -0,0 +1,5 @@
+// import LeftHalf from './LeftHalf'
+// import RightHalf from './RightHalf'
+
+// Remove the exports here so that we can verify that `modularize-imports` is working.
+// export { LeftHalf, RightHalf };
diff --git a/examples/modularize-imports/next.config.js b/examples/modularize-imports/next.config.js
new file mode 100644
index 0000000000000..55be9582d2f9b
--- /dev/null
+++ b/examples/modularize-imports/next.config.js
@@ -0,0 +1,9 @@
+module.exports = {
+ experimental: {
+ modularizeImports: {
+ '../components/halves': {
+ transform: '../components/halves/{{ member }}',
+ },
+ },
+ },
+}
diff --git a/examples/modularize-imports/package.json b/examples/modularize-imports/package.json
new file mode 100644
index 0000000000000..f9170ae254fa3
--- /dev/null
+++ b/examples/modularize-imports/package.json
@@ -0,0 +1,13 @@
+{
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "latest",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
+ }
+}
diff --git a/examples/modularize-imports/pages/index.js b/examples/modularize-imports/pages/index.js
new file mode 100644
index 0000000000000..ac543f0494121
--- /dev/null
+++ b/examples/modularize-imports/pages/index.js
@@ -0,0 +1,10 @@
+import { LeftHalf, RightHalf } from '../components/halves'
+
+const Index = () => (
+
+
+
+
+)
+
+export default Index
diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock
index e42eee1bc7c04..68ff4a132b49b 100644
--- a/packages/next-swc/Cargo.lock
+++ b/packages/next-swc/Cargo.lock
@@ -160,13 +160,34 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "block-buffer"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
+dependencies = [
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array 0.12.4",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+dependencies = [
+ "byte-tools",
]
[[package]]
@@ -199,6 +220,12 @@ version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
+[[package]]
+name = "byte-tools"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+
[[package]]
name = "byteorder"
version = "1.4.3"
@@ -331,7 +358,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
- "generic-array",
+ "generic-array 0.14.5",
"typenum",
]
@@ -428,13 +455,22 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array 0.12.4",
+]
+
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
- "block-buffer",
+ "block-buffer 0.10.2",
"crypto-common",
]
@@ -462,6 +498,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
[[package]]
name = "fastrand"
version = "1.7.0"
@@ -514,6 +556,15 @@ dependencies = [
"byteorder",
]
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
[[package]]
name = "generic-array"
version = "0.14.5"
@@ -558,6 +609,20 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+[[package]]
+name = "handlebars"
+version = "4.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d6a30320f094710245150395bc763ad23128d6a1ebbad7594dc4164b62c56b"
+dependencies = [
+ "log",
+ "pest",
+ "pest_derive",
+ "quick-error",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "hashbrown"
version = "0.11.2"
@@ -775,6 +840,12 @@ dependencies = [
"hashbrown",
]
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
[[package]]
name = "matchers"
version = "0.1.0"
@@ -892,6 +963,7 @@ dependencies = [
"easy-error",
"either",
"fxhash",
+ "handlebars",
"once_cell",
"pathdiff",
"radix_fmt",
@@ -901,6 +973,7 @@ dependencies = [
"styled_components",
"swc",
"swc_atoms",
+ "swc_cached",
"swc_common",
"swc_css",
"swc_ecma_loader",
@@ -1012,6 +1085,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+[[package]]
+name = "opaque-debug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+
[[package]]
name = "ordered-float"
version = "2.10.0"
@@ -1097,6 +1176,49 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
+dependencies = [
+ "maplit",
+ "pest",
+ "sha-1 0.8.2",
+]
+
[[package]]
name = "petgraph"
version = "0.6.0"
@@ -1267,6 +1389,12 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
[[package]]
name = "quote"
version = "1.0.15"
@@ -1565,6 +1693,18 @@ dependencies = [
"serde",
]
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer 0.7.3",
+ "digest 0.8.1",
+ "fake-simd",
+ "opaque-debug",
+]
+
[[package]]
name = "sha-1"
version = "0.10.0"
@@ -1573,7 +1713,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
- "digest",
+ "digest 0.10.3",
]
[[package]]
@@ -2274,7 +2414,7 @@ dependencies = [
"once_cell",
"regex",
"serde",
- "sha-1",
+ "sha-1 0.10.0",
"string_enum",
"swc_atoms",
"swc_common",
@@ -2297,7 +2437,7 @@ dependencies = [
"hex",
"serde",
"serde_json",
- "sha-1",
+ "sha-1 0.10.0",
"swc_common",
"swc_ecma_ast",
"swc_ecma_codegen",
@@ -2692,6 +2832,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
[[package]]
name = "unicode-bidi"
version = "0.3.7"
diff --git a/packages/next-swc/crates/core/Cargo.toml b/packages/next-swc/crates/core/Cargo.toml
index 09d272b195827..4e5e5be3b190c 100644
--- a/packages/next-swc/crates/core/Cargo.toml
+++ b/packages/next-swc/crates/core/Cargo.toml
@@ -28,7 +28,9 @@ swc_ecma_loader = {version = "0.29.0", features = ["node", "lru"]}
swc_ecmascript = {version = "0.132.0", features = ["codegen", "minifier", "optimization", "parser", "react", "transforms", "typescript", "utils", "visit"]}
swc_node_base = "0.5.1"
swc_stylis = "0.96.1"
+swc_cached = "0.1.1"
tracing = {version = "0.1.28", features = ["release_max_level_off"]}
+handlebars = "4.2.1"
[dev-dependencies]
swc_ecma_transforms_testing = "0.69.0"
diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs
index dd13ea2af8168..9ca3d33a8ee55 100644
--- a/packages/next-swc/crates/core/src/lib.rs
+++ b/packages/next-swc/crates/core/src/lib.rs
@@ -49,6 +49,7 @@ mod auto_cjs;
pub mod disallow_re_export_all_in_page;
pub mod emotion;
pub mod hook_optimizer;
+pub mod modularize_imports;
pub mod next_dynamic;
pub mod next_ssg;
pub mod page_config;
@@ -102,6 +103,9 @@ pub struct TransformOptions {
#[serde(default)]
pub emotion: Option,
+
+ #[serde(default)]
+ pub modularize_imports: Option,
}
pub fn custom_before_pass<'a, C: Comments + 'a>(
@@ -191,6 +195,10 @@ pub fn custom_before_pass<'a, C: Comments + 'a>(
}
})
.unwrap_or_else(|| Either::Right(noop())),
+ match &opts.modularize_imports {
+ Some(config) => Either::Left(modularize_imports::modularize_imports(config.clone())),
+ None => Either::Right(noop()),
+ }
)
}
diff --git a/packages/next-swc/crates/core/src/modularize_imports.rs b/packages/next-swc/crates/core/src/modularize_imports.rs
new file mode 100644
index 0000000000000..5322f9d872d2c
--- /dev/null
+++ b/packages/next-swc/crates/core/src/modularize_imports.rs
@@ -0,0 +1,235 @@
+use std::borrow::Cow;
+use std::collections::HashMap;
+
+use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext};
+use once_cell::sync::Lazy;
+use regex::{Captures, Regex};
+use serde::{Deserialize, Serialize};
+use swc_cached::regex::CachedRegex;
+use swc_ecmascript::ast::*;
+use swc_ecmascript::visit::{noop_fold_type, Fold};
+
+static DUP_SLASH_REGEX: Lazy = Lazy::new(|| Regex::new(r"//").unwrap());
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(transparent)]
+pub struct Config {
+ pub packages: HashMap,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PackageConfig {
+ pub transform: String,
+ #[serde(default)]
+ pub prevent_full_import: bool,
+ #[serde(default)]
+ pub skip_default_conversion: bool,
+}
+
+struct FoldImports {
+ renderer: handlebars::Handlebars<'static>,
+ packages: Vec<(CachedRegex, PackageConfig)>,
+}
+
+struct Rewriter<'a> {
+ renderer: &'a handlebars::Handlebars<'static>,
+ key: &'a str,
+ config: &'a PackageConfig,
+ group: Vec<&'a str>,
+}
+
+impl<'a> Rewriter<'a> {
+ fn rewrite(&self, old_decl: &ImportDecl) -> Vec {
+ if old_decl.type_only || old_decl.asserts.is_some() {
+ return vec![old_decl.clone()];
+ }
+
+ let mut out: Vec = Vec::with_capacity(old_decl.specifiers.len());
+
+ for spec in &old_decl.specifiers {
+ match spec {
+ ImportSpecifier::Named(named_spec) => {
+ #[derive(Serialize)]
+ #[serde(untagged)]
+ enum Data<'a> {
+ Plain(&'a str),
+ Array(&'a [&'a str]),
+ }
+ let mut ctx: HashMap<&str, Data> = HashMap::new();
+ ctx.insert("matches", Data::Array(&self.group[..]));
+ ctx.insert(
+ "member",
+ Data::Plain(
+ named_spec
+ .imported
+ .as_ref()
+ .map(|x| match x {
+ ModuleExportName::Ident(x) => x.as_ref(),
+ ModuleExportName::Str(x) => x.value.as_ref(),
+ })
+ .unwrap_or_else(|| named_spec.local.as_ref()),
+ ),
+ );
+ let new_path = self
+ .renderer
+ .render_template(&self.config.transform, &ctx)
+ .unwrap_or_else(|e| {
+ panic!("error rendering template for '{}': {}", self.key, e);
+ });
+ let new_path = DUP_SLASH_REGEX.replace_all(&new_path, |_: &Captures| "/");
+ let specifier = if self.config.skip_default_conversion {
+ ImportSpecifier::Named(named_spec.clone())
+ } else {
+ ImportSpecifier::Default(ImportDefaultSpecifier {
+ local: named_spec.local.clone(),
+ span: named_spec.span,
+ })
+ };
+ out.push(ImportDecl {
+ specifiers: vec![specifier],
+ src: Str::from(new_path.as_ref()),
+ span: old_decl.span,
+ type_only: false,
+ asserts: None,
+ });
+ }
+ _ => {
+ if self.config.prevent_full_import {
+ panic!(
+ "import {:?} causes the entire module to be imported",
+ old_decl
+ );
+ } else {
+ // Give up
+ return vec![old_decl.clone()];
+ }
+ }
+ }
+ }
+ out
+ }
+}
+
+impl FoldImports {
+ fn should_rewrite<'a>(&'a self, name: &'a str) -> Option> {
+ for (regex, config) in &self.packages {
+ let group = regex.captures(name);
+ if let Some(group) = group {
+ let group = group
+ .iter()
+ .map(|x| x.map(|x| x.as_str()).unwrap_or_default())
+ .collect::>();
+ return Some(Rewriter {
+ renderer: &self.renderer,
+ key: name,
+ config,
+ group,
+ });
+ }
+ }
+ None
+ }
+}
+
+impl Fold for FoldImports {
+ noop_fold_type!();
+ fn fold_module(&mut self, mut module: Module) -> Module {
+ let mut new_items: Vec = vec![];
+ for item in module.body {
+ match item {
+ ModuleItem::ModuleDecl(ModuleDecl::Import(decl)) => {
+ match self.should_rewrite(&decl.src.value) {
+ Some(rewriter) => {
+ let rewritten = rewriter.rewrite(&decl);
+ new_items.extend(
+ rewritten
+ .into_iter()
+ .map(|x| ModuleItem::ModuleDecl(ModuleDecl::Import(x))),
+ );
+ }
+ None => new_items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(decl))),
+ }
+ }
+ x => {
+ new_items.push(x);
+ }
+ }
+ }
+ module.body = new_items;
+ module
+ }
+}
+
+pub fn modularize_imports(config: Config) -> impl Fold {
+ let mut folder = FoldImports {
+ renderer: handlebars::Handlebars::new(),
+ packages: vec![],
+ };
+ folder
+ .renderer
+ .register_helper("lowerCase", Box::new(helper_lower_case));
+ folder
+ .renderer
+ .register_helper("upperCase", Box::new(helper_upper_case));
+ folder
+ .renderer
+ .register_helper("camelCase", Box::new(helper_camel_case));
+ for (mut k, v) in config.packages {
+ // XXX: Should we keep this hack?
+ if !k.starts_with('^') && !k.ends_with('$') {
+ k = format!("^{}$", k);
+ }
+ folder.packages.push((
+ CachedRegex::new(&k).expect("transform-imports: invalid regex"),
+ v,
+ ));
+ }
+ folder
+}
+
+fn helper_lower_case(
+ h: &Helper<'_, '_>,
+ _: &Handlebars<'_>,
+ _: &Context,
+ _: &mut RenderContext<'_, '_>,
+ out: &mut dyn Output,
+) -> HelperResult {
+ // get parameter from helper or throw an error
+ let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
+ out.write(param.to_lowercase().as_ref())?;
+ Ok(())
+}
+
+fn helper_upper_case(
+ h: &Helper<'_, '_>,
+ _: &Handlebars<'_>,
+ _: &Context,
+ _: &mut RenderContext<'_, '_>,
+ out: &mut dyn Output,
+) -> HelperResult {
+ // get parameter from helper or throw an error
+ let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
+ out.write(param.to_uppercase().as_ref())?;
+ Ok(())
+}
+
+fn helper_camel_case(
+ h: &Helper<'_, '_>,
+ _: &Handlebars<'_>,
+ _: &Context,
+ _: &mut RenderContext<'_, '_>,
+ out: &mut dyn Output,
+) -> HelperResult {
+ // get parameter from helper or throw an error
+ let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
+ let value = if param.is_empty() || param.chars().next().unwrap().is_lowercase() {
+ Cow::Borrowed(param)
+ } else {
+ let mut it = param.chars();
+ let fst = it.next().unwrap();
+ Cow::Owned(fst.to_lowercase().chain(it).collect::())
+ };
+ out.write(value.as_ref())?;
+ Ok(())
+}
diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs
index ed9cf5b247b70..9a7d8b7e5e56a 100644
--- a/packages/next-swc/crates/core/tests/fixture.rs
+++ b/packages/next-swc/crates/core/tests/fixture.rs
@@ -1,6 +1,7 @@
use next_swc::{
amp_attributes::amp_attributes,
emotion::{self, EmotionOptions},
+ modularize_imports::modularize_imports,
next_dynamic::next_dynamic,
next_ssg::next_ssg,
page_config::page_config_test,
@@ -312,3 +313,46 @@ fn next_emotion_fixture(input: PathBuf) {
&output,
);
}
+
+#[fixture("tests/fixture/modularize-imports/**/input.js")]
+fn modularize_imports_fixture(input: PathBuf) {
+ use next_swc::modularize_imports::PackageConfig;
+ let output = input.parent().unwrap().join("output.js");
+ test_fixture(
+ syntax(),
+ &|_tr| {
+ modularize_imports(next_swc::modularize_imports::Config {
+ packages: vec![
+ (
+ "react-bootstrap".to_string(),
+ PackageConfig {
+ transform: "react-bootstrap/lib/{{member}}".into(),
+ prevent_full_import: false,
+ skip_default_conversion: false,
+ },
+ ),
+ (
+ "my-library/?(((\\w*)?/?)*)".to_string(),
+ PackageConfig {
+ transform: "my-library/{{ matches.[1] }}/{{member}}".into(),
+ prevent_full_import: false,
+ skip_default_conversion: false,
+ },
+ ),
+ (
+ "my-library-2".to_string(),
+ PackageConfig {
+ transform: "my-library-2/{{ camelCase member }}".into(),
+ prevent_full_import: false,
+ skip_default_conversion: true,
+ },
+ ),
+ ]
+ .into_iter()
+ .collect(),
+ })
+ },
+ &input,
+ &output,
+ );
+}
diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js
new file mode 100644
index 0000000000000..679eeeb2ddcf5
--- /dev/null
+++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js
@@ -0,0 +1,3 @@
+import { MyModule } from 'my-library';
+import { App } from 'my-library/components';
+import { Header, Footer } from 'my-library/components/App';
diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js
new file mode 100644
index 0000000000000..c4b5cadfeeb67
--- /dev/null
+++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js
@@ -0,0 +1,4 @@
+import MyModule from 'my-library/MyModule';
+import App from 'my-library/components/App';
+import Header from 'my-library/components/App/Header';
+import Footer from 'my-library/components/App/Footer';
diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js
new file mode 100644
index 0000000000000..f152bdc8f19dd
--- /dev/null
+++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js
@@ -0,0 +1,2 @@
+import { Grid, Row, Col as Col1 } from 'react-bootstrap';
+import { MyModule, Widget } from 'my-library-2';
diff --git a/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js
new file mode 100644
index 0000000000000..d3798eb0e9e40
--- /dev/null
+++ b/packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js
@@ -0,0 +1,5 @@
+import Grid from "react-bootstrap/lib/Grid";
+import Row from "react-bootstrap/lib/Row";
+import Col1 from "react-bootstrap/lib/Col";
+import { MyModule } from 'my-library-2/myModule';
+import { Widget } from 'my-library-2/widget';
diff --git a/packages/next-swc/crates/core/tests/full.rs b/packages/next-swc/crates/core/tests/full.rs
index bd7c4a6f65d72..12817af23a1e9 100644
--- a/packages/next-swc/crates/core/tests/full.rs
+++ b/packages/next-swc/crates/core/tests/full.rs
@@ -62,6 +62,7 @@ fn test(input: &Path, minify: bool) {
relay: None,
shake_exports: None,
emotion: Some(assert_json("{}")),
+ modularize_imports: None,
};
let options = options.patch(&fm);
diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js
index 00af330c102f1..33ed7b9d00eaa 100644
--- a/packages/next/build/swc/options.js
+++ b/packages/next/build/swc/options.js
@@ -102,6 +102,7 @@ function getBaseSWCOptions({
: null,
removeConsole: nextConfig?.compiler?.removeConsole,
reactRemoveProperties: nextConfig?.compiler?.reactRemoveProperties,
+ modularizeImports: nextConfig?.experimental?.modularizeImports,
relay: nextConfig?.compiler?.relay,
emotion: getEmotionOptions(nextConfig, development),
}
diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index 4e8d4d4a8e531..9ad462a01dc91 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -1653,6 +1653,7 @@ export default async function getBaseWebpackConfig(
styledComponents: config.compiler?.styledComponents,
relay: config.compiler?.relay,
emotion: config.experimental?.emotion,
+ modularizeImports: config.experimental?.modularizeImports,
})
const cache: any = {
diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts
index 0e8c6d5a53cbe..0007760e40174 100644
--- a/packages/next/server/config-shared.ts
+++ b/packages/next/server/config-shared.ts
@@ -118,6 +118,14 @@ export interface ExperimentalConfig {
autoLabel?: 'dev-only' | 'always' | 'never'
labelFormat?: string
}
+ modularizeImports?: Record<
+ string,
+ {
+ transform: string
+ preventFullImport?: boolean
+ skipDefaultConversion?: boolean
+ }
+ >
}
/**