From 029e1f669658820cfbf4e157e7fefaa890515f24 Mon Sep 17 00:00:00 2001 From: harpsealjs Date: Wed, 17 Jul 2024 15:55:40 +0800 Subject: [PATCH] feat: support webpackExports in magic comments (#7198) --- .../src/parser_plugin/import_parser_plugin.rs | 12 +- .../src/webpack_comment.rs | 56 +++++++- .../cases/chunks/inline-options/index.js | 130 +++++++++--------- .../chunks/inline-options/test.filter.js | 6 - .../docs/en/api/modules/module-methods.mdx | 9 ++ .../docs/zh/api/modules/module-methods.mdx | 8 ++ 6 files changed, 141 insertions(+), 80 deletions(-) delete mode 100644 tests/webpack-test/cases/chunks/inline-options/test.filter.js diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs b/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs index 82ad11e3ad7b..1d28d93316f7 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/import_parser_plugin.rs @@ -5,6 +5,7 @@ use rspack_core::{ChunkGroupOptions, DynamicImportFetchPriority}; use rspack_core::{ContextNameSpaceObject, ContextOptions, DependencyCategory, SpanExt}; use swc_core::common::Spanned; use swc_core::ecma::ast::{CallExpr, Callee}; +use swc_core::ecma::atoms::Atom; use super::JavascriptParserPlugin; use crate::dependency::{ImportContextDependency, ImportDependency, ImportEagerDependency}; @@ -70,6 +71,11 @@ impl JavascriptParserPlugin for ImportParserPlugin { .or(dynamic_import_fetch_priority); let include = magic_comment_options.get_webpack_include(); let exclude = magic_comment_options.get_webpack_exclude(); + let exports = magic_comment_options.get_webpack_exports().map(|x| { + x.iter() + .map(|name| Atom::from(name.to_owned())) + .collect::>() + }); let param = parser.evaluate_expression(dyn_imported.expr.as_ref()); @@ -81,8 +87,7 @@ impl JavascriptParserPlugin for ImportParserPlugin { node.span.real_hi(), param.string().as_str().into(), Some(span), - // TODO scan dynamic import referenced exports - None, + exports, ); parser.dependencies.push(Box::new(dep)); return Some(true); @@ -92,8 +97,7 @@ impl JavascriptParserPlugin for ImportParserPlugin { node.span.real_hi(), param.string().as_str().into(), Some(span), - // TODO scan dynamic import referenced exports - None, + exports, )); let mut block = AsyncDependenciesBlock::new( *parser.module_identifier, diff --git a/crates/rspack_plugin_javascript/src/webpack_comment.rs b/crates/rspack_plugin_javascript/src/webpack_comment.rs index 5294147760bb..0d661e39ad4c 100644 --- a/crates/rspack_plugin_javascript/src/webpack_comment.rs +++ b/crates/rspack_plugin_javascript/src/webpack_comment.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use once_cell::sync::Lazy; use regex::Captures; use rspack_error::miette::{Diagnostic, Severity}; @@ -20,6 +21,7 @@ pub enum WebpackComment { ExcludeRegexp, ExcludeFlags, Mode, + Exports, } #[derive(Debug)] @@ -95,6 +97,13 @@ impl WebpackCommentMap { }) }) } + + pub fn get_webpack_exports(&self) -> Option> { + self + .0 + .get(&WebpackComment::Exports) + .map(|expr| expr.split(',').map(|x| x.to_owned()).collect_vec()) + } } fn add_magic_comment_warning( @@ -128,13 +137,16 @@ fn add_magic_comment_warning( // _4 for number // _5 for true/false // _6 for regexp -// _7 for identifier -// TODO: regexp/array +// _7 for array +// _8 for identifier static WEBPACK_MAGIC_COMMENT_REGEXP: Lazy = Lazy::new(|| { - regex::Regex::new(r#"(?P<_0>webpack[a-zA-Z\d_-]+)\s*:\s*("(?P<_1>[^"]+)"|'(?P<_2>[^']+)'|`(?P<_3>[^`]+)`|(?P<_4>[\d.-]+)|(?P<_5>true|false)|(?P<_6>/([^,]+)/([dgimsuvy]*))|(?P<_7>([^,]+)))"#) + regex::Regex::new(r#"(?P<_0>webpack[a-zA-Z\d_-]+)\s*:\s*("(?P<_1>[^"]+)"|'(?P<_2>[^']+)'|`(?P<_3>[^`]+)`|(?P<_4>[\d.-]+)|(?P<_5>true|false)|(?P<_6>/([^,]+)/([dgimsuvy]*))|\[(?P<_7>[^\]]+)|(?P<_8>([^,]+)))"#) .expect("invalid regex") }); +static WEBAPCK_EXPORT_NAME_REGEXP: Lazy = + Lazy::new(|| regex::Regex::new(r#"^["`'](\w+)["`']$"#).expect("invalid regex")); + pub fn try_extract_webpack_magic_comment( source_file: &SourceFile, comments: &Option<&dyn Comments>, @@ -337,9 +349,43 @@ fn analyze_comments( error_span, ); } - _ => { - // TODO: other magic comment + "webpackExports" => { + if let Some(item_value_match) = captures + .name("_1") + .or(captures.name("_2")) + .or(captures.name("_3")) + { + result.insert( + WebpackComment::Exports, + item_value_match.as_str().trim().to_string(), + ); + return; + } else if let Some(item_value_match) = captures.name("_7") { + if let Some(exports) = + item_value_match + .as_str() + .split(',') + .try_fold("".to_string(), |acc, item| { + WEBAPCK_EXPORT_NAME_REGEXP + .captures(item.trim()) + .and_then(|matched| matched.get(1).map(|x| x.as_str())) + .map(|name| format!("{acc},{name}")) + }) + { + result.insert(WebpackComment::Exports, exports); + return; + } + } + add_magic_comment_warning( + source_file, + item_name, + r#"a string or an array of strings"#, + &captures, + warning_diagnostics, + error_span, + ); } + _ => {} } } } diff --git a/tests/webpack-test/cases/chunks/inline-options/index.js b/tests/webpack-test/cases/chunks/inline-options/index.js index c4a92227ad24..16e1282e07ce 100644 --- a/tests/webpack-test/cases/chunks/inline-options/index.js +++ b/tests/webpack-test/cases/chunks/inline-options/index.js @@ -116,71 +116,71 @@ it("should not find module when mode is weak and chunk not served elsewhere (wit }); }); -// if (process.env.NODE_ENV === "production") { -// it("should contain only one export from webpackExports from module", function () { -// return import(/* webpackExports: "usedExports" */ "./dir12/a?1").then( -// module => { -// expect(module.usedExports).toEqual(["usedExports"]); -// } -// ); -// }); - -// it("should contain only webpackExports from module", function () { -// return import( -// /* webpackExports: ["a", "usedExports", "b"] */ "./dir12/a?2" -// ).then(module => { -// expect(module.usedExports).toEqual(["a", "b", "usedExports"]); -// }); -// }); - -// it("should contain only webpackExports from module in eager mode", function () { -// return import( -// /* -// webpackMode: "eager", -// webpackExports: ["a", "usedExports", "b"] -// */ "./dir12/a?3" -// ).then(module => { -// expect(module.usedExports).toEqual(["a", "b", "usedExports"]); -// }); -// }); - -// it("should contain webpackExports from module in weak mode", function () { -// require.resolve("./dir12/a?4"); -// return import( -// /* -// webpackMode: "weak", -// webpackExports: ["a", "usedExports", "b"] -// */ "./dir12/a?4" -// ).then(module => { -// expect(module.usedExports).toEqual(["a", "b", "usedExports"]); -// }); -// }); - -// it("should not mangle webpackExports from module", function () { -// return import(/* webpackExports: "longnameforexport" */ "./dir12/a?5").then( -// module => { -// expect(module).toHaveProperty("longnameforexport"); -// } -// ); -// }); - -// it("should not mangle default webpackExports from module", function () { -// return import(/* webpackExports: "default" */ "./dir12/a?6").then( -// module => { -// expect(module).toHaveProperty("default"); -// } -// ); -// }); - -// it("should contain only webpackExports from module in context mode", function () { -// const x = "b"; -// return import(/* webpackExports: "usedExports" */ `./dir13/${x}`).then( -// module => { -// expect(module.usedExports).toEqual(["usedExports"]); -// } -// ); -// }); -// } +if (process.env.NODE_ENV === "production") { + it("should contain only one export from webpackExports from module", function () { + return import(/* webpackExports: "usedExports" */ "./dir12/a?1").then( + module => { + expect(module.usedExports).toEqual(["usedExports"]); + } + ); + }); + + it("should contain only webpackExports from module", function () { + return import( + /* webpackExports: ["a", "usedExports", "b"] */ "./dir12/a?2" + ).then(module => { + expect(module.usedExports).toEqual(["a", "b", "usedExports"]); + }); + }); + + it("should contain only webpackExports from module in eager mode", function () { + return import( + /* + webpackMode: "eager", + webpackExports: ["a", "usedExports", "b"] + */ "./dir12/a?3" + ).then(module => { + expect(module.usedExports).toEqual(["a", "b", "usedExports"]); + }); + }); + + it("should contain webpackExports from module in weak mode", function () { + require.resolve("./dir12/a?4"); + return import( + /* + webpackMode: "weak", + webpackExports: ["a", "usedExports", "b"] + */ "./dir12/a?4" + ).then(module => { + expect(module.usedExports).toEqual(["a", "b", "usedExports"]); + }); + }); + + it("should not mangle webpackExports from module", function () { + return import(/* webpackExports: "longnameforexport" */ "./dir12/a?5").then( + module => { + expect(module).toHaveProperty("longnameforexport"); + } + ); + }); + + it("should not mangle default webpackExports from module", function () { + return import(/* webpackExports: "default" */ "./dir12/a?6").then( + module => { + expect(module).toHaveProperty("default"); + } + ); + }); + + it("should contain only webpackExports from module in context mode", function () { + const x = "b"; + return import(/* webpackExports: "usedExports" */ `./dir13/${x}`).then( + module => { + expect(module.usedExports).toEqual(["usedExports"]); + } + ); + }); +} function testChunkLoading(load, expectedSyncInitial, expectedSyncRequested) { var sync = false; diff --git a/tests/webpack-test/cases/chunks/inline-options/test.filter.js b/tests/webpack-test/cases/chunks/inline-options/test.filter.js deleted file mode 100644 index cf71dbbe1954..000000000000 --- a/tests/webpack-test/cases/chunks/inline-options/test.filter.js +++ /dev/null @@ -1,6 +0,0 @@ -const { FilteredStatus } = require("../../../lib/util/filterUtil"); - -module.exports = () => [ - FilteredStatus.PARTIAL_PASS, - "support magic comment `webpackExports`" -]; diff --git a/website/docs/en/api/modules/module-methods.mdx b/website/docs/en/api/modules/module-methods.mdx index 3c43a2410bfc..d21bae585582 100644 --- a/website/docs/en/api/modules/module-methods.mdx +++ b/website/docs/en/api/modules/module-methods.mdx @@ -107,6 +107,7 @@ Inline comments to make features work. By adding comments to the import, we can import( /* webpackChunkName: "my-chunk-name" */ /* webpackMode: "lazy" */ + /* webpackExports: ["default", "named"] */ /* webpackFetchPriority: "high" */ 'module' ); @@ -200,6 +201,14 @@ A regular expression that will be matched against during import resolution. Any Note that `webpackInclude` and `webpackExclude` options do not interfere with the prefix. eg: `./locale`. ::: +##### webpackExports + + + +- **Type:**: `string | string[]` + +Tells webpack to only bundle the specified exports of a dynamically `import()`ed module. It can decrease the output size of a chunk. + ## CommonJS Rspack is also support `CommonJS` syntax natively, you can use `require` and `module.exports` methods. diff --git a/website/docs/zh/api/modules/module-methods.mdx b/website/docs/zh/api/modules/module-methods.mdx index 4123736a91f1..3db83abedb51 100644 --- a/website/docs/zh/api/modules/module-methods.mdx +++ b/website/docs/zh/api/modules/module-methods.mdx @@ -198,6 +198,14 @@ import( 请注意,`webpackInclude` 和 `webpackExclude` 选项不会影响前缀。例如:`./locale`。 ::: +##### webpackExports + + + +- **Type:**: `string | string[]` + +使 Rspack 在处理该动态 `import()` 模块时仅打包指定的导出。这样可以降低 chunk 的产物体积。 + ## CommonJS Rspack 也支持 `CommonJS` 语法,可以使用 `require` 和 `module.exports` 语法。