Skip to content

Commit

Permalink
feat: support webpackExports in magic comments
Browse files Browse the repository at this point in the history
  • Loading branch information
LingyuCoder committed Jul 17, 2024
1 parent 2fe5430 commit 93c6458
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 80 deletions.
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

0 comments on commit 93c6458

Please sign in to comment.