1use std::{borrow::Cow, fmt, sync::LazyLock};
2
3use regex::Regex;
4use rspack_core::{ContextMode, DependencyRange};
5use rspack_error::{Diagnostic, Error, Severity};
6use rspack_regex::RspackRegex;
7use rspack_util::SpanExt;
8use rustc_hash::{FxHashMap, FxHashSet};
9use swc_experimental_allocator::Allocator;
10use swc_experimental_ecma_ast::{
11 Comment, CommentKind, EsVersion, Expr, GetSpan, Lit, Prop, PropName, PropOrSpread, Span, UnaryOp,
12};
13use swc_experimental_ecma_parser::{EsSyntax, Syntax, parse_file_as_expr};
14use swc_experimental_ecma_transforms_base::remove_paren::remove_paren;
15
16use crate::{
17 utils::object_properties::FromAstExpr,
18 visitors::{JavascriptParser, create_traceable_error},
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum RspackComment {
23 ChunkName,
24 Prefetch,
25 Preload,
26 Ignore,
27 FetchPriority,
28 IncludeRegexp,
29 ExcludeRegexp,
30 Mode,
31 Exports,
32}
33
34impl RspackComment {
35 fn prefixed_name(self, prefix: MagicCommentPrefix) -> String {
36 format!("{prefix}{self}")
37 }
38}
39
40impl fmt::Display for RspackComment {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.write_str(match self {
43 Self::ChunkName => "ChunkName",
44 Self::Prefetch => "Prefetch",
45 Self::Preload => "Preload",
46 Self::Ignore => "Ignore",
47 Self::FetchPriority => "FetchPriority",
48 Self::IncludeRegexp => "Include",
49 Self::ExcludeRegexp => "Exclude",
50 Self::Mode => "Mode",
51 Self::Exports => "Exports",
52 })
53 }
54}
55
56impl TryFrom<&str> for RspackComment {
57 type Error = ();
58
59 fn try_from(value: &str) -> Result<Self, Self::Error> {
60 match value {
61 "ChunkName" => Ok(Self::ChunkName),
62 "Prefetch" => Ok(Self::Prefetch),
63 "Preload" => Ok(Self::Preload),
64 "Ignore" => Ok(Self::Ignore),
65 "FetchPriority" => Ok(Self::FetchPriority),
66 "Include" => Ok(Self::IncludeRegexp),
67 "Exclude" => Ok(Self::ExcludeRegexp),
68 "Mode" => Ok(Self::Mode),
69 "Exports" => Ok(Self::Exports),
70 _ => Err(()),
71 }
72 }
73}
74
75#[derive(Debug, PartialEq, Eq)]
76pub enum MagicCommentValue {
77 Bool(bool),
78 String(String),
79 Number(String),
80 RegExp { source: String, flags: String },
81 Array(Vec<String>),
82 Unknown,
83}
84
85#[derive(Debug)]
86pub struct RspackCommentMap(FxHashMap<RspackComment, MagicCommentItem>);
87
88#[derive(Debug, Clone, Copy)]
89pub struct RawMagicComment<'a> {
90 pub text: &'a str,
91 pub span: DependencyRange,
92}
93
94impl RspackCommentMap {
95 fn new() -> Self {
96 Self(Default::default())
97 }
98
99 fn insert(&mut self, key: RspackComment, value: MagicCommentItem) {
100 self.0.insert(key, value);
101 }
102
103 fn push_conflict_warning(
104 source: &str,
105 ignored_comment_name: impl fmt::Display,
106 preferred_comment_name: impl fmt::Display,
107 item: &MagicCommentItem,
108 warning_diagnostics: &mut Vec<Diagnostic>,
109 ) {
110 let mut error: Error = create_traceable_error(
111 "Magic comments conflict".into(),
112 format!(
113 "`{ignored_comment_name}` is ignored because `{preferred_comment_name}` is also specified. Prefer `{preferred_comment_name}`."
114 ),
115 source.to_owned(),
116 item.span,
117 );
118 error.severity = Severity::Warning;
119 error.hide_stack = Some(true);
120 warning_diagnostics.push(error.into())
121 }
122
123 fn insert_with_conflict_warning(
124 &mut self,
125 source: &str,
126 rspack_comment: RspackComment,
127 item: MagicCommentItem,
128 warning_diagnostics: &mut Vec<Diagnostic>,
129 ) {
130 if let Some(existing) = self.0.get_mut(&rspack_comment) {
131 match (existing.prefix, item.prefix) {
132 (MagicCommentPrefix::Rspack, MagicCommentPrefix::Webpack) => {
133 Self::push_conflict_warning(
134 source,
135 rspack_comment.prefixed_name(MagicCommentPrefix::Webpack),
136 rspack_comment.prefixed_name(MagicCommentPrefix::Rspack),
137 &item,
138 warning_diagnostics,
139 );
140 }
141 (MagicCommentPrefix::Webpack, MagicCommentPrefix::Rspack) => {
142 Self::push_conflict_warning(
143 source,
144 rspack_comment.prefixed_name(MagicCommentPrefix::Webpack),
145 rspack_comment.prefixed_name(MagicCommentPrefix::Rspack),
146 existing,
147 warning_diagnostics,
148 );
149 *existing = item;
150 }
151 _ => {
152 Self::push_conflict_warning(
153 source,
154 rspack_comment.prefixed_name(item.prefix),
155 rspack_comment.prefixed_name(existing.prefix),
156 &item,
157 warning_diagnostics,
158 );
159 }
160 }
161 } else {
162 self.insert(rspack_comment, item);
163 }
164 }
165
166 pub fn get_ignore_value(&self) -> Option<&MagicCommentValue> {
167 self.0.get(&RspackComment::Ignore).map(|item| &item.value)
168 }
169
170 pub fn get_mode(&self) -> Option<&String> {
171 match self.0.get(&RspackComment::Mode).map(|item| &item.value) {
172 Some(MagicCommentValue::String(value)) => Some(value),
173 _ => None,
174 }
175 }
176
177 pub fn get_chunk_name(&self) -> Option<&String> {
178 match self
179 .0
180 .get(&RspackComment::ChunkName)
181 .map(|item| &item.value)
182 {
183 Some(MagicCommentValue::String(value)) => Some(value),
184 _ => None,
185 }
186 }
187
188 pub fn get_prefetch(&self) -> Option<Cow<'_, str>> {
189 match self.0.get(&RspackComment::Prefetch).map(|item| &item.value) {
190 Some(MagicCommentValue::Bool(true)) => Some(Cow::Borrowed("true")),
191 Some(MagicCommentValue::Number(value)) => Some(Cow::Borrowed(value.as_str())),
192 _ => None,
193 }
194 }
195
196 pub fn get_preload(&self) -> Option<Cow<'_, str>> {
197 match self.0.get(&RspackComment::Preload).map(|item| &item.value) {
198 Some(MagicCommentValue::Bool(true)) => Some(Cow::Borrowed("true")),
199 Some(MagicCommentValue::Number(value)) => Some(Cow::Borrowed(value.as_str())),
200 _ => None,
201 }
202 }
203
204 pub fn get_ignore(&self) -> Option<bool> {
205 match self.0.get(&RspackComment::Ignore).map(|item| &item.value) {
206 Some(MagicCommentValue::Bool(value)) => Some(*value),
207 _ => None,
208 }
209 }
210
211 pub fn get_fetch_priority(&self) -> Option<&String> {
212 match self
213 .0
214 .get(&RspackComment::FetchPriority)
215 .map(|item| &item.value)
216 {
217 Some(MagicCommentValue::String(value)) => Some(value),
218 _ => None,
219 }
220 }
221
222 pub fn get_include(&self) -> Option<RspackRegex> {
223 self
224 .0
225 .get(&RspackComment::IncludeRegexp)
226 .and_then(|item| match &item.value {
227 MagicCommentValue::RegExp { source, flags } => {
228 Some(RspackRegex::with_flags(source, flags).unwrap_or_else(|_| {
229 unreachable!();
231 }))
232 }
233 _ => None,
234 })
235 }
236
237 pub fn get_exclude(&self) -> Option<RspackRegex> {
238 self
239 .0
240 .get(&RspackComment::ExcludeRegexp)
241 .and_then(|item| match &item.value {
242 MagicCommentValue::RegExp { source, flags } => {
243 Some(RspackRegex::with_flags(source, flags).unwrap_or_else(|_| {
244 unreachable!();
246 }))
247 }
248 _ => None,
249 })
250 }
251
252 pub fn get_exports(&self) -> Option<Vec<String>> {
253 match self.0.get(&RspackComment::Exports).map(|item| &item.value) {
254 Some(MagicCommentValue::String(value)) => Some(vec![value.clone()]),
255 Some(MagicCommentValue::Array(value)) => Some(value.clone()),
256 _ => None,
257 }
258 }
259}
260
261fn push_magic_comment_parse_warning(
262 source: &str,
263 comment_name: impl fmt::Display,
264 comment_type: &str,
265 received: &str,
266 warning_diagnostics: &mut Vec<Diagnostic>,
267 span: DependencyRange,
268) {
269 let mut error: Error = create_traceable_error(
270 "Magic comments parse failed".into(),
271 format!("`{comment_name}` expected {comment_type}, but received: {received}."),
272 source.to_owned(),
273 span,
274 );
275 error.severity = Severity::Warning;
276 error.hide_stack = Some(true);
277 warning_diagnostics.push(error.into())
278}
279
280pub fn try_extract_magic_comment(
281 parser: &mut JavascriptParser,
282 error_span: Span,
283 span: Span,
284) -> RspackCommentMap {
285 let mut result = RspackCommentMap::new();
286 let mut warning_diagnostics = Vec::new();
287 let allocator = parser.ast.allocator;
288 if let Some(comments) = parser.ast.comments.leading.get(&span.start) {
289 analyze_comments(
290 allocator,
291 parser.source,
292 comments,
293 error_span,
294 &mut warning_diagnostics,
295 &mut result,
296 );
297 }
298 if let Some(comments) = parser.ast.comments.trailing.get(&span.end) {
299 analyze_comments(
300 allocator,
301 parser.source,
302 comments,
303 error_span,
304 &mut warning_diagnostics,
305 &mut result,
306 );
307 }
308 parser.add_warnings(warning_diagnostics);
309 result
310}
311
312fn value_span_to_error_span_with_offset(
318 comment_span: DependencyRange,
319 value_span: Span,
320 comment_offset: usize,
321) -> Option<DependencyRange> {
322 const BLOCK_COMMENT_START_LEN: usize = 2; const OBJECT_LITERAL_PREFIX_LEN: usize = 2; let value_start = value_span.real_lo() as usize;
329 let value_end = value_span.real_hi() as usize;
330 if value_start < OBJECT_LITERAL_PREFIX_LEN || value_end < OBJECT_LITERAL_PREFIX_LEN {
331 return None;
332 }
333
334 let comment_start = comment_span.start as usize;
335 let start = comment_start + BLOCK_COMMENT_START_LEN + comment_offset + value_start
336 - OBJECT_LITERAL_PREFIX_LEN;
337 let end = comment_start + BLOCK_COMMENT_START_LEN + comment_offset + value_end
338 - OBJECT_LITERAL_PREFIX_LEN;
339
340 Some(DependencyRange::new(start as u32, end as u32))
341}
342
343fn value_span_to_comment_offsets_with_offset(
344 comment_text: &str,
345 value_span: Span,
346 comment_offset: usize,
347) -> Option<(usize, usize)> {
348 const OBJECT_LITERAL_PREFIX_LEN: usize = 2; let start = comment_offset
351 + value_span
352 .real_lo()
353 .checked_sub(OBJECT_LITERAL_PREFIX_LEN as u32)? as usize;
354 let end = comment_offset
355 + value_span
356 .real_hi()
357 .checked_sub(OBJECT_LITERAL_PREFIX_LEN as u32)? as usize;
358
359 (start <= end && end <= comment_text.len()).then_some((start, end))
360}
361
362fn raw_value_with_offset<'a>(
363 comment_text: &'a str,
364 value: &Expr,
365 comment_offset: usize,
366) -> Option<&'a str> {
367 let (start, end) =
368 value_span_to_comment_offsets_with_offset(comment_text, value.span(), comment_offset)?;
369 comment_text.get(start..end).map(str::trim)
370}
371
372fn parse_magic_comment_object<'a>(
373 allocator: &'a Allocator,
374 comment_text: &str,
375) -> Option<(Expr<'a>, usize)> {
376 let magic_comment_start = find_magic_comment_start(comment_text)?;
377 let comment_text = comment_text.get(magic_comment_start..)?;
378 let source = format!("({{{comment_text}}})");
379 let source = allocator.alloc_str(&source);
380 let mut expr = parse_file_as_expr(
381 allocator,
382 source,
383 Syntax::Es(EsSyntax::default()),
384 EsVersion::EsNext,
385 None,
386 )
387 .ok()?;
388 remove_paren(&mut expr, allocator, None);
389 Some((expr, magic_comment_start))
390}
391
392static WEBPACK_COMMENT_REGEXP: LazyLock<Regex> = LazyLock::new(|| {
393 Regex::new(
394 r#"(^|[^\w])(?P<key>(?:webpack|rspack)[A-Z][A-Za-z]+|"(?:webpack|rspack)[A-Z][A-Za-z]+"|'(?:webpack|rspack)[A-Z][A-Za-z]+')\s*:"#,
395 )
396 .expect("invalid regex")
397});
398
399fn find_magic_comment_start(comment_text: &str) -> Option<usize> {
400 WEBPACK_COMMENT_REGEXP
401 .captures(comment_text)
402 .and_then(|captures| captures.name("key").map(|matched| matched.start()))
403}
404
405fn prop_name_to_str<'a>(name: &'a PropName<'a>) -> Option<Cow<'a, str>> {
406 match name {
407 PropName::Ident(ident) => Some(Cow::Borrowed(ident.sym.as_str())),
408 PropName::Str(str) => Some(str.value.to_string_lossy()),
409 _ => None,
410 }
411}
412
413fn expr_to_str<'a>(expr: &'a Expr<'a>) -> Option<Cow<'a, str>> {
414 match expr {
415 Expr::Lit(lit) => match &**lit {
416 Lit::Str(str) => Some(str.value.to_string_lossy()),
417 _ => None,
418 },
419 Expr::Tpl(tpl) if tpl.exprs.is_empty() && tpl.quasis.len() == 1 => {
420 tpl.quasis.first().map(|el| Cow::Borrowed(el.raw.as_ref()))
421 }
422 _ => None,
423 }
424}
425
426fn expr_to_bool(expr: &Expr) -> Option<bool> {
427 match expr {
428 Expr::Lit(lit) => match &**lit {
429 Lit::Bool(bool) => Some(bool.value),
430 _ => None,
431 },
432 _ => None,
433 }
434}
435
436fn is_number_expr(expr: &Expr) -> bool {
437 match expr {
438 Expr::Lit(lit) => matches!(&**lit, Lit::Num(_)),
439 Expr::Unary(unary) if matches!(unary.op, UnaryOp::Minus) => {
440 matches!(&unary.arg, Expr::Lit(lit) if matches!(&**lit, Lit::Num(_)))
441 }
442 _ => false,
443 }
444}
445
446#[cfg(test)]
447fn expr_to_order_str<'a>(comment_text: &'a str, expr: &Expr) -> Option<&'a str> {
448 expr_to_order_str_with_offset(comment_text, expr, 0)
449}
450
451fn expr_to_order_str_with_offset<'a>(
452 comment_text: &'a str,
453 expr: &Expr,
454 comment_offset: usize,
455) -> Option<&'a str> {
456 if expr_to_bool(expr).is_some() || is_number_expr(expr) {
457 raw_value_with_offset(comment_text, expr, comment_offset)
458 } else {
459 None
460 }
461}
462
463fn expr_to_regexp<'a>(expr: &'a Expr<'a>) -> Option<(&'a str, &'a str)> {
464 match expr {
465 Expr::Lit(lit) => match &**lit {
466 Lit::Regex(regex) => Some((regex.exp.as_str(), regex.flags.as_str())),
467 _ => None,
468 },
469 _ => None,
470 }
471}
472
473fn expr_to_magic_comment_value_with_offset(
474 comment_text: &str,
475 expr: &Expr,
476 comment_offset: usize,
477) -> Option<MagicCommentValue> {
478 if let Some(value) = expr_to_bool(expr) {
479 return Some(MagicCommentValue::Bool(value));
480 }
481
482 if let Some(value) = expr_to_str(expr) {
483 return Some(MagicCommentValue::String(value.into_owned()));
484 }
485
486 if is_number_expr(expr) {
487 return raw_value_with_offset(comment_text, expr, comment_offset)
488 .map(|value| MagicCommentValue::Number(value.to_string()));
489 }
490
491 if let Some((source, flags)) = expr_to_regexp(expr) {
492 return Some(MagicCommentValue::RegExp {
493 source: source.to_string(),
494 flags: flags.to_string(),
495 });
496 }
497
498 let Expr::Array(array) = expr else {
499 return None;
500 };
501 let mut items = Vec::new();
502 for elem in &array.elems {
503 let elem = elem.as_ref()?;
504 if elem.spread.is_some() {
505 return None;
506 }
507 items.push(expr_to_str(&elem.expr)?.into_owned());
508 }
509 Some(MagicCommentValue::Array(items))
510}
511
512fn expr_to_exports(expr: &Expr) -> Option<MagicCommentValue> {
513 if let Some(string) = expr_to_str(expr) {
514 let trimmed = string.trim();
515 if trimmed.len() == string.len() {
516 return Some(MagicCommentValue::String(string.into_owned()));
517 }
518 return Some(MagicCommentValue::String(trimmed.to_string()));
519 }
520
521 let Expr::Array(array) = expr else {
522 return None;
523 };
524
525 let mut exports = Vec::new();
526 for elem in &array.elems {
527 let elem = elem.as_ref()?;
528 if elem.spread.is_some() {
529 return None;
530 }
531 exports.push(expr_to_str(&elem.expr)?.into_owned());
532 }
533
534 Some(MagicCommentValue::Array(exports))
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538enum MagicCommentPrefix {
539 Rspack,
540 Webpack,
541}
542
543impl fmt::Display for MagicCommentPrefix {
544 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545 f.write_str(match self {
546 Self::Rspack => "rspack",
547 Self::Webpack => "webpack",
548 })
549 }
550}
551
552#[derive(Debug)]
553struct MagicCommentItem {
554 prefix: MagicCommentPrefix,
555 value: MagicCommentValue,
556 span: DependencyRange,
557}
558
559fn parse_magic_comment_name(name: &str) -> Option<(RspackComment, MagicCommentPrefix)> {
560 let (name, prefix) = if let Some(name) = name.strip_prefix("rspack") {
561 (name, MagicCommentPrefix::Rspack)
562 } else if let Some(name) = name.strip_prefix("webpack") {
563 (name, MagicCommentPrefix::Webpack)
564 } else {
565 return None;
566 };
567
568 Some((RspackComment::try_from(name).ok()?, prefix))
569}
570
571fn analyze_comments(
572 allocator: &Allocator,
573 source: &str,
574 comments: &[Comment],
575 error_span: Span,
576 warning_diagnostics: &mut Vec<Diagnostic>,
577 result: &mut RspackCommentMap,
578) {
579 let comments = comments
580 .iter()
581 .filter(|comment| matches!(comment.kind, CommentKind::Block))
582 .map(|comment| RawMagicComment {
583 text: &comment.text,
584 span: comment.span.into(),
585 })
586 .collect::<Vec<_>>();
587 analyze_raw_comments(
588 allocator,
589 source,
590 &comments,
591 error_span.into(),
592 warning_diagnostics,
593 result,
594 true,
595 false,
596 );
597}
598
599#[allow(clippy::too_many_arguments)]
600fn analyze_raw_comments(
601 allocator: &Allocator,
602 source: &str,
603 comments: &[RawMagicComment<'_>],
604 error_span: DependencyRange,
605 warning_diagnostics: &mut Vec<Diagnostic>,
606 result: &mut RspackCommentMap,
607 warn_on_same_prefix: bool,
608 warn_on_parse_error: bool,
609) {
610 let mut parsed_comment = FxHashSet::<DependencyRange>::default();
611 for comment in comments.iter().rev() {
612 if !parsed_comment.insert(comment.span) {
613 continue;
614 }
615 let Some((expr, comment_offset)) = parse_magic_comment_object(allocator, comment.text) else {
616 if warn_on_parse_error && find_magic_comment_start(comment.text).is_some() {
617 let mut error: Error = create_traceable_error(
618 "Magic comments parse failed".into(),
619 format!(
620 "Compilation error while processing magic comment(-s): /*{}*/",
621 comment.text
622 ),
623 source.to_owned(),
624 comment.span,
625 );
626 error.severity = Severity::Warning;
627 error.hide_stack = Some(true);
628 warning_diagnostics.push(error.into());
629 }
630 continue;
631 };
632 let Expr::Object(object) = &expr else {
633 continue;
634 };
635 for prop in &object.props {
636 let PropOrSpread::Prop(prop) = prop else {
637 continue;
638 };
639 let Prop::KeyValue(prop) = &**prop else {
640 continue;
641 };
642 let Some(item_name) = prop_name_to_str(&prop.key) else {
643 continue;
644 };
645 let Some((rspack_comment, prefix)) = parse_magic_comment_name(item_name.as_ref()) else {
646 continue;
647 };
648 let value = &prop.value;
649 let item_name = rspack_comment.prefixed_name(prefix);
650 let received = raw_value_with_offset(comment.text, value, comment_offset).unwrap_or_default();
651 let item_span =
652 value_span_to_error_span_with_offset(comment.span, value.span(), comment_offset)
653 .unwrap_or(error_span);
654 let push_parse_warning = |comment_type| {
655 push_magic_comment_parse_warning(
656 source,
657 item_name,
658 comment_type,
659 received,
660 warning_diagnostics,
661 item_span,
662 );
663 };
664
665 let value = match rspack_comment {
666 RspackComment::ChunkName => {
667 if let Some(value) = expr_to_str(value) {
668 MagicCommentValue::String(value.into_owned())
669 } else {
670 push_parse_warning("a string");
671 continue;
672 }
673 }
674 RspackComment::Prefetch => {
675 if let Some(value) = expr_to_order_str_with_offset(comment.text, value, comment_offset) {
676 if value == "true" {
677 MagicCommentValue::Bool(true)
678 } else {
679 MagicCommentValue::Number(value.to_string())
680 }
681 } else {
682 push_parse_warning("true or a number");
683 continue;
684 }
685 }
686 RspackComment::Preload => {
687 if let Some(value) = expr_to_order_str_with_offset(comment.text, value, comment_offset) {
688 if value == "true" {
689 MagicCommentValue::Bool(true)
690 } else {
691 MagicCommentValue::Number(value.to_string())
692 }
693 } else {
694 push_parse_warning("true or a number");
695 continue;
696 }
697 }
698 RspackComment::Ignore => {
699 if let Some(value) = expr_to_bool(value) {
700 MagicCommentValue::Bool(value)
701 } else {
702 let value =
703 expr_to_magic_comment_value_with_offset(comment.text, value, comment_offset)
704 .unwrap_or(MagicCommentValue::Unknown);
705 push_parse_warning("a boolean");
706 value
707 }
708 }
709 RspackComment::Mode => {
710 if let Some(mode) = ContextMode::from_ast_expr(value)
711 .ok()
712 .flatten()
713 .filter(|mode| {
714 matches!(
715 mode,
716 ContextMode::Lazy | ContextMode::LazyOnce | ContextMode::Eager | ContextMode::Weak
717 )
718 })
719 {
720 MagicCommentValue::String(mode.as_str().to_string())
721 } else {
722 push_parse_warning(r#""lazy", "lazy-once", "eager" or "weak""#);
723 continue;
724 }
725 }
726 RspackComment::FetchPriority => {
727 if let Some(priority) = expr_to_str(value)
728 && matches!(priority.as_ref(), "low" | "high" | "auto")
729 {
730 MagicCommentValue::String(priority.into_owned())
731 } else {
732 push_parse_warning(r#""low", "high" or "auto""#);
733 continue;
734 }
735 }
736 RspackComment::IncludeRegexp => {
737 if let Some((regexp, flags)) = expr_to_regexp(value)
738 && RspackRegex::with_flags(regexp, flags).is_ok()
739 {
740 MagicCommentValue::RegExp {
741 source: regexp.to_string(),
742 flags: flags.to_string(),
743 }
744 } else {
745 push_parse_warning(r#"a regular expression"#);
746 continue;
747 }
748 }
749 RspackComment::ExcludeRegexp => {
750 if let Some((regexp, flags)) = expr_to_regexp(value)
751 && RspackRegex::with_flags(regexp, flags).is_ok()
752 {
753 MagicCommentValue::RegExp {
754 source: regexp.to_string(),
755 flags: flags.to_string(),
756 }
757 } else {
758 push_parse_warning(r#"a regular expression"#);
759 continue;
760 }
761 }
762 RspackComment::Exports => {
763 if let Some(exports) = expr_to_exports(value) {
764 exports
765 } else {
766 push_parse_warning(r#"a string or an array of strings"#);
767 continue;
768 }
769 }
770 };
771 let item = MagicCommentItem {
772 prefix,
773 value,
774 span: item_span,
775 };
776 if !warn_on_same_prefix
777 && let Some(existing) = result.0.get(&rspack_comment)
778 && existing.prefix == item.prefix
779 {
780 continue;
781 }
782 result.insert_with_conflict_warning(source, rspack_comment, item, warning_diagnostics);
783 }
784 }
785}
786
787pub fn try_extract_magic_comment_from_comments(
788 source: &str,
789 comments: &[RawMagicComment<'_>],
790 error_span: DependencyRange,
791) -> (RspackCommentMap, Vec<Diagnostic>) {
792 let allocator = Allocator::new();
793 let mut result = RspackCommentMap::new();
794 let mut warning_diagnostics = Vec::new();
795 analyze_raw_comments(
796 &allocator,
797 source,
798 comments,
799 error_span,
800 &mut warning_diagnostics,
801 &mut result,
802 false,
803 true,
804 );
805 (result, warning_diagnostics)
806}
807
808#[cfg(test)]
809mod tests_extract_magic_comment_object {
810 use swc_experimental_ecma_ast::DUMMY_SP;
811
812 use super::*;
813
814 fn with_value<R>(raw: &str, name: &str, f: impl FnOnce(&Expr<'_>) -> Option<R>) -> Option<R> {
815 let allocator = Allocator::new();
816 let (expr, _) = parse_magic_comment_object(&allocator, raw)?;
817 let Expr::Object(object) = &expr else {
818 return None;
819 };
820 for prop in &object.props {
821 let PropOrSpread::Prop(prop) = prop else {
822 continue;
823 };
824 let Prop::KeyValue(prop) = &**prop else {
825 continue;
826 };
827 if prop_name_to_str(&prop.key).as_deref() == Some(name) {
828 return f(&prop.value);
829 }
830 }
831 None
832 }
833
834 fn extract(raw: &str) -> (RspackCommentMap, Vec<Diagnostic>) {
835 let mut result = RspackCommentMap::new();
836 let mut warning_diagnostics = Vec::new();
837 let allocator = Allocator::new();
838 analyze_comments(
839 &allocator,
840 "",
841 &[Comment {
842 kind: CommentKind::Block,
843 span: DUMMY_SP,
844 text: swc_experimental_allocator::atom::Atom::new_in(raw, &allocator),
845 }],
846 DUMMY_SP,
847 &mut warning_diagnostics,
848 &mut result,
849 );
850 (result, warning_diagnostics)
851 }
852
853 fn try_match_string(raw: &str) -> Option<(String, String)> {
854 let name = "webpackInclude";
855 with_value(raw, name, |value| {
856 Some((name.to_string(), expr_to_str(value)?.into_owned()))
857 })
858 }
859
860 fn try_match_order(raw: &str) -> Option<(String, String)> {
861 let name = "webpackInclude";
862 with_value(raw, name, |value| {
863 Some((name.to_string(), expr_to_order_str(raw, value)?.to_string()))
864 })
865 }
866
867 fn try_match_regex(raw: &str) -> Option<(String, String, String)> {
868 let name = "webpackInclude";
869 with_value(raw, name, |value| {
870 let (regexp, flags) = expr_to_regexp(value)?;
871 Some((name.to_string(), regexp.to_string(), flags.to_string()))
872 })
873 }
874
875 fn test_extract_string() {
876 assert_eq!(
877 try_match_string("webpackInclude: \"abc\""),
878 Some(("webpackInclude".to_string(), "abc".to_string()))
879 );
880 assert_eq!(
881 try_match_string("webpackInclude: 'abc'"),
882 Some(("webpackInclude".to_string(), "abc".to_string()))
883 );
884 assert_eq!(
885 try_match_string("webpackInclude: `abc`"),
886 Some(("webpackInclude".to_string(), "abc".to_string()))
887 );
888 assert_eq!(
889 try_match_string("webpackInclude: \"abc_-|123\""),
890 Some(("webpackInclude".to_string(), "abc_-|123".to_string()))
891 );
892 }
893
894 fn test_extract_number() {
895 assert_eq!(
896 try_match_order("webpackInclude: 123"),
897 Some(("webpackInclude".to_string(), "123".to_string()))
898 );
899 assert_eq!(
900 try_match_order("webpackInclude: 123.456"),
901 Some(("webpackInclude".to_string(), "123.456".to_string()))
902 );
903 assert_eq!(
904 try_match_order("webpackInclude: -123.456"),
905 Some(("webpackInclude".to_string(), "-123.456".to_string()))
906 );
907 }
908
909 fn test_extract_boolean() {
910 assert_eq!(
911 try_match_order("webpackInclude: true"),
912 Some(("webpackInclude".to_string(), "true".to_string()))
913 );
914 assert_eq!(
915 try_match_order("webpackInclude: false"),
916 Some(("webpackInclude".to_string(), "false".to_string()))
917 );
918 }
919
920 fn test_extract_array() {
921 assert_eq!(
922 with_value(
923 "webpackExports: [\"a\", `b`, 'c']",
924 "webpackExports",
925 expr_to_exports
926 ),
927 Some(MagicCommentValue::Array(vec![
928 "a".to_string(),
929 "b".to_string(),
930 "c".to_string()
931 ]))
932 );
933 }
934
935 fn test_extract_regexp() {
936 assert_eq!(
937 try_match_regex("webpackInclude: /abc/"),
938 Some((
939 "webpackInclude".to_string(),
940 "abc".to_string(),
941 String::new()
942 ))
943 );
944 assert_eq!(
945 try_match_regex("webpackInclude: /abc/ig"),
946 Some((
947 "webpackInclude".to_string(),
948 "abc".to_string(),
949 "ig".to_string()
950 ))
951 );
952 assert_eq!(
953 try_match_regex("webpackInclude: /[^,+]/ig"),
954 Some((
955 "webpackInclude".to_string(),
956 "[^,+]".to_string(),
957 "ig".to_string()
958 ))
959 );
960 assert_eq!(
961 try_match_regex("webpackInclude: /a\\/b\\/c/ig"),
962 Some((
963 "webpackInclude".to_string(),
964 "a\\/b\\/c".to_string(),
965 "ig".to_string()
966 ))
967 );
968 assert_eq!(
969 try_match_regex("webpackInclude: /components[\\/][^\\/]+\\.vue$/"),
970 Some((
971 "webpackInclude".to_string(),
972 "components[\\/][^\\/]+\\.vue$".to_string(),
973 String::new()
974 ))
975 );
976 assert_eq!(
977 try_match_regex(r#"webpackInclude: /components[/\\][^/\\]+\.vue$/"#),
978 Some((
979 "webpackInclude".to_string(),
980 r#"components[/\\][^/\\]+\.vue$"#.to_string(),
981 String::new()
982 ))
983 );
984 assert_eq!(
985 try_match_regex("webpackInclude: /^.{2,}$/"),
986 Some((
987 "webpackInclude".to_string(),
988 "^.{2,}$".to_string(),
989 String::new()
990 ))
991 );
992 assert_eq!(
993 try_match_regex("webpackInclude: /^.{2,}$/, webpackExclude: /^.{3,}$/"),
994 Some((
995 "webpackInclude".to_string(),
996 "^.{2,}$".to_string(),
997 String::new()
998 ))
999 );
1000 assert_eq!(
1002 try_match_regex(
1003 "webpackInclude: /(?!.*node_modules)(?:\\/src\\/(?!\\.)(?=.)[^/]*?\\.stories\\.tsx)$/"
1004 ),
1005 Some((
1006 "webpackInclude".to_string(),
1007 "(?!.*node_modules)(?:\\/src\\/(?!\\.)(?=.)[^/]*?\\.stories\\.tsx)$".to_string(),
1008 String::new()
1009 ))
1010 );
1011 }
1012
1013 #[test]
1014 fn test_rspack_magic_comment_name_aliases() {
1015 assert_eq!(
1016 parse_magic_comment_name("rspackChunkName").map(|(comment, _)| comment),
1017 Some(RspackComment::ChunkName)
1018 );
1019 assert_eq!(
1020 parse_magic_comment_name("rspackPrefetch").map(|(comment, _)| comment),
1021 Some(RspackComment::Prefetch)
1022 );
1023 assert_eq!(
1024 parse_magic_comment_name("rspackPreload").map(|(comment, _)| comment),
1025 Some(RspackComment::Preload)
1026 );
1027 assert_eq!(
1028 parse_magic_comment_name("rspackIgnore").map(|(comment, _)| comment),
1029 Some(RspackComment::Ignore)
1030 );
1031 assert_eq!(
1032 parse_magic_comment_name("rspackMode").map(|(comment, _)| comment),
1033 Some(RspackComment::Mode)
1034 );
1035 assert_eq!(
1036 parse_magic_comment_name("rspackFetchPriority").map(|(comment, _)| comment),
1037 Some(RspackComment::FetchPriority)
1038 );
1039 assert_eq!(
1040 parse_magic_comment_name("rspackInclude").map(|(comment, _)| comment),
1041 Some(RspackComment::IncludeRegexp)
1042 );
1043 assert_eq!(
1044 parse_magic_comment_name("rspackExclude").map(|(comment, _)| comment),
1045 Some(RspackComment::ExcludeRegexp)
1046 );
1047 assert_eq!(
1048 parse_magic_comment_name("rspackExports").map(|(comment, _)| comment),
1049 Some(RspackComment::Exports)
1050 );
1051 }
1052
1053 #[test]
1054 fn test_extract_rspack_prefixed_magic_comments() {
1055 let (comments, warnings) = extract(
1056 r#"
1057 rspackChunkName: "chunk",
1058 rspackPrefetch: 1,
1059 rspackPreload: true,
1060 rspackIgnore: true,
1061 rspackMode: "eager",
1062 rspackFetchPriority: "high",
1063 rspackInclude: /\.js$/,
1064 rspackExclude: /\.test\.js$/,
1065 rspackExports: ["a", "b"]
1066 "#,
1067 );
1068
1069 assert!(warnings.is_empty());
1070 assert_eq!(comments.get_chunk_name(), Some(&"chunk".to_string()));
1071 assert_eq!(comments.get_prefetch().as_deref(), Some("1"));
1072 assert_eq!(comments.get_preload().as_deref(), Some("true"));
1073 assert_eq!(comments.get_ignore(), Some(true));
1074 assert_eq!(comments.get_mode(), Some(&"eager".to_string()));
1075 assert_eq!(comments.get_fetch_priority(), Some(&"high".to_string()));
1076 assert!(comments.get_include().is_some());
1077 assert!(comments.get_exclude().is_some());
1078 assert_eq!(comments.get_exports(), Some(vec!["a".into(), "b".into()]));
1079 }
1080
1081 #[test]
1082 fn test_extract_preserved_magic_comments() {
1083 let (comments, warnings) = extract("@preserve webpackIgnore: true");
1084
1085 assert!(warnings.is_empty());
1086 assert_eq!(comments.get_ignore(), Some(true));
1087
1088 let (comments, warnings) = extract("@license MIT webpackIgnore: true");
1089
1090 assert!(warnings.is_empty());
1091 assert_eq!(comments.get_ignore(), Some(true));
1092 }
1093
1094 #[test]
1095 fn test_rspack_prefixed_magic_comments_override_webpack_prefixed_comments() {
1096 let (comments, warnings) = extract(
1097 r#"
1098 webpackChunkName: "webpack-chunk",
1099 rspackChunkName: "rspack-chunk",
1100 rspackMode: "eager",
1101 webpackMode: "lazy",
1102 webpackPrefetch: 1,
1103 rspackPrefetch: true,
1104 rspackPreload: 2,
1105 webpackPreload: true,
1106 webpackIgnore: false,
1107 rspackIgnore: true,
1108 rspackFetchPriority: "high",
1109 webpackFetchPriority: "low",
1110 webpackInclude: /\.jsx$/,
1111 rspackInclude: /\.js$/,
1112 rspackExclude: /\.test\.js$/,
1113 webpackExclude: /\.spec\.js$/,
1114 webpackExports: ["webpack"],
1115 rspackExports: ["rspack"]
1116 "#,
1117 );
1118
1119 assert_eq!(comments.get_chunk_name(), Some(&"rspack-chunk".to_string()));
1120 assert_eq!(comments.get_mode(), Some(&"eager".to_string()));
1121 assert_eq!(comments.get_prefetch().as_deref(), Some("true"));
1122 assert_eq!(comments.get_preload().as_deref(), Some("2"));
1123 assert_eq!(comments.get_ignore(), Some(true));
1124 assert_eq!(comments.get_fetch_priority(), Some(&"high".to_string()));
1125 assert_eq!(comments.get_include().unwrap().source(), r#"\.js$"#);
1126 assert_eq!(comments.get_exclude().unwrap().source(), r#"\.test\.js$"#);
1127 assert_eq!(comments.get_exports(), Some(vec!["rspack".into()]));
1128 assert_eq!(warnings.len(), 9);
1129 for webpack_name in [
1130 "webpackChunkName",
1131 "webpackMode",
1132 "webpackPrefetch",
1133 "webpackPreload",
1134 "webpackIgnore",
1135 "webpackFetchPriority",
1136 "webpackInclude",
1137 "webpackExclude",
1138 "webpackExports",
1139 ] {
1140 assert!(
1141 warnings.iter().any(|warning| warning
1142 .message
1143 .contains(&format!("`{webpack_name}` is ignored"))),
1144 "missing conflict warning for {webpack_name}"
1145 );
1146 }
1147 }
1148
1149 #[test]
1150 fn test_rspack_prefixed_magic_comments_override_webpack_prefixed_comments_in_any_order() {
1151 let (comments, warnings) = extract(
1152 r#"
1153 rspackChunkName: "rspack-chunk",
1154 webpackChunkName: "webpack-chunk"
1155 "#,
1156 );
1157
1158 assert_eq!(comments.get_chunk_name(), Some(&"rspack-chunk".to_string()));
1159 assert_eq!(warnings.len(), 1);
1160 assert!(
1161 warnings[0]
1162 .message
1163 .contains("`webpackChunkName` is ignored")
1164 );
1165 }
1166
1167 #[test]
1168 fn test_repeated_magic_comments_with_same_prefix_keep_first_value() {
1169 let (comments, warnings) = extract(
1170 r#"
1171 webpackChunkName: "first-webpack-chunk",
1172 webpackChunkName: "second-webpack-chunk",
1173 rspackMode: "eager",
1174 rspackMode: "lazy"
1175 "#,
1176 );
1177
1178 assert_eq!(
1179 comments.get_chunk_name(),
1180 Some(&"first-webpack-chunk".to_string())
1181 );
1182 assert_eq!(comments.get_mode(), Some(&"eager".to_string()));
1183 assert_eq!(warnings.len(), 2);
1184 assert!(
1185 warnings[0]
1186 .message
1187 .contains("`webpackChunkName` is ignored")
1188 );
1189 assert!(warnings[1].message.contains("`rspackMode` is ignored"));
1190 }
1191
1192 #[test]
1193 fn test_extract_magic_comment_object() {
1194 test_extract_string();
1195 test_extract_number();
1196 test_extract_boolean();
1197 test_extract_array();
1198 test_extract_regexp();
1199 }
1200}