Skip to content

Commit

Permalink
Implement named_import_transform (#54530)
Browse files Browse the repository at this point in the history
This is the first step to enable the automatic "modularize imports"
optimization for some libraries. It transforms named imports like
`import { A, B, C as F } from 'foo'` to a special loader string: `import
{ A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo"`.

In a follow-up PR we'll apply corresponding optimization with another
SWC transformer.
  • Loading branch information
shuding authored Aug 24, 2023
1 parent 2590f49 commit 3f1fae9
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 0 deletions.
8 changes: 8 additions & 0 deletions packages/next-swc/crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub mod amp_attributes;
mod auto_cjs;
pub mod cjs_optimizer;
pub mod disallow_re_export_all_in_page;
pub mod named_import_transform;
pub mod next_dynamic;
pub mod next_ssg;
pub mod page_config;
Expand Down Expand Up @@ -128,6 +129,9 @@ pub struct TransformOptions {
#[serde(default)]
pub modularize_imports: Option<modularize_imports::Config>,

#[serde(default)]
pub auto_modularize_imports: Option<named_import_transform::Config>,

#[serde(default)]
pub font_loaders: Option<next_transform_font::Config>,

Expand Down Expand Up @@ -245,6 +249,10 @@ where
Some(config) => Either::Left(shake_exports::shake_exports(config.clone())),
None => Either::Right(noop()),
},
match &opts.auto_modularize_imports {
Some(config) => Either::Left(named_import_transform::named_import_transform(config.clone())),
None => Either::Right(noop()),
},
opts.emotion
.as_ref()
.and_then(|config| {
Expand Down
83 changes: 83 additions & 0 deletions packages/next-swc/crates/core/src/named_import_transform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use serde::Deserialize;
use turbopack_binding::swc::core::{
common::DUMMY_SP,
ecma::{ast::*, visit::Fold},
};

#[derive(Clone, Debug, Deserialize)]
pub struct Config {
pub packages: Vec<String>,
}

pub fn named_import_transform(config: Config) -> impl Fold {
NamedImportTransform {
packages: config.packages,
}
}

#[derive(Debug, Default)]
struct NamedImportTransform {
packages: Vec<String>,
}

impl Fold for NamedImportTransform {
fn fold_import_decl(&mut self, decl: ImportDecl) -> ImportDecl {
// Match named imports and check if it's included in the packages
let src_value = decl.src.value.clone();

if self.packages.iter().any(|p| src_value == *p) {
let mut specifier_names = vec![];

// Skip the transform if the default or namespace import is present
let mut skip_transform = false;

for specifier in &decl.specifiers {
match specifier {
ImportSpecifier::Named(specifier) => {
// Push the import name as string to the vec
if let Some(imported) = &specifier.imported {
match imported {
ModuleExportName::Ident(ident) => {
specifier_names.push(ident.sym.to_string());
}
ModuleExportName::Str(str_) => {
specifier_names.push(str_.value.to_string());
}
}
} else {
specifier_names.push(specifier.local.sym.to_string());
}
}
ImportSpecifier::Default(_) => {
skip_transform = true;
break;
}
ImportSpecifier::Namespace(_) => {
skip_transform = true;
break;
}
}
}

if !skip_transform {
let new_src = format!(
"barrel-optimize-loader?names={}!{}",
specifier_names.join(","),
src_value
);

// Create a new import declaration, keep everything the same except the source
let mut new_decl = decl.clone();
new_decl.src = Box::new(Str {
span: DUMMY_SP,
value: new_src.into(),
raw: None,
});

return new_decl;
}
}

decl
}
}
27 changes: 27 additions & 0 deletions packages/next-swc/crates/core/tests/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{env::current_dir, path::PathBuf};
use next_swc::{
amp_attributes::amp_attributes,
cjs_optimizer::cjs_optimizer,
named_import_transform::named_import_transform,
next_dynamic::next_dynamic,
next_ssg::next_ssg,
page_config::page_config_test,
Expand Down Expand Up @@ -454,6 +455,32 @@ fn cjs_optimize_fixture(input: PathBuf) {
);
}

#[fixture("tests/fixture/named-import-transform/**/input.js")]
fn named_import_transform_fixture(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");
test_fixture(
syntax(),
&|_tr| {
let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();

chain!(
resolver(unresolved_mark, top_level_mark, false),
named_import_transform(json(
r#"
{
"packages": ["foo", "bar"]
}
"#
))
)
},
&input,
&output,
Default::default(),
);
}

fn json<T>(s: &str) -> T
where
T: DeserializeOwned,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { A, B, C as F } from 'foo'
import D from 'bar'
import E from 'baz'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo";
import D from 'bar';
import E from 'baz';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { A, B, C as F } from 'foo'
import { D } from 'bar'
import E from 'baz'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { A, B, C as F } from "barrel-optimize-loader?names=A,B,C!foo";
import { D } from "barrel-optimize-loader?names=D!bar";
import E from 'baz';
1 change: 1 addition & 0 deletions packages/next-swc/crates/core/tests/full.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ fn test(input: &Path, minify: bool) {
app_dir: None,
server_actions: None,
cjs_require_optimizer: None,
auto_modularize_imports: None,
};

let unresolved_mark = Mark::new();
Expand Down
5 changes: 5 additions & 0 deletions packages/next/src/build/swc/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,11 @@ export function getLoaderSWCOptions({
},
},
}
baseOptions.autoModularizeImports = {
packages: [
// TODO: Add a list of packages that should be optimized by default
],
}

const isNextDist = nextDistPath.test(filename)

Expand Down
1 change: 1 addition & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,7 @@ export default async function getBaseWebpackConfig(
'next-invalid-import-error-loader',
'next-metadata-route-loader',
'modularize-import-loader',
'barrel-optimize-loader',
].reduce((alias, loader) => {
// using multiple aliases to replace `resolveLoader.modules`
alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default function transformSource(this: any, source: string) {
// const { names }: any = this.getOptions()
// const { resourcePath } = this
return source
}
12 changes: 12 additions & 0 deletions test/development/basic/auto-modularize-imports/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
11 changes: 11 additions & 0 deletions test/development/basic/auto-modularize-imports/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import { IceCream } from 'lucide-react'

export default function Page() {
return (
<div>
<IceCream />
</div>
)
}

0 comments on commit 3f1fae9

Please sign in to comment.