Skip to content

Commit

Permalink
feat(loadable-components): Transpile lazy and signatures configurat…
Browse files Browse the repository at this point in the history
…ion (#242)

closes #241

This pull request brings behavior closer to babel plugin:
1. Add ability to transpile `lazy` from `@loadable/component` without
hacks
2. Add ability to configure signatures. [babel plugin
doc](gregberge/loadable-components@edaf30c)

**BREAKING CHANGE:**

Before this changes plugin detected loadable only by `loadable` keyword.
After you should to configure `signatures` if you are using imports non
from `@loadable/component`.
For example: 
```
["loadable-components", { "signatures": [
    {
        "from": "myLoadableWrapper",
        "name": "default" 
    },
    {
        "from": "myLoadableWrapper",
        "name": "lazy" 
    }]
}]
```
  • Loading branch information
Themezv committed Jul 7, 2024
1 parent 448aedf commit d911134
Show file tree
Hide file tree
Showing 51 changed files with 466 additions and 18 deletions.
9 changes: 5 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/loadable-components/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
once_cell = "1.19.0"
regex = "1.10.4"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
swc_common = { version = "0.34.3", features = ["concurrent"] }
swc_core = { version = "0.96.0", features = [
Expand Down
15 changes: 15 additions & 0 deletions packages/loadable-components/README.tmpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,19 @@
["loadable-components", {}]
```

Sometimes you need to wrap loadable with your own custom logic. There are many use cases for it, from injecting telemetry to hiding external libraries behind facade.
By default `loadable-components` are configured to transform dynamic imports used only inside loadable helpers, but can be configured to instrument any other function of your choice.
```json
["loadable-components", { "signatures": [
{
"from": "myLoadableWrapper",
"name": "default"
},
{
"from": "myLoadableWrapper",
"name": "lazy"
}]
}]
```

${CHANGELOG}
150 changes: 140 additions & 10 deletions packages/loadable-components/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#![allow(clippy::boxed_local)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]

use std::collections::HashSet;

use once_cell::sync::Lazy;
use serde::Deserialize;
use swc_common::{
comments::{Comment, CommentKind, Comments},
util::take::Take,
BytePos, Spanned, DUMMY_SP,
};
use swc_core::quote;
use swc_core::{atoms::Atom, quote};
use swc_ecma_ast::*;
use swc_ecma_utils::{quote_ident, ExprFactory};
use swc_ecma_visit::{Visit, VisitMut, VisitMutWith, VisitWith};
Expand All @@ -31,43 +34,63 @@ static WEBPACK_MATCH_PADDED_HYPHENS_REPLACE_REGEX: Lazy<regex::Regex> =
static MATCH_LEFT_HYPHENS_REPLACE_REGEX: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"^-").unwrap());

fn get_config(plugin_config: &str) -> PluginConfig {
serde_json::from_str::<PluginConfig>(plugin_config)
.expect("invalid config for swc-loadable-components")
}

#[plugin_transform]
fn loadable_components_plugin(
mut program: Program,
_data: TransformPluginProgramMetadata,
data: TransformPluginProgramMetadata,
) -> Program {
program.visit_mut_with(&mut loadable_transform(PluginCommentsProxy));
let config = get_config(
&data
.get_transform_plugin_config()
.expect("failed to get plugin config for swc-loadable-components"),
);

program.visit_mut_with(&mut loadable_transform(
PluginCommentsProxy,
config.signatures,
));

program
}

pub fn loadable_transform<C>(comments: C) -> impl VisitMut
pub fn loadable_transform<C>(comments: C, signatures: Vec<Signature>) -> impl VisitMut
where
C: Comments,
{
Loadable { comments }
Loadable {
comments,
signatures,
specifiers: HashSet::new(),
}
}

struct Loadable<C>
where
C: Comments,
{
comments: C,
signatures: Vec<Signature>,
specifiers: HashSet<Atom>,
}

impl<C> Loadable<C>
where
C: Comments,
{
fn is_valid_identifier(e: &Expr) -> bool {
fn is_valid_identifier(&self, e: &Expr) -> bool {
match e {
Expr::Ident(i) => &*i.sym == "loadable",
Expr::Ident(i) => self.specifiers.contains(&i.sym),
Expr::Member(MemberExpr {
obj,
prop: MemberProp::Ident(prop),
..
}) => match &**obj {
Expr::Ident(i) => &*i.sym == "loadable" && &*prop.sym == "lib",
Expr::Ident(i) => self.specifiers.contains(&i.sym) && &*prop.sym == "lib",
_ => false,
},
_ => false,
Expand Down Expand Up @@ -616,11 +639,40 @@ impl<C> VisitMut for Loadable<C>
where
C: Comments,
{
fn visit_mut_import_decl(&mut self, import_decl: &mut ImportDecl) {
for signature in self.signatures.iter() {
if signature.from == *import_decl.src.value {
for specifier in import_decl.specifiers.iter() {
match specifier {
ImportSpecifier::Default(default_spec) => {
if signature.is_default_specifier() {
self.specifiers.insert(default_spec.local.sym.clone());
}
}
ImportSpecifier::Named(named_specifier) => {
if let Some(ModuleExportName::Ident(imported)) =
&named_specifier.imported
{
if imported.sym == signature.name {
self.specifiers.insert(named_specifier.local.sym.clone());
return;
}
}
if named_specifier.local.sym == signature.name {
self.specifiers.insert(named_specifier.local.sym.clone());
}
}
_ => (),
}
}
}
}
}

fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
call.visit_mut_children_with(self);

match &call.callee {
Callee::Expr(callee) if Self::is_valid_identifier(callee) => {}
Callee::Expr(callee) if self.is_valid_identifier(callee) => {}
_ => return,
}

Expand Down Expand Up @@ -725,3 +777,81 @@ fn clone_params(e: &Expr) -> Vec<Param> {
_ => Default::default(),
}
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct Signature {
pub name: Atom,
pub from: Atom,
}

impl Default for Signature {
fn default() -> Self {
Signature {
name: "default".into(),
from: "@loadable/component".into(),
}
}
}

impl Signature {
fn is_default_specifier(&self) -> bool {
self.name == *"default"
}

pub fn default_lazy() -> Self {
Signature {
name: "lazy".into(),
from: "@loadable/component".into(),
}
}
}

#[derive(Debug, Clone, Deserialize, PartialEq)]
struct PluginConfig {
#[serde(default = "default_signatures")]
signatures: Vec<Signature>,
}

fn default_signatures() -> Vec<Signature> {
vec![Signature::default(), Signature::default_lazy()]
}

impl Default for PluginConfig {
fn default() -> Self {
Self {
signatures: default_signatures(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn should_return_default_config_signatures() {
let config = get_config("{}");
assert_eq!(config.signatures, default_signatures())
}

#[test]
fn should_return_custom_signatures() {
let config = get_config(
r#"{
"signatures": [
{
"from": "myLoadableWrapper",
"name": "lazy"
}
]
}"#,
);
assert_eq!(
config.signatures,
vec![Signature {
from: "myLoadableWrapper".into(),
name: "lazy".into()
}]
)
}
}
48 changes: 44 additions & 4 deletions packages/loadable-components/tests/fixture.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
use std::path::PathBuf;

use swc_core::ecma::{transforms::testing::test_fixture, visit::as_folder};
use swc_plugin_loadable_components::loadable_transform;
use swc_plugin_loadable_components::{loadable_transform, Signature};

#[testing::fixture("tests/fixture/**/input.js")]
fn fixture(input: PathBuf) {
#[testing::fixture("tests/fixture/aggressive import/**/input.js")]
#[testing::fixture("tests/fixture/lazy/**/input.js")]
#[testing::fixture("tests/fixture/loadable.lib/**/input.js")]
#[testing::fixture("tests/fixture/magic comments/**/input.js")]
#[testing::fixture("tests/fixture/simple import/**/input.js")]
fn fixture_default_signatures(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");

test_fixture(
Default::default(),
&|t| as_folder(loadable_transform(t.comments.clone())),
&|t| {
as_folder(loadable_transform(
t.comments.clone(),
vec![Signature::default(), Signature::default_lazy()],
))
},
&input,
&output,
Default::default(),
);
}

#[testing::fixture("tests/fixture/signatures/**/input.js")]
fn fixture_custom_signatures(input: PathBuf) {
let output = input.parent().unwrap().join("output.js");

test_fixture(
Default::default(),
&|t| {
as_folder(loadable_transform(
t.comments.clone(),
vec![
Signature {
name: "lazy".into(),
from: "my-custom-package".into(),
},
Signature {
name: "custom".into(),
from: "my-custom-package".into(),
},
Signature {
name: "default".into(),
from: "my-custom-package".into(),
},
],
))
},
&input,
&output,
Default::default(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import loadable from "@loadable/component";
loadable(({ foo }) => import(/* webpackChunkName: "Pages" */ `./${foo}`));
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import loadable from "@loadable/component";
loadable({
resolved: {},
chunkName ({ foo }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import loadable from "@loadable/component";
loadable(
(props) =>
import(/* webpackChunkName: "pages/[request]" */ `./pages/${props.path}`),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import loadable from "@loadable/component";
loadable({
resolved: {},
chunkName (props) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import loadable from "@loadable/component";
loadable((props) => import(/* webpackChunkName: "Pages" */ `./${props.foo}`));
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import loadable from "@loadable/component";
loadable({
resolved: {},
chunkName (props) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import loadable from "@loadable/component";
loadable((props) => import(`./dir/${props.foo}/test`));
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import loadable from "@loadable/component";
loadable({
resolved: {},
chunkName (props) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import loadable from "@loadable/component";
loadable(({ foo }) => import(`./dir/${foo}/test`));
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import loadable from "@loadable/component";
loadable({
resolved: {},
chunkName ({ foo }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import loadable from "@loadable/component";
loadable((props) => import(`./${props.foo}`));
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import loadable from "@loadable/component";
loadable({
resolved: {},
chunkName (props) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { lazy } from "@loadable/component";

lazy(() => import("./ModA"));
Loading

0 comments on commit d911134

Please sign in to comment.