1#![deny(clippy::all)]
2#![allow(dead_code)]
3#![recursion_limit = "256"]
4
5use std::{path::PathBuf, sync::Arc};
6
7use preset_env_base::query::targets_to_versions;
8pub use preset_env_base::{query::Targets, version::Version, BrowserData, Versions};
9use rustc_hash::FxHashSet;
10use serde::Deserialize;
11use swc_atoms::{atom, Atom};
12use swc_common::{comments::Comments, pass::Optional, FromVariant, Mark, SyntaxContext, DUMMY_SP};
13use swc_ecma_ast::*;
14use swc_ecma_transforms::{
15 compat::{
16 bugfixes,
17 class_fields_use_set::class_fields_use_set,
18 es2015::{self, generator::generator},
19 es2016, es2017, es2018, es2019, es2020, es2022, es3,
20 regexp::{self, regexp},
21 },
22 Assumptions,
23};
24use swc_ecma_utils::{prepend_stmts, ExprFactory};
25use swc_ecma_visit::{visit_mut_pass, VisitMut, VisitMutWith, VisitWith};
26
27pub use self::transform_data::Feature;
28
29#[macro_use]
30mod util;
31mod corejs2;
32mod corejs3;
33mod node_colon_prefix_strip;
34mod regenerator;
35mod transform_data;
36
37pub trait Caniuse {
38 fn caniuse(&self, feature: Feature) -> bool;
39}
40
41fn transform_internal<C>(
42 unresolved_mark: Mark,
43 comments: Option<C>,
44 assumptions: Assumptions,
45 loose: bool,
46 dynamic_import: bool,
47 debug: bool,
48 caniuse: impl (Fn(Feature) -> bool),
49) -> impl Pass
50where
51 C: Comments + Clone,
52{
53 let pass = noop_pass();
54
55 macro_rules! add {
56 ($prev:expr, $feature:ident, $pass:expr) => {{
57 add!($prev, $feature, $pass, false)
58 }};
59 ($prev:expr, $feature:ident, $pass:expr, $default:expr) => {{
60 let f = transform_data::Feature::$feature;
61
62 let enable = !caniuse(f);
63
64 if debug {
65 println!("{}: {:?}", f.as_str(), enable);
66 }
67 ($prev, Optional::new($pass, enable))
68 }};
69 }
70
71 macro_rules! add_compiler {
72 ($($feature:ident)|*) => {{
73 let mut feature = swc_ecma_compiler::Features::empty();
74 $(
75 {
76 let f = transform_data::Feature::$feature;
77 let enable = !caniuse(f);
78
79 if debug {
80 println!("{}: {:?}", f.as_str(), enable);
81 }
82
83 if enable {
84 feature |= swc_ecma_compiler::Features::from(f);
85 }
86 }
87 )*
88 feature
89 }};
90 (| $($feature:ident)|*) => {{
91 add_compiler!($($feature)|*)
92 }};
93 ($prev:expr, $($feature:ident)|*) => {{
94 let feature = add_compiler!($($feature)|*);
95 (
96 $prev,
97 swc_ecma_compiler::Compiler::new(swc_ecma_compiler::Config {
98 includes: feature,
99 assumptions,
100 ..Default::default()
101 }),
102 )
103 }};
104 ($prev:expr, | $($feature:ident)|*) => {{
105 add_compiler!($prev, $($feature)|*)
106 }};
107 }
108
109 let pass = (
110 pass,
111 Optional::new(
112 class_fields_use_set(assumptions.pure_getters),
113 assumptions.set_public_class_fields,
114 ),
115 );
116
117 let pass = {
118 let enable_dot_all_regex = !caniuse(Feature::DotAllRegex);
119 let enable_named_capturing_groups_regex = !caniuse(Feature::NamedCapturingGroupsRegex);
120 let enable_sticky_regex = !caniuse(Feature::StickyRegex);
121 let enable_unicode_property_regex = !caniuse(Feature::UnicodePropertyRegex);
122 let enable_unicode_regex = !caniuse(Feature::UnicodeRegex);
123 let enable_unicode_sets_regex = !caniuse(Feature::UnicodeSetsRegex);
124
125 let enable = enable_dot_all_regex
126 || enable_named_capturing_groups_regex
127 || enable_sticky_regex
128 || enable_unicode_property_regex
129 || enable_unicode_regex;
130
131 (
132 pass,
133 Optional::new(
134 regexp(regexp::Config {
135 dot_all_regex: enable_dot_all_regex,
136 has_indices: false,
138 lookbehind_assertion: false,
140 named_capturing_groups_regex: enable_named_capturing_groups_regex,
141 sticky_regex: enable_sticky_regex,
142 unicode_property_regex: enable_unicode_property_regex,
143 unicode_regex: enable_unicode_regex,
144 unicode_sets_regex: enable_unicode_sets_regex,
145 }),
146 enable,
147 ),
148 )
149 };
150
151 let pass = add_compiler!(pass, ClassStaticBlock);
158 let pass = add!(
159 pass,
160 ClassProperties,
161 es2022::class_properties(
162 es2022::class_properties::Config {
163 private_as_properties: loose || assumptions.private_fields_as_properties,
164 set_public_fields: loose || assumptions.set_public_class_fields,
165 constant_super: loose || assumptions.constant_super,
166 no_document_all: loose || assumptions.no_document_all,
167 pure_getter: loose || assumptions.pure_getters,
168 },
169 unresolved_mark
170 )
171 );
172
173 #[rustfmt::skip]
174 let pass = add_compiler!(
175 pass,
176 | PrivatePropertyInObject
177 | LogicalAssignmentOperators
178 | ExportNamespaceFrom
179 );
180
181 let pass = add!(
183 pass,
184 NullishCoalescing,
185 es2020::nullish_coalescing(es2020::nullish_coalescing::Config {
186 no_document_all: loose || assumptions.no_document_all
187 })
188 );
189
190 let pass = add!(
191 pass,
192 OptionalChaining,
193 es2020::optional_chaining(
194 es2020::optional_chaining::Config {
195 no_document_all: loose || assumptions.no_document_all,
196 pure_getter: loose || assumptions.pure_getters
197 },
198 unresolved_mark
199 )
200 );
201
202 let pass = add!(pass, OptionalCatchBinding, es2019::optional_catch_binding());
204
205 let pass = add!(
207 pass,
208 ObjectRestSpread,
209 es2018::object_rest_spread(es2018::object_rest_spread::Config {
210 no_symbol: loose || assumptions.object_rest_no_symbols,
211 set_property: loose || assumptions.set_spread_properties,
212 pure_getters: loose || assumptions.pure_getters
213 })
214 );
215
216 let pass = add!(
218 pass,
219 AsyncToGenerator,
220 es2017::async_to_generator(
221 es2017::async_to_generator::Config {
222 ignore_function_length: loose || assumptions.ignore_function_length,
223 },
224 unresolved_mark
225 )
226 );
227
228 let pass = add!(pass, ExponentiationOperator, es2016::exponentiation());
230
231 let pass = add!(pass, BlockScopedFunctions, es2015::block_scoped_functions());
233 let pass = add!(
234 pass,
235 TemplateLiterals,
236 es2015::template_literal(es2015::template_literal::Config {
237 ignore_to_primitive: loose || assumptions.ignore_to_primitive_hint,
238 mutable_template: loose || assumptions.mutable_template_object
239 }),
240 true
241 );
242 let pass = add!(
243 pass,
244 Classes,
245 es2015::classes(es2015::classes::Config {
246 constant_super: loose || assumptions.constant_super,
247 no_class_calls: loose || assumptions.no_class_calls,
248 set_class_methods: loose || assumptions.set_class_methods,
249 super_is_callable_constructor: loose || assumptions.super_is_callable_constructor,
250 })
251 );
252 let pass = add!(pass, NewTarget, es2015::new_target(), true);
253 let pass = add!(
254 pass,
255 Spread,
256 es2015::spread(es2015::spread::Config { loose }, unresolved_mark),
257 true
258 );
259 let pass = add!(pass, ObjectSuper, es2015::object_super());
260 let pass = add!(pass, ShorthandProperties, es2015::shorthand());
261 let pass = add!(pass, FunctionName, es2015::function_name());
262 let pass = add!(
263 pass,
264 ForOf,
265 es2015::for_of(es2015::for_of::Config {
266 loose,
267 assume_array: loose || assumptions.iterable_is_array
268 }),
269 true
270 );
271 let pass = add!(
272 pass,
273 Parameters,
274 es2015::parameters(
275 es2015::parameters::Config {
276 ignore_function_length: loose || assumptions.ignore_function_length
277 },
278 unresolved_mark
279 )
280 );
281 let pass = add!(pass, ArrowFunctions, es2015::arrow(unresolved_mark));
282 let pass = add!(pass, DuplicateKeys, es2015::duplicate_keys());
283 let pass = add!(pass, StickyRegex, es2015::sticky_regex());
284 let pass = add!(pass, TypeOfSymbol, es2015::instance_of());
285 let pass = add!(
286 pass,
287 TypeOfSymbol,
288 es2015::typeof_symbol(es2015::typeof_symbol::Config { loose })
289 );
290 let pass = add!(
291 pass,
292 ComputedProperties,
293 es2015::computed_properties(es2015::computed_props::Config { loose }),
294 true
295 );
296 let pass = add!(
297 pass,
298 Destructuring,
299 es2015::destructuring(es2015::destructuring::Config { loose }),
300 true
301 );
302 let pass = add!(
303 pass,
304 BlockScoping,
305 es2015::block_scoping(unresolved_mark),
306 true
307 );
308 let pass = add!(
309 pass,
310 Regenerator,
311 generator(unresolved_mark, comments),
312 true
313 );
314
315 let pass = add!(pass, PropertyLiterals, es3::property_literals());
327 let pass = add!(
328 pass,
329 MemberExpressionLiterals,
330 es3::member_expression_literals()
331 );
332 let pass = add!(pass, ReservedWords, es3::reserved_words(dynamic_import));
333
334 let pass = add!(pass, BugfixEdgeDefaultParam, bugfixes::edge_default_param());
336 let pass = add!(
337 pass,
338 BugfixAsyncArrowsInClass,
339 bugfixes::async_arrows_in_class(unresolved_mark)
340 );
341 let pass = add!(
342 pass,
343 BugfixTaggedTemplateCaching,
344 bugfixes::template_literal_caching()
345 );
346
347 add!(
348 pass,
349 BugfixSafariIdDestructuringCollisionInFunctionExpression,
350 bugfixes::safari_id_destructuring_collision_in_function_expression()
351 )
352}
353
354pub fn transform_from_env<C>(
355 unresolved_mark: Mark,
356 comments: Option<C>,
357 env_config: EnvConfig,
358 assumptions: Assumptions,
359) -> impl Pass
360where
361 C: Comments + Clone,
362{
363 let pass = Optional::new(
364 node_colon_prefix_strip::strip_node_colon_prefix(unresolved_mark),
365 env_config.feature_config.targets.node.is_some_and(|v| {
366 v < Version {
368 major: 14,
369 minor: 18,
370 patch: 0,
371 } || v.major == 15
372 }),
373 );
374
375 let pass = (
376 pass,
377 transform_internal(
378 unresolved_mark,
379 comments,
380 assumptions,
381 env_config.config.loose,
382 env_config.config.dynamic_import,
383 env_config.config.debug,
384 move |f| env_config.feature_config.caniuse(f),
385 ),
386 );
387
388 if env_config.config.debug {
389 println!("Targets: {:?}", &env_config.core_js_config.targets);
390 }
391
392 (
393 pass,
394 visit_mut_pass(Polyfills {
395 mode: env_config.config.mode,
396 regenerator: false,
397 corejs: env_config.config.core_js.unwrap_or(Version {
398 major: 3,
399 minor: 0,
400 patch: 0,
401 }),
402 shipped_proposals: env_config.config.shipped_proposals,
403 targets: env_config.core_js_config.targets,
404 includes: env_config.core_js_config.included_modules,
405 excludes: env_config.core_js_config.excluded_modules,
406 unresolved_mark,
407 }),
408 )
409}
410
411pub fn transform_from_es_version<C>(
412 unresolved_mark: Mark,
413 comments: Option<C>,
414 es_version: EsVersion,
415 assumptions: Assumptions,
416 loose: bool,
417) -> impl Pass
418where
419 C: Comments + Clone,
420{
421 transform_internal(
422 unresolved_mark,
423 comments,
424 assumptions,
425 loose,
426 true,
427 false,
428 move |f| es_version.caniuse(f),
429 )
430}
431
432#[derive(Debug)]
433struct Polyfills {
434 mode: Option<Mode>,
435 targets: Arc<Versions>,
436 shipped_proposals: bool,
437 corejs: Version,
438 regenerator: bool,
439 includes: FxHashSet<String>,
440 excludes: FxHashSet<String>,
441 unresolved_mark: Mark,
442}
443impl Polyfills {
444 fn collect<T>(&mut self, m: &mut T) -> Vec<Atom>
445 where
446 T: VisitWith<corejs2::UsageVisitor>
447 + VisitWith<corejs3::UsageVisitor>
448 + VisitMutWith<corejs2::Entry>
449 + VisitMutWith<corejs3::Entry>,
450 {
451 let required = match self.mode {
452 None => Default::default(),
453 Some(Mode::Usage) => {
454 let mut r = match self.corejs {
455 Version { major: 2, .. } => {
456 let mut v = corejs2::UsageVisitor::new(self.targets.clone());
457 m.visit_with(&mut v);
458
459 v.required
460 }
461 Version { major: 3, .. } => {
462 let mut v = corejs3::UsageVisitor::new(
463 self.targets.clone(),
464 self.shipped_proposals,
465 self.corejs,
466 );
467 m.visit_with(&mut v);
468 v.required
469 }
470
471 _ => unimplemented!("corejs version other than 2 / 3"),
472 };
473
474 if regenerator::is_required(m) {
475 r.insert("regenerator-runtime/runtime.js");
476 }
477
478 r
479 }
480 Some(Mode::Entry) => match self.corejs {
481 Version { major: 2, .. } => {
482 let mut v = corejs2::Entry::new(self.targets.clone(), self.regenerator);
483 m.visit_mut_with(&mut v);
484 v.imports
485 }
486
487 Version { major: 3, .. } => {
488 let mut v =
489 corejs3::Entry::new(self.targets.clone(), self.corejs, !self.regenerator);
490 m.visit_mut_with(&mut v);
491 v.imports
492 }
493
494 _ => unimplemented!("corejs version other than 2 / 3"),
495 },
496 };
497 required
498 .iter()
499 .filter(|s| {
500 !s.starts_with("esnext") || !required.contains(&s.replace("esnext", "es").as_str())
501 })
502 .filter(|s| !self.excludes.contains(&***s))
503 .map(|s| -> Atom {
504 if *s != "regenerator-runtime/runtime.js" {
505 format!("core-js/modules/{s}.js").into()
506 } else {
507 "regenerator-runtime/runtime.js".to_string().into()
508 }
509 })
510 .chain(self.includes.iter().map(|s| {
511 if s != "regenerator-runtime/runtime.js" {
512 format!("core-js/modules/{s}.js").into()
513 } else {
514 "regenerator-runtime/runtime.js".to_string().into()
515 }
516 }))
517 .collect::<Vec<_>>()
518 }
519}
520impl VisitMut for Polyfills {
521 fn visit_mut_module(&mut self, m: &mut Module) {
522 let span = m.span;
523 let required = self.collect(m);
524 if cfg!(debug_assertions) {
525 let mut v = required.into_iter().collect::<Vec<_>>();
526 v.sort();
527 prepend_stmts(
528 &mut m.body,
529 v.into_iter().map(|src| {
530 ImportDecl {
531 span,
532 specifiers: Vec::new(),
533 src: Str {
534 span: DUMMY_SP,
535 raw: None,
536 value: src,
537 }
538 .into(),
539 type_only: false,
540 with: None,
541 phase: Default::default(),
542 }
543 .into()
544 }),
545 );
546 } else {
547 prepend_stmts(
548 &mut m.body,
549 required.into_iter().map(|src| {
550 ImportDecl {
551 span,
552 specifiers: Vec::new(),
553 src: Str {
554 span: DUMMY_SP,
555 raw: None,
556 value: src,
557 }
558 .into(),
559 type_only: false,
560 with: None,
561 phase: Default::default(),
562 }
563 .into()
564 }),
565 );
566 }
567
568 m.body.retain(|item| !matches!(item, ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { src, .. })) if src.span == DUMMY_SP && src.value == atom!("")));
569 }
570
571 fn visit_mut_script(&mut self, m: &mut Script) {
572 let span = m.span;
573 let required = self.collect(m);
574 if cfg!(debug_assertions) {
575 let mut v = required.into_iter().collect::<Vec<_>>();
576 v.sort();
577 prepend_stmts(
578 &mut m.body,
579 v.into_iter().map(|src| {
580 ExprStmt {
581 span: DUMMY_SP,
582 expr: CallExpr {
583 span,
584 callee: Ident {
585 ctxt: SyntaxContext::empty().apply_mark(self.unresolved_mark),
586 sym: atom!("require"),
587 ..Default::default()
588 }
589 .as_callee(),
590 args: vec![Str {
591 span: DUMMY_SP,
592 value: src,
593 raw: None,
594 }
595 .as_arg()],
596 type_args: None,
597 ..Default::default()
598 }
599 .into(),
600 }
601 .into()
602 }),
603 );
604 } else {
605 prepend_stmts(
606 &mut m.body,
607 required.into_iter().map(|src| {
608 ExprStmt {
609 span: DUMMY_SP,
610 expr: CallExpr {
611 span,
612 callee: Ident {
613 ctxt: SyntaxContext::empty().apply_mark(self.unresolved_mark),
614 sym: atom!("require"),
615 ..Default::default()
616 }
617 .as_callee(),
618 args: vec![Str {
619 span: DUMMY_SP,
620 value: src,
621 raw: None,
622 }
623 .as_arg()],
624 ..Default::default()
625 }
626 .into(),
627 }
628 .into()
629 }),
630 );
631 }
632 }
633}
634
635#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
636pub enum Mode {
637 #[serde(rename = "usage")]
638 Usage,
639 #[serde(rename = "entry")]
640 Entry,
641}
642
643#[derive(Debug, Clone, Deserialize, Default)]
644#[serde(rename_all = "camelCase")]
645pub struct Config {
646 #[serde(default)]
647 pub mode: Option<Mode>,
648
649 #[serde(default)]
650 pub debug: bool,
651
652 #[serde(default)]
653 pub dynamic_import: bool,
654
655 #[serde(default)]
656 pub loose: bool,
657
658 #[serde(default)]
663 pub skip: Vec<Atom>,
664
665 #[serde(default)]
666 pub include: Vec<FeatureOrModule>,
667
668 #[serde(default)]
669 pub exclude: Vec<FeatureOrModule>,
670
671 #[serde(default)]
673 pub core_js: Option<Version>,
674
675 #[serde(default)]
676 pub targets: Option<Targets>,
677
678 #[serde(default)]
679 pub path: Option<PathBuf>,
680
681 #[serde(default)]
682 pub shipped_proposals: bool,
683
684 #[serde(default)]
685 pub force_all_transforms: bool,
686
687 #[serde(default)]
688 pub bugfixes: bool,
689}
690
691#[derive(Debug, Clone, Default)]
692pub struct FeatureConfig {
693 targets: Arc<Versions>,
694 include: Vec<Feature>,
695 exclude: Vec<Feature>,
696 is_any_target: bool,
697 force_all_transforms: bool,
698 bugfixes: bool,
699}
700
701struct CoreJSConfig {
702 targets: Arc<Versions>,
703 included_modules: FxHashSet<String>,
704 excluded_modules: FxHashSet<String>,
705}
706
707pub struct EnvConfig {
708 config: Config,
709 feature_config: Arc<FeatureConfig>,
710 core_js_config: CoreJSConfig,
711}
712
713impl From<Config> for EnvConfig {
714 fn from(mut config: Config) -> Self {
715 let targets = targets_to_versions(config.targets.take(), config.path.take())
716 .expect("failed to parse targets");
717 let is_any_target = targets.is_any_target();
718
719 let (include, included_modules) = FeatureOrModule::split(config.include.clone());
720 let (exclude, excluded_modules) = FeatureOrModule::split(config.exclude.clone());
721
722 let feature_config = FeatureConfig {
723 targets: Arc::clone(&targets),
724 include,
725 exclude,
726 is_any_target,
727 force_all_transforms: config.force_all_transforms,
728 bugfixes: config.bugfixes,
729 };
730 let core_js_config = CoreJSConfig {
731 targets: Arc::clone(&targets),
732 included_modules,
733 excluded_modules,
734 };
735 Self {
736 config,
737 feature_config: Arc::new(feature_config),
738 core_js_config,
739 }
740 }
741}
742
743impl EnvConfig {
744 pub fn get_feature_config(&self) -> Arc<FeatureConfig> {
745 Arc::clone(&self.feature_config)
746 }
747}
748
749impl Caniuse for FeatureConfig {
750 fn caniuse(&self, feature: Feature) -> bool {
751 if self.exclude.contains(&feature) {
752 return true;
753 }
754
755 if self.force_all_transforms || self.is_any_target {
756 return false;
757 }
758
759 if self.include.contains(&feature) {
760 return false;
761 }
762
763 !feature.should_enable(&self.targets, self.bugfixes, false)
764 }
765}
766
767#[derive(Debug, Clone, Deserialize, FromVariant)]
768#[serde(untagged)]
769pub enum FeatureOrModule {
770 Feature(Feature),
771 CoreJsModule(String),
772}
773
774impl FeatureOrModule {
775 pub fn split(vec: Vec<FeatureOrModule>) -> (Vec<Feature>, FxHashSet<String>) {
776 let mut features: Vec<_> = Default::default();
777 let mut modules: FxHashSet<_> = Default::default();
778
779 for v in vec {
780 match v {
781 FeatureOrModule::Feature(f) => features.push(f),
782 FeatureOrModule::CoreJsModule(m) => {
783 modules.insert(m);
784 }
785 }
786 }
787
788 (features, modules)
789 }
790}
791
792impl Caniuse for EsVersion {
793 fn caniuse(&self, feature: Feature) -> bool {
794 if self == &EsVersion::EsNext {
796 return true;
797 }
798
799 match feature {
800 Feature::ClassProperties
802 | Feature::ClassStaticBlock
803 | Feature::PrivatePropertyInObject => *self >= EsVersion::Es2022,
804
805 Feature::LogicalAssignmentOperators => *self >= EsVersion::Es2021,
807
808 Feature::ExportNamespaceFrom
810 | Feature::NullishCoalescing
811 | Feature::OptionalChaining => *self >= EsVersion::Es2020,
812
813 Feature::OptionalCatchBinding => *self >= EsVersion::Es2019,
815
816 Feature::ObjectRestSpread
818 | Feature::DotAllRegex
819 | Feature::NamedCapturingGroupsRegex
820 | Feature::UnicodePropertyRegex => *self >= EsVersion::Es2018,
821
822 Feature::AsyncToGenerator => *self >= EsVersion::Es2017,
824
825 Feature::ExponentiationOperator => *self >= EsVersion::Es2016,
827
828 Feature::ArrowFunctions
830 | Feature::BlockScopedFunctions
831 | Feature::BlockScoping
832 | Feature::Classes
833 | Feature::ComputedProperties
834 | Feature::Destructuring
835 | Feature::DuplicateKeys
836 | Feature::ForOf
837 | Feature::FunctionName
838 | Feature::NewTarget
839 | Feature::ObjectSuper
840 | Feature::Parameters
841 | Feature::Regenerator
842 | Feature::ShorthandProperties
843 | Feature::Spread
844 | Feature::StickyRegex
845 | Feature::TemplateLiterals
846 | Feature::TypeOfSymbol
847 | Feature::UnicodeRegex => *self >= EsVersion::Es2015,
848
849 Feature::PropertyLiterals
851 | Feature::MemberExpressionLiterals
852 | Feature::ReservedWords => *self >= EsVersion::Es5,
853
854 Feature::BugfixAsyncArrowsInClass
856 | Feature::BugfixEdgeDefaultParam
857 | Feature::BugfixTaggedTemplateCaching
858 | Feature::BugfixSafariIdDestructuringCollisionInFunctionExpression
859 | Feature::BugfixTransformEdgeFunctionName
860 | Feature::BugfixTransformSafariBlockShadowing
861 | Feature::BugfixTransformSafariForShadowing
862 | Feature::BugfixTransformV8SpreadParametersInOptionalChaining
863 | Feature::BugfixTransformV8StaticClassFieldsRedefineReadonly
864 | Feature::BugfixTransformFirefoxClassInComputedClassKey
865 | Feature::BugfixTransformSafariClassFieldInitializerScope => true,
866
867 _ => true,
868 }
869 }
870}