Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support webpackExports in magic comments #7198

Merged
merged 1 commit into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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::<Vec<_>>()
});

let param = parser.evaluate_expression(dyn_imported.expr.as_ref());

Expand All @@ -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);
Expand All @@ -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,
Expand Down
56 changes: 51 additions & 5 deletions crates/rspack_plugin_javascript/src/webpack_comment.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Captures;
use rspack_error::miette::{Diagnostic, Severity};
Expand All @@ -20,6 +21,7 @@ pub enum WebpackComment {
ExcludeRegexp,
ExcludeFlags,
Mode,
Exports,
}

#[derive(Debug)]
Expand Down Expand Up @@ -95,6 +97,13 @@ impl WebpackCommentMap {
})
})
}

pub fn get_webpack_exports(&self) -> Option<Vec<String>> {
self
.0
.get(&WebpackComment::Exports)
.map(|expr| expr.split(',').map(|x| x.to_owned()).collect_vec())
}
}

fn add_magic_comment_warning(
Expand Down Expand Up @@ -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<regex::Regex> = 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<regex::Regex> =
Lazy::new(|| regex::Regex::new(r#"^["`'](\w+)["`']$"#).expect("invalid regex"));

pub fn try_extract_webpack_magic_comment(
source_file: &SourceFile,
comments: &Option<&dyn Comments>,
Expand Down Expand Up @@ -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,
);
}
_ => {}
}
}
}
Expand Down
130 changes: 65 additions & 65 deletions tests/webpack-test/cases/chunks/inline-options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 0 additions & 6 deletions tests/webpack-test/cases/chunks/inline-options/test.filter.js

This file was deleted.

9 changes: 9 additions & 0 deletions website/docs/en/api/modules/module-methods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
Expand Down Expand Up @@ -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

<ApiMeta addedVersion="1.0.0" />

- **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.
Expand Down
8 changes: 8 additions & 0 deletions website/docs/zh/api/modules/module-methods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ import(
请注意,`webpackInclude` 和 `webpackExclude` 选项不会影响前缀。例如:`./locale`。
:::

##### webpackExports

<ApiMeta addedVersion="1.0.0" />

- **Type:**: `string | string[]`

使 Rspack 在处理该动态 `import()` 模块时仅打包指定的导出。这样可以降低 chunk 的产物体积。

## CommonJS

Rspack 也支持 `CommonJS` 语法,可以使用 `require` 和 `module.exports` 语法。
Expand Down
Loading