Skip to main content

rspack_plugin_javascript/plugin/
side_effects_flag_plugin.rs

1use std::{borrow::Cow, fmt::Debug};
2
3use rayon::prelude::*;
4use rspack_collections::{IdentifierMap, IdentifierSet};
5use rspack_core::{
6  AsyncModulesArtifact, BoxModule, Compilation, CompilationFinishModules,
7  CompilationOptimizeDependencies, ConnectionState, DependencyExtraMeta, DependencyId,
8  ExportsInfoArtifact, FactoryMeta, GetTargetResult, Logger, ModuleFactoryCreateData, ModuleGraph,
9  ModuleGraphConnection, ModuleIdentifier, NormalModuleCreateData, NormalModuleFactoryModule,
10  OptimizationBailoutItem, Plugin, ResolvedExportInfoTarget, SideEffectsDoOptimize,
11  SideEffectsDoOptimizeMoveTarget, SideEffectsOptimizeArtifact, SideEffectsState,
12  SideEffectsStateArtifact,
13  build_module_graph::BuildModuleGraphArtifact,
14  can_move_target, get_target,
15  incremental::{self, IncrementalPasses, Mutation},
16};
17use rspack_error::{Diagnostic, Result};
18use rspack_hook::{plugin, plugin_hook};
19use rspack_paths::{AssertUtf8, Utf8Path};
20use sugar_path::SugarPath;
21use swc_experimental_ecma_ast::{ClassMember, Key, PropName};
22
23use crate::{
24  FLAG_DEPENDENCY_EXPORTS_STAGE, deferred_pure_check_is_impure,
25  dependency::{ESMExportImportedSpecifierDependency, ESMImportSpecifierDependency},
26};
27
28pub static SIDE_EFFECTS_FLAG_PLUGIN_STAGE: i32 = FLAG_DEPENDENCY_EXPORTS_STAGE + 10;
29const CALL_WITH_SIDE_EFFECTS_BAILOUT: &str = "Call with side effects";
30
31#[derive(Clone, Debug)]
32enum SideEffects {
33  Bool(bool),
34  String(String),
35  Array(Vec<String>),
36}
37
38impl SideEffects {
39  pub fn from_description(description: &serde_json::Value) -> Option<Self> {
40    description.get("sideEffects").and_then(|value| {
41      if let Some(b) = value.as_bool() {
42        Some(SideEffects::Bool(b))
43      } else if let Some(s) = value.as_str() {
44        Some(SideEffects::String(s.to_owned()))
45      } else if let Some(vec) = value.as_array() {
46        let mut side_effects = vec![];
47        for value in vec {
48          if let Some(str) = value.as_str() {
49            side_effects.push(str.to_string());
50          } else {
51            return None;
52          }
53        }
54        Some(SideEffects::Array(side_effects))
55      } else {
56        None
57      }
58    })
59  }
60}
61
62fn get_side_effects_from_package_json(side_effects: SideEffects, relative_path: &Utf8Path) -> bool {
63  match side_effects {
64    SideEffects::Bool(s) => s,
65    SideEffects::String(s) => glob_match_with_normalized_pattern(&s, relative_path.as_str()),
66    SideEffects::Array(patterns) => patterns
67      .iter()
68      .any(|pattern| glob_match_with_normalized_pattern(pattern, relative_path.as_str())),
69  }
70}
71
72fn glob_match_with_normalized_pattern(pattern: &str, string: &str) -> bool {
73  let trim_start = pattern.trim_start_matches("./");
74  let normalized_glob = if trim_start.contains('/') {
75    trim_start.to_string()
76  } else {
77    String::from("**/") + trim_start
78  };
79  fast_glob::glob_match(&normalized_glob, string.trim_start_matches("./"))
80}
81
82pub trait ClassExt<'a> {
83  fn class_key(&'a self) -> Option<&'a PropName<'a>>;
84  fn is_static(&self) -> bool;
85}
86
87impl<'a> ClassExt<'a> for ClassMember<'a> {
88  fn class_key(&'a self) -> Option<&'a PropName<'a>> {
89    match self {
90      ClassMember::Constructor(c) => Some(&c.key),
91      ClassMember::Method(m) => Some(&m.key),
92      ClassMember::PrivateMethod(_) => None,
93      ClassMember::ClassProp(c) => Some(&c.key),
94      ClassMember::PrivateProp(_) => None,
95      ClassMember::Empty(_) => None,
96      ClassMember::StaticBlock(_) => None,
97      ClassMember::AutoAccessor(a) => match &a.key {
98        Key::Private(_) => None,
99        Key::Public(public) => Some(public),
100      },
101    }
102  }
103
104  fn is_static(&self) -> bool {
105    match self {
106      ClassMember::Constructor(_) => false,
107      ClassMember::Method(m) => m.is_static,
108      ClassMember::PrivateMethod(m) => m.is_static,
109      ClassMember::ClassProp(p) => p.is_static,
110      ClassMember::PrivateProp(p) => p.is_static,
111      ClassMember::Empty(_) => false,
112      ClassMember::StaticBlock(_) => true,
113      ClassMember::AutoAccessor(a) => a.is_static,
114    }
115  }
116}
117
118#[plugin]
119#[derive(Debug, Default)]
120pub struct SideEffectsFlagPlugin {
121  analyze_side_effects_free: bool,
122}
123
124impl SideEffectsFlagPlugin {
125  pub fn new(analyze_side_effects_free: bool) -> Self {
126    Self::new_inner(analyze_side_effects_free)
127  }
128}
129
130#[plugin_hook(NormalModuleFactoryModule for SideEffectsFlagPlugin,tracing=false)]
131async fn nmf_module(
132  &self,
133  _data: &mut ModuleFactoryCreateData,
134  create_data: &NormalModuleCreateData,
135  module: &mut BoxModule,
136) -> Result<()> {
137  if let Some(has_side_effects) = create_data.side_effects {
138    module.set_factory_meta(FactoryMeta {
139      side_effect_free: Some(!has_side_effects),
140    });
141    return Ok(());
142  }
143
144  let resource_data = create_data.resource_resolve_data.as_ref();
145  let Some(resource_path) = resource_data.path() else {
146    return Ok(());
147  };
148  let Some(description) = resource_data.description() else {
149    return Ok(());
150  };
151  let package_path = description.path();
152  let Some(side_effects) = SideEffects::from_description(description.json()) else {
153    return Ok(());
154  };
155  let relative_path = resource_path
156    .as_std_path()
157    .relative(package_path)
158    .assert_utf8();
159  let has_side_effects = get_side_effects_from_package_json(side_effects, relative_path.as_path());
160
161  module.set_factory_meta(FactoryMeta {
162    side_effect_free: Some(!has_side_effects),
163  });
164  Ok(())
165}
166
167// run after exports info are set
168#[plugin_hook(CompilationFinishModules for SideEffectsFlagPlugin, stage = SIDE_EFFECTS_FLAG_PLUGIN_STAGE)]
169async fn finish_modules(
170  &self,
171  compilation: &Compilation,
172  _modules: &mut AsyncModulesArtifact,
173  exports_info_artifact: &mut ExportsInfoArtifact,
174  side_effects_state_artifact: &mut SideEffectsStateArtifact,
175) -> Result<()> {
176  let modules: IdentifierSet = compilation
177    .get_module_graph()
178    .modules()
179    .map(|(id, _)| *id)
180    .collect();
181
182  let module_graph = compilation.get_module_graph();
183  let mut deferred_side_effect_states = Vec::new();
184  for module_identifier in &modules {
185    let Some(module) = module_graph.module_by_identifier(module_identifier) else {
186      continue;
187    };
188
189    let build_info = module.build_info();
190    if !self.analyze_side_effects_free || build_info.deferred_pure_checks.is_empty() {
191      continue;
192    }
193
194    let baseline_side_effect_free = module.build_meta().side_effect_free();
195
196    let has_impure_deferred_check =
197      // find the first deferred pure check that resolves to an impure target
198      build_info
199        .deferred_pure_checks
200        .iter()
201        .any(|deferred_check| {
202          deferred_pure_check_is_impure(
203            module_graph,
204            exports_info_artifact,
205            &deferred_check.dep_id,
206            &deferred_check.atom,
207          )
208        });
209
210    deferred_side_effect_states.push((
211      *module_identifier,
212      baseline_side_effect_free && !has_impure_deferred_check,
213      has_impure_deferred_check,
214    ));
215  }
216
217  side_effects_state_artifact.clear();
218  for (module_id, side_effect_free, has_impure_deferred_check) in deferred_side_effect_states {
219    side_effects_state_artifact.insert(
220      module_id,
221      SideEffectsState {
222        side_effect_free,
223        optimization_bailouts_to_add: if has_impure_deferred_check {
224          vec![OptimizationBailoutItem::Message(String::from(
225            CALL_WITH_SIDE_EFFECTS_BAILOUT,
226          ))]
227        } else {
228          Vec::new()
229        },
230        optimization_bailouts_to_remove: vec![OptimizationBailoutItem::Message(String::from(
231          CALL_WITH_SIDE_EFFECTS_BAILOUT,
232        ))],
233      },
234    );
235  }
236
237  Ok(())
238}
239
240#[plugin_hook(CompilationOptimizeDependencies for SideEffectsFlagPlugin,tracing=false)]
241async fn optimize_dependencies(
242  &self,
243  compilation: &Compilation,
244  side_effects_optimize_artifact: &mut SideEffectsOptimizeArtifact,
245  build_module_graph_artifact: &mut BuildModuleGraphArtifact,
246  exports_info_artifact: &mut ExportsInfoArtifact,
247  _diagnostics: &mut Vec<Diagnostic>,
248) -> Result<Option<bool>> {
249  let logger = compilation.get_logger("rspack.SideEffectsFlagPlugin");
250  let start = logger.time("update connections");
251
252  let side_effects_state_map: IdentifierMap<ConnectionState> = {
253    let module_graph = &build_module_graph_artifact.module_graph;
254    let side_effects_state_artifact = &build_module_graph_artifact.side_effects_state_artifact;
255
256    module_graph
257      .modules_par()
258      .map(|(module_identifier, module)| {
259        (
260          *module_identifier,
261          module.get_side_effects_connection_state(
262            module_graph,
263            &compilation.module_graph_cache_artifact,
264            side_effects_state_artifact,
265            &mut Default::default(),
266            &mut Default::default(),
267          ),
268        )
269      })
270      .collect()
271  };
272  let module_graph = build_module_graph_artifact.get_module_graph();
273
274  if self.analyze_side_effects_free {
275    // `finish_modules` may change the side-effect state of modules that were not part of the
276    // incremental mutation frontier. Reuse of the previous optimize artifact can therefore keep
277    // stale connection rewrites alive, so pure-function analysis falls back to a full refresh here.
278    side_effects_optimize_artifact.clear();
279  }
280
281  let inner_start = logger.time("prepare connections");
282  let modules: Vec<_> = if !self.analyze_side_effects_free
283    && let Some(mutations) = compilation
284      .incremental
285      .mutations_read(IncrementalPasses::OPTIMIZE_DEPENDENCIES)
286    && !side_effects_optimize_artifact.is_empty()
287  {
288    side_effects_optimize_artifact.retain(|dependency_id, do_optimize| {
289      let dep_exist = module_graph
290        .connection_by_dependency_id(dependency_id)
291        .is_some();
292      let target_module_exist = module_graph
293        .module_by_identifier(&do_optimize.target_module)
294        .is_some();
295      dep_exist && target_module_exist
296    });
297
298    fn affected_incoming_modules(
299      module: &ModuleIdentifier,
300      module_graph: &ModuleGraph,
301      modules: &mut IdentifierSet,
302    ) {
303      for connection in module_graph.get_incoming_connections(module) {
304        let Some(original_module) = connection.original_module_identifier else {
305          continue;
306        };
307        if modules.contains(&original_module) {
308          continue;
309        }
310        let dep = module_graph.dependency_by_id(&connection.dependency_id);
311        if dep.is::<ESMExportImportedSpecifierDependency>() && modules.insert(original_module) {
312          affected_incoming_modules(&original_module, module_graph, modules);
313        }
314      }
315    }
316
317    let modules: IdentifierSet = mutations.iter().fold(
318      IdentifierSet::default(),
319      |mut modules, mutation| match mutation {
320        Mutation::ModuleAdd { module } | Mutation::ModuleUpdate { module } => {
321          if modules.insert(*module) {
322            affected_incoming_modules(module, module_graph, &mut modules);
323          }
324          modules.extend(
325            module_graph
326              .get_outgoing_connections(module)
327              .map(|connection| *connection.module_identifier()),
328          );
329          modules
330        }
331        _ => modules,
332      },
333    );
334
335    tracing::debug!(target: incremental::TRACING_TARGET, passes = %IncrementalPasses::OPTIMIZE_DEPENDENCIES, %mutations, ?modules);
336    let logger = compilation.get_logger("rspack.incremental.optimizeDependencies");
337    logger.log(format!(
338      "{} modules are affected, {} in total",
339      modules.len(),
340      module_graph.modules_len()
341    ));
342
343    let mut modules = modules.into_iter().collect::<Vec<_>>();
344    modules.sort_unstable();
345    modules
346  } else {
347    let mut modules = module_graph.modules_keys().copied().collect::<Vec<_>>();
348    modules.sort_unstable();
349    modules
350  };
351  logger.time_end(inner_start);
352
353  let inner_start = logger.time("find optimizable connections");
354  let mut optimized_connections = modules
355    .par_iter()
356    .filter(|module| side_effects_state_map[module] == ConnectionState::Active(false))
357    .flat_map(|module| {
358      module_graph
359        .get_incoming_connections(module)
360        .collect::<Vec<_>>()
361    })
362    .map(|connection| {
363      (
364        connection.dependency_id,
365        can_optimize_connection(
366          connection,
367          &side_effects_state_map,
368          module_graph,
369          exports_info_artifact,
370        ),
371      )
372    })
373    .collect::<Vec<_>>();
374  optimized_connections.sort_unstable_by_key(|(dep_id, _)| *dep_id);
375  for (dep_id, can_optimize) in optimized_connections {
376    if let Some(do_optimize) = can_optimize {
377      side_effects_optimize_artifact.insert(dep_id, do_optimize);
378    } else {
379      side_effects_optimize_artifact.remove(&dep_id);
380    }
381  }
382  logger.time_end(inner_start);
383
384  let mut do_optimizes = side_effects_optimize_artifact
385    .iter()
386    .map(|(dependency, do_optimize)| (*dependency, do_optimize.clone()))
387    .collect::<Vec<_>>();
388  do_optimizes.sort_unstable_by_key(|(dependency, _)| *dependency);
389
390  let inner_start = logger.time("do optimize connections");
391  let mut do_optimized_count = 0;
392  while !do_optimizes.is_empty() {
393    do_optimized_count += do_optimizes.len();
394
395    let module_graph = build_module_graph_artifact.get_module_graph_mut();
396
397    let new_connections: Vec<_> = do_optimizes
398      .into_iter()
399      .map(|(dependency, do_optimize)| {
400        do_optimize_connection(dependency, do_optimize, module_graph, exports_info_artifact)
401      })
402      .collect();
403
404    let module_graph = build_module_graph_artifact.get_module_graph();
405    do_optimizes = new_connections
406      .into_par_iter()
407      .filter(|(_, module)| side_effects_state_map[module] == ConnectionState::Active(false))
408      .filter_map(|(connection, _)| module_graph.connection_by_dependency_id(&connection))
409      .filter_map(|connection| {
410        can_optimize_connection(
411          connection,
412          &side_effects_state_map,
413          module_graph,
414          exports_info_artifact,
415        )
416        .map(|i| (connection.dependency_id, i))
417      })
418      .collect();
419    do_optimizes.sort_unstable_by_key(|(dependency, _)| *dependency);
420  }
421  logger.time_end(inner_start);
422
423  build_module_graph_artifact
424    .side_effects_state_artifact
425    .set_module_evaluation_side_effects_states(side_effects_state_map);
426
427  logger.time_end(start);
428  logger.log(format!("optimized {do_optimized_count} connections"));
429  Ok(None)
430}
431
432#[tracing::instrument(skip_all)]
433fn do_optimize_connection(
434  dependency: DependencyId,
435  do_optimize: SideEffectsDoOptimize,
436  module_graph: &mut ModuleGraph,
437  exports_info_artifact: &mut ExportsInfoArtifact,
438) -> (DependencyId, ModuleIdentifier) {
439  let SideEffectsDoOptimize {
440    ids,
441    target_module,
442    need_move_target,
443  } = do_optimize;
444  module_graph.do_update_module(&dependency, &target_module);
445  module_graph.set_dependency_extra_meta(
446    dependency,
447    DependencyExtraMeta {
448      ids,
449      explanation: Some("(skipped side-effect-free modules)"),
450    },
451  );
452  if let Some(SideEffectsDoOptimizeMoveTarget {
453    export_info,
454    target_export,
455  }) = need_move_target
456  {
457    export_info
458      .as_data_mut(exports_info_artifact)
459      .do_move_target(dependency, target_export);
460  }
461  (dependency, target_module)
462}
463
464#[tracing::instrument("can_optimize_connection", level = "trace", skip_all)]
465fn can_optimize_connection(
466  connection: &ModuleGraphConnection,
467  side_effects_state_map: &IdentifierMap<ConnectionState>,
468  module_graph: &ModuleGraph,
469  exports_info_artifact: &ExportsInfoArtifact,
470) -> Option<SideEffectsDoOptimize> {
471  let original_module = connection.original_module_identifier?;
472  let dependency_id = connection.dependency_id;
473  let dep = module_graph.dependency_by_id(&dependency_id);
474
475  if let Some(dep) = dep.downcast_ref::<ESMExportImportedSpecifierDependency>()
476    && let Some(name) = &dep.name
477  {
478    let exports_info = exports_info_artifact.get_exports_info_data(&original_module);
479    let export_info = exports_info.get_export_info_without_mut_module_graph(name);
480
481    let resolve_filter = |target: &ResolvedExportInfoTarget| {
482      side_effects_state_map[&target.module] == ConnectionState::Active(false)
483    };
484    let target = can_move_target(
485      &export_info,
486      module_graph,
487      exports_info_artifact,
488      &resolve_filter,
489    )?;
490    if !module_graph.can_update_module(&dependency_id, &target.module) {
491      return None;
492    }
493
494    let ids = dep.get_ids(module_graph);
495    let processed_ids = target.export.as_ref().map_or_else(
496      || ids.get(1..).unwrap_or_default().to_vec(),
497      |item| {
498        let mut ret = item.clone();
499        ret.extend_from_slice(ids.get(1..).unwrap_or_default());
500        ret
501      },
502    );
503    let need_move_target = match export_info {
504      Cow::Borrowed(export_info) => Some(SideEffectsDoOptimizeMoveTarget {
505        export_info: export_info.id(),
506        target_export: target.export,
507      }),
508      Cow::Owned { .. } => None,
509    };
510
511    return Some(SideEffectsDoOptimize {
512      ids: processed_ids,
513      target_module: target.module,
514      need_move_target,
515    });
516  }
517
518  if let Some(dep) = dep.downcast_ref::<ESMImportSpecifierDependency>()
519    && !dep.namespace_object_as_context
520    && let ids = dep.get_ids(module_graph)
521    && !ids.is_empty()
522  {
523    let exports_info = exports_info_artifact.get_exports_info_data(connection.module_identifier());
524    let export_info = exports_info.get_export_info_without_mut_module_graph(&ids[0]);
525
526    let resolve_filter = |target: &ResolvedExportInfoTarget| {
527      side_effects_state_map[&target.module] == ConnectionState::Active(false)
528    };
529    let Some(GetTargetResult::Target(target)) = get_target(
530      &export_info,
531      module_graph,
532      exports_info_artifact,
533      &resolve_filter,
534      &mut Default::default(),
535    ) else {
536      return None;
537    };
538
539    if !module_graph.can_update_module(&dependency_id, &target.module) {
540      return None;
541    }
542
543    let processed_ids = target.export.map_or_else(
544      || ids[1..].to_vec(),
545      |mut item| {
546        item.extend_from_slice(&ids[1..]);
547        item
548      },
549    );
550
551    return Some(SideEffectsDoOptimize {
552      ids: processed_ids,
553      target_module: target.module,
554      need_move_target: None,
555    });
556  }
557
558  None
559}
560
561impl Plugin for SideEffectsFlagPlugin {
562  fn name(&self) -> &'static str {
563    "SideEffectsFlagPlugin"
564  }
565
566  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
567    ctx
568      .normal_module_factory_hooks
569      .module
570      .tap(nmf_module::new(self));
571
572    ctx
573      .compilation_hooks
574      .finish_modules
575      .tap(finish_modules::new(self));
576
577    ctx
578      .compilation_hooks
579      .optimize_dependencies
580      .tap(optimize_dependencies::new(self));
581    Ok(())
582  }
583}
584
585#[cfg(test)]
586mod test_side_effects {
587  use super::*;
588
589  fn get_side_effects_from_package_json_helper(
590    side_effects_config: Vec<&str>,
591    relative_path: &str,
592  ) -> bool {
593    assert!(!side_effects_config.is_empty());
594    let relative_path = Utf8Path::new(relative_path);
595    let side_effects = if side_effects_config.len() > 1 {
596      SideEffects::Array(
597        side_effects_config
598          .into_iter()
599          .map(String::from)
600          .collect::<Vec<_>>(),
601      )
602    } else {
603      SideEffects::String((&side_effects_config[0]).to_string())
604    };
605
606    get_side_effects_from_package_json(side_effects, relative_path)
607  }
608
609  #[test]
610  fn cases() {
611    assert!(get_side_effects_from_package_json_helper(
612      vec!["./src/**/*.js"],
613      "./src/x/y/z.js"
614    ));
615    assert!(get_side_effects_from_package_json_helper(
616      vec!["./src/index.js", "./src/selection/index.js"],
617      "./src/selection/index.js"
618    ));
619    assert!(!get_side_effects_from_package_json_helper(
620      vec!["./src/**/*.js"],
621      "./x.js"
622    ));
623    assert!(get_side_effects_from_package_json_helper(
624      vec!["./**/src/x/y/z.js"],
625      "./src/x/y/z.js"
626    ));
627    // 				"./src/x/y/z.js",
628    // 				"./src/**/z.js",
629    assert!(get_side_effects_from_package_json_helper(
630      vec!["./src/**/z.js"],
631      "./src/x/y/z.js"
632    ));
633    // 				"./src/x/y/z.js",
634    // 				"./**/x/**/z.js",
635    assert!(get_side_effects_from_package_json_helper(
636      vec!["./**/x/**/z.js"],
637      "./src/x/y/z.js"
638    ));
639    // 				"./src/x/y/z.js",
640    // 				"./**/src/**",
641    assert!(get_side_effects_from_package_json_helper(
642      vec!["./**/src/**"],
643      "./src/x/y/z.js"
644    ));
645    // 				"./src/x/y/z.js",
646    // 				"./**/src/*",
647    assert!(!get_side_effects_from_package_json_helper(
648      vec!["./src/x/y/z.js"],
649      "./**/src/*"
650    ));
651    // 				"./src/x/y/z.js",
652    // 				"*.js",
653    assert!(get_side_effects_from_package_json_helper(
654      vec!["*.js"],
655      "./src/x/y/z.js"
656    ));
657    // 				"./src/x/y/z.js",
658    // 				"x/**/z.js",
659    assert!(!get_side_effects_from_package_json_helper(
660      vec!["./src/x/y/z.js"],
661      "x/**/z.js"
662    ));
663    // 				"./src/x/y/z.js",
664    // 				"src/**/z.js",
665    assert!(get_side_effects_from_package_json_helper(
666      vec!["./src/**/z.js"],
667      "./src/x/y/z.js"
668    ));
669    // 				"./src/x/y/z.js",
670    // 				"src/**/{x,y,z}.js",
671    assert!(get_side_effects_from_package_json_helper(
672      vec!["src/**/{x,y,z}.js"],
673      "./src/x/y/z.js"
674    ));
675    // 				"./src/x/y/z.js",
676    // 				"src/**/[x-z].js",
677    assert!(get_side_effects_from_package_json_helper(
678      vec!["./src/**/[x-z].js"],
679      "./src/x/y/z.js"
680    ));
681    // 		const array = ["./src/**/*.js", "./dirty.js"];
682    assert!(get_side_effects_from_package_json_helper(
683      vec!["./src/**/*.js", "./dirty.js"],
684      "./src/x/y/z.js"
685    ));
686    assert!(get_side_effects_from_package_json_helper(
687      vec!["./src/**/*.js", "./dirty.js"],
688      "./dirty.js"
689    ));
690    assert!(!get_side_effects_from_package_json_helper(
691      vec!["./src/**/*.js", "./dirty.js"],
692      "./clean.js"
693    ));
694    assert!(get_side_effects_from_package_json_helper(
695      vec!["./src/**/*/z.js"],
696      "./src/x/y/z.js"
697    ));
698  }
699}