Skip to content

Commit

Permalink
refactor(crates/rspack_core): align module rule test and resource
Browse files Browse the repository at this point in the history
… with webpack (web-infra-dev#1254)

* refactor: rule matcher

* refactor: impl object

* test: use regex

* test: add resource and test

* test: update rust test

* chore: safe test

* chore: remove debug

* test: more tests

* test: polish test

* test: add more tests

* test: better testing

* test: fix test

* chore: remove dirty hack
  • Loading branch information
h-a-n-a authored Nov 24, 2022
1 parent 152bd89 commit 2468d7a
Show file tree
Hide file tree
Showing 60 changed files with 41,005 additions and 207 deletions.
91 changes: 61 additions & 30 deletions crates/rspack_binding_options/src/options/raw_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,66 @@ impl Debug for RawModuleRuleUse {
}
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[cfg(feature = "node-api")]
#[napi(object)]
pub struct RawModuleRuleCondition {
/// Condition can be either a `string` or `Regexp`.
#[napi(ts_type = r#""string" | "regexp""#)]
pub r#type: String,
/// Based on the condition type, the value can be either a `string` or `Regexp`.
/// - "string": The value will be matched against the string.
/// - "regexp": The value will be matched against the raw regexp source from JS side.
pub matcher: Option<String>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[cfg(not(feature = "node-api"))]
pub struct RawModuleRuleCondition {
pub r#type: String,
pub matcher: Option<String>,
}

impl TryFrom<RawModuleRuleCondition> for rspack_core::ModuleRuleCondition {
type Error = anyhow::Error;

fn try_from(x: RawModuleRuleCondition) -> std::result::Result<Self, Self::Error> {
let matcher = x
.matcher
.ok_or_else(|| anyhow::anyhow!("Matcher is required."))?;

let result = match x.r#type.as_str() {
"string" => Self::String(matcher),
"regexp" => Self::Regexp(regex::Regex::new(&matcher)?),
_ => {
anyhow::bail!(
"Failed to resolve the condition type {}. Expected type is either `string` or `regexp`.",
x.r#type
);
}
};

Ok(result)
}
}

#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg(feature = "node-api")]
#[napi(object)]
pub struct RawModuleRule {
pub test: Option<String>,
pub resource: Option<String>,
pub resource_query: Option<String>,
/// A condition matcher matching an absolute path.
/// - String: To match the input must start with the provided string. I. e. an absolute directory path, or absolute path to the file.
/// - Regexp: It's tested with the input.
pub test: Option<RawModuleRuleCondition>,
/// A condition matcher matching an absolute path.
/// See `test` above
pub resource: Option<RawModuleRuleCondition>,
/// A condition matcher against the resource query.
/// TODO: align with webpack's `?` prefixed `resourceQuery`
pub resource_query: Option<RawModuleRuleCondition>,
// Loader experimental
#[serde(skip_deserializing)]
pub func__: Option<JsFunction>,
Expand All @@ -95,9 +147,9 @@ pub struct RawModuleRule {
#[serde(rename_all = "camelCase")]
#[cfg(not(feature = "node-api"))]
pub struct RawModuleRule {
pub test: Option<String>,
pub resource: Option<String>,
pub resource_query: Option<String>,
pub test: Option<RawModuleRuleCondition>,
pub resource: Option<RawModuleRuleCondition>,
pub resource_query: Option<RawModuleRuleCondition>,
// Loader experimental
#[serde(skip_deserializing)]
pub func__: Option<()>,
Expand Down Expand Up @@ -299,23 +351,8 @@ pub struct LoaderResult {
pub meta: Option<Vec<u8>>,
}

type LoaderThreadsafeLoaderContext = LoaderContext;
pub type LoaderThreadsafeLoaderResult = Option<LoaderResult>;

#[derive(Serialize, Deserialize, Debug)]
struct LoaderThreadsafeResult {
id: u32,
// payload
p: LoaderThreadsafeLoaderResult,
}

#[derive(Serialize, Deserialize, Debug)]
struct LoaderThreadsafeContext {
id: u32,
// payload
p: LoaderThreadsafeLoaderContext,
}

impl RawOption<ModuleRule> for RawModuleRule {
fn to_compiler_option(self, _options: &CompilerOptionsBuilder) -> anyhow::Result<ModuleRule> {
// Even this part is using the plural version of loader, it's recommended to use singular version from js side to reduce overhead (This behavior maybe changed later for advanced usage).
Expand Down Expand Up @@ -398,15 +435,9 @@ impl RawOption<ModuleRule> for RawModuleRule {
// let module_rule_tsfn: &'static Option<ModuleRuleFunc> = Box::leak(func);

Ok(ModuleRule {
test: self.test.map(|reg| regex::Regex::new(&reg)).transpose()?,
resource_query: self
.resource_query
.map(|reg| regex::Regex::new(&reg))
.transpose()?,
resource: self
.resource
.map(|reg| regex::Regex::new(&reg))
.transpose()?,
test: self.test.map(|raw| raw.try_into()).transpose()?,
resource_query: self.resource_query.map(|raw| raw.try_into()).transpose()?,
resource: self.resource.map(|raw| raw.try_into()).transpose()?,
uses,
module_type,
// Loader experimental
Expand Down
50 changes: 13 additions & 37 deletions crates/rspack_core/src/compiler/compilation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use rspack_sources::BoxSource;
use ustr::{ustr, Ustr};

use crate::{
is_source_equal, join_string_component,
is_source_equal, join_string_component, module_rule_matcher,
split_chunks::code_splitting,
tree_shaking::{
visitor::{ModuleRefAnalyze, SymbolRef, TreeShakingResult},
Expand Down Expand Up @@ -454,50 +454,26 @@ impl Compilation {
let resource_data = normal_module.resource_resolved_data();

match compiler_options
.module
.rules
.iter()
.filter_map(|module_rule| -> Option<Result<&ModuleRule>> {
if let Some(func) = &module_rule.func__ {
match func(resource_data) {
Ok(result) => {
if result {
return Some(Ok(module_rule));
}
return None
},
Err(e) => {
return Some(Err(e.into()))
}
.module
.rules
.iter()
.filter_map(|module_rule| -> Option<Result<&ModuleRule>> {
match module_rule_matcher(module_rule, resource_data) {
Ok(val) => val.then_some(Ok(module_rule)),
Err(err) => Some(Err(err)),
}
}

// Include all modules that pass test assertion. If you supply a Rule.test option, you cannot also supply a `Rule.resource`.
// See: https://webpack.js.org/configuration/module/#ruletest
if let Some(test_rule) = &module_rule.test && test_rule.is_match(&resource_data.resource) {
return Some(Ok(module_rule));
} else if let Some(resource_rule) = &module_rule.resource && resource_rule.is_match(&resource_data.resource) {
return Some(Ok(module_rule));
}

if let Some(resource_query_rule) = &module_rule.resource_query && let Some(resource_query) = &resource_data.resource_query && resource_query_rule.is_match(resource_query) {
return Some(Ok(module_rule));
}

None
})
.collect::<Result<Vec<_>>>() {
})
.collect::<Result<Vec<_>>>()
{
Ok(result) => result,
Err(err) => {
// If build error message is failed to send, then we should manually decrease the active task count
// Otherwise, it will be gracefully handled by the error message handler.
if let Err(err) =
tx.send(Msg::ModuleBuiltErrorEncountered(module_identifier, err))
{
if let Err(err) = tx.send(Msg::ModuleBuiltErrorEncountered(module_identifier, err)) {
active_task_count.fetch_sub(1, Ordering::SeqCst);
tracing::trace!("fail to send msg {:?}", err)
}
return
return;
}
}
} else {
Expand Down
38 changes: 6 additions & 32 deletions crates/rspack_core/src/normal_module_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use rspack_error::{Diagnostic, Error, Result, TWithDiagnosticArray};
use tracing::instrument;

use crate::{
parse_to_url, resolve, BoxModule, CompilerOptions, FactorizeArgs, ModuleExt, ModuleGraphModule,
ModuleIdentifier, ModuleRule, ModuleType, Msg, NormalModule, RawModule, ResolveArgs,
ResolveResult, ResourceData, SharedPluginDriver, DEPENDENCY_ID,
module_rule_matcher, parse_to_url, resolve, BoxModule, CompilerOptions, FactorizeArgs, ModuleExt,
ModuleGraphModule, ModuleIdentifier, ModuleRule, ModuleType, Msg, NormalModule, RawModule,
ResolveArgs, ResolveResult, ResourceData, SharedPluginDriver, DEPENDENCY_ID,
};

#[derive(Debug, Hash, PartialEq, Eq, Clone)]
Expand Down Expand Up @@ -276,43 +276,17 @@ impl NormalModuleFactory {
.rules
.iter()
.filter_map(|module_rule| -> Option<Result<&ModuleRule>> {
if let Some(func) = &module_rule.func__ {
match func(resource_data) {
Ok(result) => {
if result {
return Some(Ok(module_rule));
}

return None
},
Err(e) => {
return Some(Err(e.into()))
}
}
}

// Include all modules that pass test assertion. If you supply a Rule.test option, you cannot also supply a `Rule.resource`.
// See: https://webpack.js.org/configuration/module/#ruletest
if let Some(test_rule) = &module_rule.test && test_rule.is_match(&resource_data.resource) {
return Some(Ok(module_rule));
} else if let Some(resource_rule) = &module_rule.resource && resource_rule.is_match(&resource_data.resource) {
return Some(Ok(module_rule));
}

if let Some(resource_query_rule) = &module_rule.resource_query && let Some(resource_query) = &resource_data.resource_query && resource_query_rule.is_match(resource_query) {
return Some(Ok(module_rule));
match module_rule_matcher(module_rule, resource_data) {
Ok(val) => val.then_some(Ok(module_rule)),
Err(err) => Some(Err(err)),
}


None
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.for_each(|module_rule| {
if module_rule.module_type.is_some() {
resolved_module_type = module_rule.module_type;
};

});

resolved_module_type.ok_or_else(|| {
Expand Down
26 changes: 21 additions & 5 deletions crates/rspack_core/src/options/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,31 @@ pub struct ParserOptions {

type ModuleRuleFunc = Box<dyn Fn(&ResourceData) -> anyhow::Result<bool> + Send + Sync>;

#[derive(Debug)]
pub enum ModuleRuleCondition {
String(String),
Regexp(regex::Regex),
// TODO: support logical conditions
// LogicalConditions
}

#[derive(Default)]
pub struct ModuleRule {
pub test: Option<regex::Regex>,
pub resource: Option<regex::Regex>,
pub resource_query: Option<regex::Regex>,
/// A condition matcher matching an absolute path.
/// - String: To match the input must start with the provided string. I. e. an absolute directory path, or absolute path to the file.
/// - Regexp: It's tested with the input.
pub test: Option<ModuleRuleCondition>,
/// A condition matcher matching an absolute path.
/// See `test` above
pub resource: Option<ModuleRuleCondition>,
/// A condition matcher against the resource query.
/// TODO: align with webpack's `?` prefixed `resourceQuery`
pub resource_query: Option<ModuleRuleCondition>,
/// The `ModuleType` to use for the matched resource.
pub module_type: Option<ModuleType>,
// For loader experimental
pub func__: Option<ModuleRuleFunc>,
pub uses: Vec<BoxedLoader>,
/// Internal matching method, not intended to be used by the user. (Loader experimental)
pub func__: Option<ModuleRuleFunc>,
}

impl Debug for ModuleRule {
Expand Down
3 changes: 3 additions & 0 deletions crates/rspack_core/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub use hash::*;
mod tree_shaking;
pub use tree_shaking::*;

mod module_rules;
pub use module_rules::*;

pub static PATH_START_BYTE_POS_MAP: Lazy<Arc<DashMap<String, u32>>> =
Lazy::new(|| Arc::new(DashMap::new()));

Expand Down
56 changes: 56 additions & 0 deletions crates/rspack_core/src/utils/module_rules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use rspack_error::Result;
use rspack_loader_runner::ResourceData;

use crate::{ModuleRule, ModuleRuleCondition};

/// Match the condition against the given `data`. Returns `true` if the condition matches.
fn module_rule_matcher_condition(condition: &ModuleRuleCondition, data: &str) -> bool {
match condition {
ModuleRuleCondition::String(s) => data.starts_with(s),
ModuleRuleCondition::Regexp(r) => r.is_match(data),
}
}

/// Match the `ModuleRule` against the given `ResourceData`, and return the matching `ModuleRule` if matched.
pub fn module_rule_matcher(module_rule: &ModuleRule, resource_data: &ResourceData) -> Result<bool> {
// Internal function to match the condition against the given `data`.
if let Some(func) = &module_rule.func__ {
match func(resource_data) {
Ok(result) => return Ok(result),
Err(e) => return Err(e.into()),
}
}

if module_rule.test.is_none()
&& module_rule.resource.is_none()
&& module_rule.resource_query.is_none()
{
return Err(rspack_error::Error::InternalError(
"ModuleRule must have at least one condition".to_owned(),
));
}

// Include all modules that pass test assertion. If you supply a Rule.test option, you cannot also supply a `Rule.resource`.
// See: https://webpack.js.org/configuration/module/#ruletest
if let Some(test_rule) = &module_rule.test {
if !module_rule_matcher_condition(test_rule, &resource_data.resource_path) {
return Ok(false);
}
} else if let Some(resource_rule) = &module_rule.resource {
if !module_rule_matcher_condition(resource_rule, &resource_data.resource_path) {
return Ok(false);
}
}

if let Some(resource_query_rule) = &module_rule.resource_query {
if let Some(resource_query) = &resource_data.resource_query {
if !module_rule_matcher_condition(resource_query_rule, resource_query) {
return Ok(false);
}
} else {
return Ok(false);
}
}

Ok(true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ module.exports = {
module: {
rules: [
{
test: "\\.s[ac]ss$",
test: {
type: "regexp",
matcher: "\\.s[ac]ss$"
},
uses: [{ builtinLoader: "sass-loader" }],
type: "css"
}
Expand Down
Loading

0 comments on commit 2468d7a

Please sign in to comment.