diff --git a/packages/loadable-components/README.md b/packages/loadable-components/README.md index 16a483ad2..585682178 100644 --- a/packages/loadable-components/README.md +++ b/packages/loadable-components/README.md @@ -8,19 +8,40 @@ 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. + +### Custom signatures with specific package sources + ```json ["loadable-components", { "signatures": [ { "from": "myLoadableWrapper", - "name": "default" + "name": "default" }, { "from": "myLoadableWrapper", - "name": "lazy" + "name": "lazy" }] }] ``` +### Custom signatures without package source (matches any import) + +To match any import with a specific name regardless of the source package (useful for vendored or aliased packages), omit the `from` field: + +```json +["loadable-components", { "signatures": [ + { + "name": "loadable" + }] +}] +``` + +This will transform any default import named `loadable`, regardless of where it's imported from: +```js +import loadable from 'my-vendored-loadable'; // will be transformed +import loadable from '@loadable/component'; // will be transformed +``` + # @swc/plugin-loadable-components ## 11.3.0 diff --git a/packages/loadable-components/README.tmpl.md b/packages/loadable-components/README.tmpl.md index 7f65b61e8..72282e037 100644 --- a/packages/loadable-components/README.tmpl.md +++ b/packages/loadable-components/README.tmpl.md @@ -8,17 +8,38 @@ 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. + +### Custom signatures with specific package sources + ```json ["loadable-components", { "signatures": [ { "from": "myLoadableWrapper", - "name": "default" + "name": "default" }, { "from": "myLoadableWrapper", - "name": "lazy" + "name": "lazy" }] }] ``` +### Custom signatures without package source (matches any import) + +To match any import with a specific name regardless of the source package (useful for vendored or aliased packages), omit the `from` field: + +```json +["loadable-components", { "signatures": [ + { + "name": "loadable" + }] +}] +``` + +This will transform any default import named `loadable`, regardless of where it's imported from: +```js +import loadable from 'my-vendored-loadable'; // will be transformed +import loadable from '@loadable/component'; // will be transformed +``` + ${CHANGELOG} diff --git a/packages/loadable-components/src/lib.rs b/packages/loadable-components/src/lib.rs index 234e1d8e5..636067d96 100644 --- a/packages/loadable-components/src/lib.rs +++ b/packages/loadable-components/src/lib.rs @@ -636,29 +636,34 @@ where { 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()); - } + // Skip source check if from is None (match any source) + if let Some(ref from) = signature.from { + if from != &*import_decl.src.value { + continue; + } + } + + 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 { + } + 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()); + } } + _ => (), } } } @@ -776,14 +781,15 @@ fn clone_params(e: &Expr) -> Vec { #[derive(Debug, Clone, Deserialize, PartialEq)] pub struct Signature { pub name: Atom, - pub from: Wtf8Atom, + #[serde(default)] + pub from: Option, } impl Default for Signature { fn default() -> Self { Signature { name: "default".into(), - from: "@loadable/component".into(), + from: Some("@loadable/component".into()), } } } @@ -796,7 +802,7 @@ impl Signature { pub fn default_lazy() -> Self { Signature { name: "lazy".into(), - from: "@loadable/component".into(), + from: Some("@loadable/component".into()), } } } @@ -836,7 +842,7 @@ mod tests { "signatures": [ { "from": "myLoadableWrapper", - "name": "lazy" + "name": "lazy" } ] }"#, @@ -844,9 +850,29 @@ mod tests { assert_eq!( config.signatures, vec![Signature { - from: "myLoadableWrapper".into(), + from: Some("myLoadableWrapper".into()), name: "lazy".into() }] ) } + + #[test] + fn should_support_signatures_without_from() { + let config = get_config( + r#"{ + "signatures": [ + { + "name": "loadable" + } + ] + }"#, + ); + assert_eq!( + config.signatures, + vec![Signature { + from: None, + name: "loadable".into() + }] + ) + } } diff --git a/packages/loadable-components/tests/fixture.rs b/packages/loadable-components/tests/fixture.rs index 20199aea4..68e4a1a56 100644 --- a/packages/loadable-components/tests/fixture.rs +++ b/packages/loadable-components/tests/fixture.rs @@ -38,15 +38,38 @@ fn fixture_custom_signatures(input: PathBuf) { vec![ Signature { name: "lazy".into(), - from: "my-custom-package".into(), + from: Some("my-custom-package".into()), }, Signature { name: "custom".into(), - from: "my-custom-package".into(), + from: Some("my-custom-package".into()), }, Signature { name: "default".into(), - from: "my-custom-package".into(), + from: Some("my-custom-package".into()), + }, + ], + )) + }, + &input, + &output, + Default::default(), + ); +} + +#[testing::fixture("tests/fixture/aliased import/**/input.js")] +fn fixture_aliased_import(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + + test_fixture( + Default::default(), + &|t| { + visit_mut_pass(loadable_transform( + t.comments.clone(), + vec![ + Signature { + name: "default".into(), + from: None, }, ], )) diff --git a/packages/loadable-components/tests/fixture/aliased import/should work with vendored loadable/input.js b/packages/loadable-components/tests/fixture/aliased import/should work with vendored loadable/input.js new file mode 100644 index 000000000..a1842a6ae --- /dev/null +++ b/packages/loadable-components/tests/fixture/aliased import/should work with vendored loadable/input.js @@ -0,0 +1,3 @@ +import loadable from 'my-vendored-loadable'; + +loadable(() => import('./SomeComponent')); diff --git a/packages/loadable-components/tests/fixture/aliased import/should work with vendored loadable/output.js b/packages/loadable-components/tests/fixture/aliased import/should work with vendored loadable/output.js new file mode 100644 index 000000000..2d0605fe9 --- /dev/null +++ b/packages/loadable-components/tests/fixture/aliased import/should work with vendored loadable/output.js @@ -0,0 +1,39 @@ +import loadable from 'my-vendored-loadable'; +loadable({ + resolved: {}, + chunkName () { + return "SomeComponent"; + }, + isReady (props) { + const key = this.resolve(props); + if (this.resolved[key] !== true) { + return false; + } + if (typeof __webpack_modules__ !== 'undefined') { + return !!__webpack_modules__[key]; + } + return false; + }, + importAsync: ()=>import(/*webpackChunkName: "SomeComponent"*/ './SomeComponent'), + requireAsync (props) { + const key = this.resolve(props); + this.resolved[key] = false; + return this.importAsync(props).then((resolved)=>{ + this.resolved[key] = true; + return resolved; + }); + }, + requireSync (props) { + const id = this.resolve(props); + if (typeof __webpack_require__ !== 'undefined') { + return __webpack_require__(id); + } + return eval('module.require')(id); + }, + resolve () { + if (require.resolveWeak) { + return require.resolveWeak('./SomeComponent'); + } + return eval('require.resolve')('./SomeComponent'); + } +});