From a7bdeea2005a890e94b6e4a68839dd1912658213 Mon Sep 17 00:00:00 2001 From: losfair Date: Wed, 2 Mar 2022 21:31:15 +0800 Subject: [PATCH 1/8] Add modularize_imports transform. --- packages/next-swc/Cargo.lock | 157 +++++++++++- packages/next-swc/crates/core/Cargo.toml | 1 + packages/next-swc/crates/core/src/lib.rs | 8 + .../crates/core/src/modularize_imports.rs | 231 ++++++++++++++++++ packages/next-swc/crates/core/tests/full.rs | 1 + 5 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 packages/next-swc/crates/core/src/modularize_imports.rs diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock index e42eee1bc7c0..6e01829d7f44 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", @@ -1012,6 +1084,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 +1175,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 +1388,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 +1692,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 +1712,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.10.3", ] [[package]] @@ -2274,7 +2413,7 @@ dependencies = [ "once_cell", "regex", "serde", - "sha-1", + "sha-1 0.10.0", "string_enum", "swc_atoms", "swc_common", @@ -2297,7 +2436,7 @@ dependencies = [ "hex", "serde", "serde_json", - "sha-1", + "sha-1 0.10.0", "swc_common", "swc_ecma_ast", "swc_ecma_codegen", @@ -2692,6 +2831,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 09d272b19582..4e02393ec440 100644 --- a/packages/next-swc/crates/core/Cargo.toml +++ b/packages/next-swc/crates/core/Cargo.toml @@ -29,6 +29,7 @@ swc_ecmascript = {version = "0.132.0", features = ["codegen", "minifier", "optim swc_node_base = "0.5.1" swc_stylis = "0.96.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 dd13ea2af816..9ca3d33a8ee5 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 000000000000..96eef4a74f95 --- /dev/null +++ b/packages/next-swc/crates/core/src/modularize_imports.rs @@ -0,0 +1,231 @@ +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_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, + pub prevent_full_import: bool, + pub skip_default_conversion: bool, +} + +struct FoldImports { + renderer: handlebars::Handlebars<'static>, + packages: Vec<(Regex, 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![]; + + 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((Regex::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/full.rs b/packages/next-swc/crates/core/tests/full.rs index bd7c4a6f65d7..12817af23a1e 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); From 004694fce4ca1043352385c64529bfaab7225616 Mon Sep 17 00:00:00 2001 From: losfair Date: Wed, 2 Mar 2022 21:31:23 +0800 Subject: [PATCH 2/8] Tests for modularize-imports. --- .../next-swc/crates/core/tests/fixture.rs | 44 +++++++++++++++++++ .../fixture/modularize-imports/regex/input.js | 3 ++ .../modularize-imports/regex/output.js | 4 ++ .../modularize-imports/simple/input.js | 2 + .../modularize-imports/simple/output.js | 5 +++ 5 files changed, 58 insertions(+) create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/regex/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/modularize-imports/simple/output.js diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index ed9cf5b247b7..9a7d8b7e5e56 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 000000000000..679eeeb2ddcf --- /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 000000000000..c4b5cadfeeb6 --- /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 000000000000..f152bdc8f19d --- /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 000000000000..d3798eb0e9e4 --- /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'; From fcb94d888ebfdede570a329409656d5fdbe5696c Mon Sep 17 00:00:00 2001 From: losfair Date: Wed, 2 Mar 2022 23:03:02 +0800 Subject: [PATCH 3/8] Wire up configuration. --- packages/next-swc/crates/core/src/modularize_imports.rs | 4 +++- packages/next/build/swc/options.js | 1 + packages/next/build/webpack-config.ts | 1 + packages/next/server/config-shared.ts | 8 ++++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/next-swc/crates/core/src/modularize_imports.rs b/packages/next-swc/crates/core/src/modularize_imports.rs index 96eef4a74f95..e2dac456d90a 100644 --- a/packages/next-swc/crates/core/src/modularize_imports.rs +++ b/packages/next-swc/crates/core/src/modularize_imports.rs @@ -20,7 +20,9 @@ pub struct Config { #[serde(rename_all = "camelCase")] pub struct PackageConfig { pub transform: String, + #[serde(default)] pub prevent_full_import: bool, + #[serde(default)] pub skip_default_conversion: bool, } @@ -174,7 +176,7 @@ pub fn modularize_imports(config: Config) -> impl Fold { .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("$") { + if !k.starts_with('^') && !k.ends_with('$') { k = format!("^{}$", k); } folder diff --git a/packages/next/build/swc/options.js b/packages/next/build/swc/options.js index 00af330c102f..33ed7b9d00ea 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 a42abcae003e..17532938930a 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1638,6 +1638,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 0e8c6d5a53cb..0007760e4017 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 + } + > } /** From 96f8b4c364dc2994b5c45201fdff40aa8bf01385 Mon Sep 17 00:00:00 2001 From: losfair Date: Wed, 2 Mar 2022 23:04:31 +0800 Subject: [PATCH 4/8] Add example. --- examples/modularize-imports/.gitignore | 34 +++++++++++++++++++ examples/modularize-imports/README.md | 27 +++++++++++++++ .../components/halves/LeftHalf.js | 3 ++ .../components/halves/RightHalf.js | 3 ++ .../components/halves/index.js | 5 +++ examples/modularize-imports/next.config.js | 9 +++++ examples/modularize-imports/package.json | 13 +++++++ examples/modularize-imports/pages/index.js | 10 ++++++ 8 files changed, 104 insertions(+) create mode 100644 examples/modularize-imports/.gitignore create mode 100644 examples/modularize-imports/README.md create mode 100644 examples/modularize-imports/components/halves/LeftHalf.js create mode 100644 examples/modularize-imports/components/halves/RightHalf.js create mode 100644 examples/modularize-imports/components/halves/index.js create mode 100644 examples/modularize-imports/next.config.js create mode 100644 examples/modularize-imports/package.json create mode 100644 examples/modularize-imports/pages/index.js diff --git a/examples/modularize-imports/.gitignore b/examples/modularize-imports/.gitignore new file mode 100644 index 000000000000..1437c53f70bc --- /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 000000000000..c80cdb596798 --- /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 000000000000..5f96fe8a0d9a --- /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 000000000000..b1b5d5e0d3b4 --- /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 000000000000..ca81fb870177 --- /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 000000000000..55be9582d2f9 --- /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 000000000000..f9170ae254fa --- /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 000000000000..ac543f049412 --- /dev/null +++ b/examples/modularize-imports/pages/index.js @@ -0,0 +1,10 @@ +import { LeftHalf, RightHalf } from '../components/halves' + +const Index = () => ( +
+ + +
+) + +export default Index From d42bd84f5d52cc16c8a54648e578d01a1aff55ac Mon Sep 17 00:00:00 2001 From: losfair Date: Wed, 2 Mar 2022 23:19:51 +0800 Subject: [PATCH 5/8] Add docs for modularize-imports. --- docs/advanced-features/compiler.md | 83 ++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/docs/advanced-features/compiler.md b/docs/advanced-features/compiler.md index 16d415a0a11d..35acc704d1ba 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. From 7cbc423b7ab6e38100777c4695df7f7bdb55838b Mon Sep 17 00:00:00 2001 From: losfair Date: Wed, 2 Mar 2022 23:33:42 +0800 Subject: [PATCH 6/8] Fix lints --- examples/modularize-imports/components/halves/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/modularize-imports/components/halves/index.js b/examples/modularize-imports/components/halves/index.js index ca81fb870177..b542f3266a39 100644 --- a/examples/modularize-imports/components/halves/index.js +++ b/examples/modularize-imports/components/halves/index.js @@ -1,5 +1,5 @@ -import LeftHalf from './LeftHalf' -import RightHalf from './RightHalf' +// 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 }; From f56478a237a339bb25dcd4557c7d924ebae501b7 Mon Sep 17 00:00:00 2001 From: losfair Date: Thu, 17 Mar 2022 13:52:06 +0800 Subject: [PATCH 7/8] CachedRegex & pre-allocate output buffer --- packages/next-swc/Cargo.lock | 1 + packages/next-swc/crates/core/Cargo.toml | 1 + packages/next-swc/crates/core/src/modularize_imports.rs | 7 ++++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/next-swc/Cargo.lock b/packages/next-swc/Cargo.lock index 6e01829d7f44..68ff4a132b49 100644 --- a/packages/next-swc/Cargo.lock +++ b/packages/next-swc/Cargo.lock @@ -973,6 +973,7 @@ dependencies = [ "styled_components", "swc", "swc_atoms", + "swc_cached", "swc_common", "swc_css", "swc_ecma_loader", diff --git a/packages/next-swc/crates/core/Cargo.toml b/packages/next-swc/crates/core/Cargo.toml index 4e02393ec440..4e5e5be3b190 100644 --- a/packages/next-swc/crates/core/Cargo.toml +++ b/packages/next-swc/crates/core/Cargo.toml @@ -28,6 +28,7 @@ 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" diff --git a/packages/next-swc/crates/core/src/modularize_imports.rs b/packages/next-swc/crates/core/src/modularize_imports.rs index e2dac456d90a..d1bb350c2d9e 100644 --- a/packages/next-swc/crates/core/src/modularize_imports.rs +++ b/packages/next-swc/crates/core/src/modularize_imports.rs @@ -5,6 +5,7 @@ use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContex 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}; @@ -28,7 +29,7 @@ pub struct PackageConfig { struct FoldImports { renderer: handlebars::Handlebars<'static>, - packages: Vec<(Regex, PackageConfig)>, + packages: Vec<(CachedRegex, PackageConfig)>, } struct Rewriter<'a> { @@ -44,7 +45,7 @@ impl<'a> Rewriter<'a> { return vec![old_decl.clone()]; } - let mut out: Vec = vec![]; + let mut out: Vec = Vec::with_capacity(old_decl.specifiers.len()); for spec in &old_decl.specifiers { match spec { @@ -181,7 +182,7 @@ pub fn modularize_imports(config: Config) -> impl Fold { } folder .packages - .push((Regex::new(&k).expect("transform-imports: invalid regex"), v)); + .push((CachedRegex::new(&k).expect("transform-imports: invalid regex"), v)); } folder } From 7acf9a309ab3f991f85ac0f8fe34eb3bfa89f9db Mon Sep 17 00:00:00 2001 From: losfair Date: Thu, 17 Mar 2022 14:10:39 +0800 Subject: [PATCH 8/8] Fix formatting --- packages/next-swc/crates/core/src/modularize_imports.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/next-swc/crates/core/src/modularize_imports.rs b/packages/next-swc/crates/core/src/modularize_imports.rs index d1bb350c2d9e..5322f9d872d2 100644 --- a/packages/next-swc/crates/core/src/modularize_imports.rs +++ b/packages/next-swc/crates/core/src/modularize_imports.rs @@ -180,9 +180,10 @@ pub fn modularize_imports(config: Config) -> impl Fold { if !k.starts_with('^') && !k.ends_with('$') { k = format!("^{}$", k); } - folder - .packages - .push((CachedRegex::new(&k).expect("transform-imports: invalid regex"), v)); + folder.packages.push(( + CachedRegex::new(&k).expect("transform-imports: invalid regex"), + v, + )); } folder }