1use std::{fmt::Display, future::Future, str::FromStr};
2
3use anyhow::{Result, bail};
4use next_taskless::expand_next_js_template;
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use swc_core::{
7 common::{GLOBALS, Spanned, source_map::SmallPos},
8 ecma::ast::{Expr, Lit, Program},
9};
10use turbo_rcstr::{RcStr, rcstr};
11use turbo_tasks::{
12 FxIndexMap, NonLocalValue, ResolvedVc, TaskInput, ValueDefault, Vc, trace::TraceRawVcs,
13 util::WrapFuture,
14};
15use turbo_tasks_fs::{
16 self, File, FileContent, FileSystem, FileSystemPath, json::parse_json_rope_with_source_context,
17 rope::Rope,
18};
19use turbopack::module_options::RuleCondition;
20use turbopack_core::{
21 asset::AssetContent,
22 compile_time_info::{CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment},
23 condition::ContextCondition,
24 issue::{
25 Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
26 OptionStyledString, StyledString,
27 },
28 module::Module,
29 source::Source,
30 virtual_source::VirtualSource,
31};
32use turbopack_ecmascript::{
33 EcmascriptParsable,
34 analyzer::{ConstantValue, JsValue, ObjectPart},
35 parse::ParseResult,
36};
37
38use crate::{
39 embed_js::next_js_fs,
40 next_config::{NextConfig, RouteHas},
41 next_import_map::get_next_package,
42 next_manifests::MiddlewareMatcher,
43 next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
44};
45
46const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
47
48#[turbo_tasks::value(transparent)]
51pub struct OptionEnvMap(#[turbo_tasks(trace_ignore)] FxIndexMap<RcStr, Option<RcStr>>);
52
53pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
54 let mut defines = FxIndexMap::default();
55
56 for (k, v) in define_env {
57 defines
58 .entry(
59 k.split('.')
60 .map(|s| DefinableNameSegment::Name(s.into()))
61 .collect::<Vec<_>>(),
62 )
63 .or_insert_with(|| {
64 if let Some(v) = v {
65 let val = serde_json::Value::from_str(v);
66 match val {
67 Ok(v) => v.into(),
68 _ => CompileTimeDefineValue::Evaluate(v.clone()),
69 }
70 } else {
71 CompileTimeDefineValue::Undefined
72 }
73 });
74 }
75
76 CompileTimeDefines(defines)
77}
78
79#[derive(
80 Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, Serialize, Deserialize, TraceRawVcs,
81)]
82pub enum PathType {
83 PagesPage,
84 PagesApi,
85 Data,
86}
87
88#[turbo_tasks::function]
90pub async fn pathname_for_path(
91 server_root: FileSystemPath,
92 server_path: FileSystemPath,
93 path_ty: PathType,
94) -> Result<Vc<RcStr>> {
95 let server_path_value = server_path.clone();
96 let path = if let Some(path) = server_root.get_path_to(&server_path_value) {
97 path
98 } else {
99 bail!(
100 "server_path ({}) is not in server_root ({})",
101 server_path.value_to_string().await?,
102 server_root.value_to_string().await?
103 )
104 };
105 let path = match (path_ty, path) {
106 (PathType::Data, "") => rcstr!("/index"),
108 (_, path) => format!("/{path}").into(),
111 };
112
113 Ok(Vc::cell(path))
114}
115
116pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
120 if pathname == "/" {
121 "/index".to_string()
122 } else if pathname == "/index" || pathname.starts_with("/index/") {
123 format!("/index{pathname}")
124 } else {
125 pathname.to_string()
126 }
127}
128
129pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
131 format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
132}
133
134#[turbo_tasks::function]
135pub async fn get_transpiled_packages(
136 next_config: Vc<NextConfig>,
137 project_path: FileSystemPath,
138) -> Result<Vc<Vec<RcStr>>> {
139 let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;
140
141 let default_transpiled_packages: Vec<RcStr> = load_next_js_templateon(
142 project_path,
143 rcstr!("dist/lib/default-transpiled-packages.json"),
144 )
145 .await?;
146
147 transpile_packages.extend(default_transpiled_packages.iter().cloned());
148
149 Ok(Vc::cell(transpile_packages))
150}
151
152pub async fn foreign_code_context_condition(
153 next_config: Vc<NextConfig>,
154 project_path: FileSystemPath,
155) -> Result<ContextCondition> {
156 let transpiled_packages = get_transpiled_packages(next_config, project_path.clone()).await?;
157
158 let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
163 get_next_package(project_path.clone())
164 .await?
165 .join(NEXT_TEMPLATE_PATH)?,
166 ));
167
168 let result = ContextCondition::all(vec![
169 ContextCondition::InDirectory("node_modules".to_string()),
170 not_next_template_dir,
171 ContextCondition::not(ContextCondition::any(
172 transpiled_packages
173 .iter()
174 .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
175 .collect(),
176 )),
177 ]);
178 Ok(result)
179}
180
181pub async fn internal_assets_conditions() -> Result<ContextCondition> {
188 Ok(ContextCondition::any(vec![
189 ContextCondition::InPath(next_js_fs().root().owned().await?),
190 ContextCondition::InPath(
191 turbopack_ecmascript_runtime::embed_fs()
192 .root()
193 .owned()
194 .await?,
195 ),
196 ContextCondition::InPath(turbopack_node::embed_js::embed_fs().root().owned().await?),
197 ]))
198}
199
200pub fn app_function_name(page: impl Display) -> String {
201 format!("app{page}")
202}
203pub fn pages_function_name(page: impl Display) -> String {
204 format!("pages{page}")
205}
206
207#[derive(
208 Default,
209 PartialEq,
210 Eq,
211 Clone,
212 Copy,
213 Debug,
214 TraceRawVcs,
215 Serialize,
216 Deserialize,
217 Hash,
218 PartialOrd,
219 Ord,
220 TaskInput,
221 NonLocalValue,
222)]
223#[serde(rename_all = "lowercase")]
224pub enum NextRuntime {
225 #[default]
226 NodeJs,
227 #[serde(alias = "experimental-edge")]
228 Edge,
229}
230
231impl NextRuntime {
232 pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
235 match self {
236 NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
237 NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
238 }
239 .into_iter()
240 }
241
242 pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
244 match self {
245 NextRuntime::NodeJs => [rcstr!("node")],
246 NextRuntime::Edge => [rcstr!("edge-light")],
247 }
248 .into_iter()
249 }
250}
251
252#[turbo_tasks::value]
253#[derive(Debug, Clone)]
254pub enum MiddlewareMatcherKind {
255 Str(String),
256 Matcher(MiddlewareMatcher),
257}
258
259#[turbo_tasks::value]
260#[derive(Default, Clone)]
261pub struct NextSourceConfig {
262 pub runtime: NextRuntime,
263
264 pub matcher: Option<Vec<MiddlewareMatcherKind>>,
266
267 pub regions: Option<Vec<RcStr>>,
268}
269
270#[turbo_tasks::value_impl]
271impl ValueDefault for NextSourceConfig {
272 #[turbo_tasks::function]
273 pub fn value_default() -> Vc<Self> {
274 NextSourceConfig::default().cell()
275 }
276}
277
278#[turbo_tasks::value(shared)]
280pub struct NextSourceConfigParsingIssue {
281 source: IssueSource,
282 detail: ResolvedVc<StyledString>,
283}
284
285#[turbo_tasks::value_impl]
286impl NextSourceConfigParsingIssue {
287 #[turbo_tasks::function]
288 pub fn new(source: IssueSource, detail: ResolvedVc<StyledString>) -> Vc<Self> {
289 Self { source, detail }.cell()
290 }
291}
292
293#[turbo_tasks::value_impl]
294impl Issue for NextSourceConfigParsingIssue {
295 fn severity(&self) -> IssueSeverity {
296 IssueSeverity::Warning
297 }
298
299 #[turbo_tasks::function]
300 fn title(&self) -> Vc<StyledString> {
301 StyledString::Text(rcstr!(
302 "Next.js can't recognize the exported `config` field in route"
303 ))
304 .cell()
305 }
306
307 #[turbo_tasks::function]
308 fn stage(&self) -> Vc<IssueStage> {
309 IssueStage::Parse.into()
310 }
311
312 #[turbo_tasks::function]
313 fn file_path(&self) -> Vc<FileSystemPath> {
314 self.source.file_path()
315 }
316
317 #[turbo_tasks::function]
318 fn description(&self) -> Vc<OptionStyledString> {
319 Vc::cell(Some(
320 StyledString::Text(
321 "The exported configuration object in a source file need to have a very specific \
322 format from which some properties can be statically parsed at compiled-time."
323 .into(),
324 )
325 .resolved_cell(),
326 ))
327 }
328
329 #[turbo_tasks::function]
330 fn detail(&self) -> Vc<OptionStyledString> {
331 Vc::cell(Some(self.detail))
332 }
333
334 #[turbo_tasks::function]
335 fn source(&self) -> Vc<OptionIssueSource> {
336 Vc::cell(Some(self.source))
337 }
338}
339
340async fn emit_invalid_config_warning(
341 source: IssueSource,
342 detail: &str,
343 value: &JsValue,
344) -> Result<()> {
345 let (explainer, hints) = value.explain(2, 0);
346 NextSourceConfigParsingIssue::new(
347 source,
348 StyledString::Text(format!("{detail} Got {explainer}.{hints}").into()).cell(),
349 )
350 .to_resolved()
351 .await?
352 .emit();
353 Ok(())
354}
355
356async fn parse_route_matcher_from_js_value(
357 source: IssueSource,
358 value: &JsValue,
359) -> Result<Option<Vec<MiddlewareMatcherKind>>> {
360 let parse_matcher_kind_matcher = |value: &JsValue| {
361 let mut route_has = vec![];
362 if let JsValue::Array { items, .. } = value {
363 for item in items {
364 if let JsValue::Object { parts, .. } = item {
365 let mut route_type = None;
366 let mut route_key = None;
367 let mut route_value = None;
368
369 for matcher_part in parts {
370 if let ObjectPart::KeyValue(part_key, part_value) = matcher_part {
371 match part_key.as_str() {
372 Some("type") => {
373 route_type = part_value.as_str().map(|v| v.to_string())
374 }
375 Some("key") => {
376 route_key = part_value.as_str().map(|v| v.to_string())
377 }
378 Some("value") => {
379 route_value = part_value.as_str().map(|v| v.to_string())
380 }
381 _ => {}
382 }
383 }
384 }
385 let r = match route_type.as_deref() {
386 Some("header") => route_key.map(|route_key| RouteHas::Header {
387 key: route_key.into(),
388 value: route_value.map(From::from),
389 }),
390 Some("cookie") => route_key.map(|route_key| RouteHas::Cookie {
391 key: route_key.into(),
392 value: route_value.map(From::from),
393 }),
394 Some("query") => route_key.map(|route_key| RouteHas::Query {
395 key: route_key.into(),
396 value: route_value.map(From::from),
397 }),
398 Some("host") => route_value.map(|route_value| RouteHas::Host {
399 value: route_value.into(),
400 }),
401 _ => None,
402 };
403
404 if let Some(r) = r {
405 route_has.push(r);
406 }
407 }
408 }
409 }
410
411 route_has
412 };
413
414 let mut matchers = vec![];
415
416 match value {
417 JsValue::Constant(matcher) => {
418 if let Some(matcher) = matcher.as_str() {
419 matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
420 } else {
421 emit_invalid_config_warning(
422 source,
423 "The matcher property must be a string or array of strings",
424 value,
425 )
426 .await?;
427 }
428 }
429 JsValue::Array { items, .. } => {
430 for item in items {
431 if let Some(matcher) = item.as_str() {
432 matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
433 } else if let JsValue::Object { parts, .. } = item {
434 let mut matcher = MiddlewareMatcher::default();
435 for matcher_part in parts {
436 if let ObjectPart::KeyValue(key, value) = matcher_part {
437 match key.as_str() {
438 Some("source") => {
439 if let Some(value) = value.as_str() {
440 matcher.original_source = value.into();
441 }
442 }
443 Some("locale") => {
444 matcher.locale = value.as_bool().unwrap_or_default();
445 }
446 Some("missing") => {
447 matcher.missing = Some(parse_matcher_kind_matcher(value))
448 }
449 Some("has") => {
450 matcher.has = Some(parse_matcher_kind_matcher(value))
451 }
452 _ => {
453 }
455 }
456 }
457 }
458
459 matchers.push(MiddlewareMatcherKind::Matcher(matcher));
460 } else {
461 emit_invalid_config_warning(
462 source,
463 "The matcher property must be a string or array of strings",
464 value,
465 )
466 .await?;
467 }
468 }
469 }
470 _ => {
471 emit_invalid_config_warning(
472 source,
473 "The matcher property must be a string or array of strings",
474 value,
475 )
476 .await?
477 }
478 }
479
480 Ok(if matchers.is_empty() {
481 None
482 } else {
483 Some(matchers)
484 })
485}
486
487#[turbo_tasks::function]
488pub async fn parse_config_from_source(
489 source: ResolvedVc<Box<dyn Source>>,
490 module: ResolvedVc<Box<dyn Module>>,
491 default_runtime: NextRuntime,
492) -> Result<Vc<NextSourceConfig>> {
493 if let Some(ecmascript_asset) = ResolvedVc::try_sidecast::<Box<dyn EcmascriptParsable>>(module)
494 && let ParseResult::Ok {
495 program: Program::Module(module_ast),
496 globals,
497 eval_context,
498 ..
499 } = &*ecmascript_asset.parse_original().await?
500 {
501 for item in &module_ast.body {
502 if let Some(decl) = item
503 .as_module_decl()
504 .and_then(|mod_decl| mod_decl.as_export_decl())
505 .and_then(|export_decl| export_decl.decl.as_var())
506 {
507 for decl in &decl.decls {
508 let decl_ident = decl.name.as_ident();
509
510 if let Some(ident) = decl_ident
513 && ident.sym == "config"
514 {
515 if let Some(init) = decl.init.as_ref() {
516 return WrapFuture::new(
517 async {
518 let value = eval_context.eval(init);
519 Ok(parse_config_from_js_value(
520 IssueSource::from_swc_offsets(
521 source,
522 init.span_lo().to_u32(),
523 init.span_hi().to_u32(),
524 ),
525 &value,
526 default_runtime,
527 )
528 .await?
529 .cell())
530 },
531 |f, ctx| GLOBALS.set(globals, || f.poll(ctx)),
532 )
533 .await;
534 } else {
535 NextSourceConfigParsingIssue::new(
536 IssueSource::from_swc_offsets(
537 source,
538 ident.span_lo().to_u32(),
539 ident.span_hi().to_u32(),
540 ),
541 StyledString::Text(rcstr!(
542 "The exported config object must contain an variable \
543 initializer."
544 ))
545 .cell(),
546 )
547 .to_resolved()
548 .await?
549 .emit();
550 }
551 }
552 else if let Some(ident) = decl_ident
555 && ident.sym == "runtime"
556 {
557 let runtime_value_issue = NextSourceConfigParsingIssue::new(
558 IssueSource::from_swc_offsets(
559 source,
560 ident.span_lo().to_u32(),
561 ident.span_hi().to_u32(),
562 ),
563 StyledString::Text(rcstr!(
564 "The runtime property must be either \"nodejs\" or \"edge\"."
565 ))
566 .cell(),
567 )
568 .to_resolved()
569 .await?;
570 if let Some(init) = decl.init.as_ref() {
571 if let Expr::Lit(Lit::Str(str_value)) = &**init {
574 let mut config = NextSourceConfig::default();
575
576 let runtime = &str_value.value;
577 match runtime.as_str() {
578 "edge" | "experimental-edge" => {
579 config.runtime = NextRuntime::Edge;
580 }
581 "nodejs" => {
582 config.runtime = NextRuntime::NodeJs;
583 }
584 _ => {
585 runtime_value_issue.emit();
586 }
587 }
588
589 return Ok(config.cell());
590 } else {
591 runtime_value_issue.emit();
592 }
593 } else {
594 NextSourceConfigParsingIssue::new(
595 IssueSource::from_swc_offsets(
596 source,
597 ident.span_lo().to_u32(),
598 ident.span_hi().to_u32(),
599 ),
600 StyledString::Text(rcstr!(
601 "The exported segment runtime option must contain an variable \
602 initializer."
603 ))
604 .cell(),
605 )
606 .to_resolved()
607 .await?
608 .emit();
609 }
610 }
611 }
612 }
613 }
614 }
615 let config = NextSourceConfig {
616 runtime: default_runtime,
617 ..Default::default()
618 };
619
620 Ok(config.cell())
621}
622
623async fn parse_config_from_js_value(
624 source: IssueSource,
625 value: &JsValue,
626 default_runtime: NextRuntime,
627) -> Result<NextSourceConfig> {
628 let mut config = NextSourceConfig {
629 runtime: default_runtime,
630 ..Default::default()
631 };
632
633 if let JsValue::Object { parts, .. } = value {
634 for part in parts {
635 match part {
636 ObjectPart::Spread(_) => {
637 emit_invalid_config_warning(
638 source,
639 "Spread properties are not supported in the config export.",
640 value,
641 )
642 .await?
643 }
644 ObjectPart::KeyValue(key, value) => {
645 if let Some(key) = key.as_str() {
646 match key {
647 "runtime" => {
648 if let JsValue::Constant(runtime) = value {
649 if let Some(runtime) = runtime.as_str() {
650 match runtime {
651 "edge" | "experimental-edge" => {
652 config.runtime = NextRuntime::Edge;
653 }
654 "nodejs" => {
655 config.runtime = NextRuntime::NodeJs;
656 }
657 _ => {
658 emit_invalid_config_warning(
659 source,
660 "The runtime property must be either \
661 \"nodejs\" or \"edge\".",
662 value,
663 )
664 .await?;
665 }
666 }
667 }
668 } else {
669 emit_invalid_config_warning(
670 source,
671 "The runtime property must be a constant string.",
672 value,
673 )
674 .await?;
675 }
676 }
677 "matcher" => {
678 config.matcher =
679 parse_route_matcher_from_js_value(source, value).await?;
680 }
681 "regions" => {
682 config.regions = match value {
683 JsValue::Constant(ConstantValue::Str(str)) => {
685 Some(vec![str.to_string().into()])
686 }
687 JsValue::Array { items, .. } => {
691 let mut regions: Vec<RcStr> = Vec::new();
692 for item in items {
693 if let JsValue::Constant(ConstantValue::Str(str)) = item
694 {
695 regions.push(str.to_string().into());
696 } else {
697 emit_invalid_config_warning(
698 source,
699 "Values of the `config.regions` array need to \
700 static strings",
701 item,
702 )
703 .await?;
704 }
705 }
706 Some(regions)
707 }
708 _ => {
709 emit_invalid_config_warning(
710 source,
711 "`config.regions` needs to be a static string or \
712 array of static strings",
713 value,
714 )
715 .await?;
716 None
717 }
718 };
719 }
720 _ => {}
721 }
722 } else {
723 emit_invalid_config_warning(
724 source,
725 "The exported config object must not contain non-constant strings.",
726 key,
727 )
728 .await?;
729 }
730 }
731 }
732 }
733 } else {
734 emit_invalid_config_warning(
735 source,
736 "The exported config object must be a valid object literal.",
737 value,
738 )
739 .await?;
740 }
741
742 Ok(config)
743}
744
745pub async fn load_next_js_template(
748 template_path: &str,
749 project_path: FileSystemPath,
750 replacements: &[(&str, &str)],
751 injections: &[(&str, &str)],
752 imports: &[(&str, Option<&str>)],
753) -> Result<Vc<Box<dyn Source>>> {
754 let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
755
756 let content = file_content_rope(template_path.read()).await?;
757 let content = content.to_str()?;
758
759 let package_root = &*get_next_package(project_path).await?;
760
761 let content = expand_next_js_template(
762 &content,
763 &template_path.path,
764 &package_root.path,
765 replacements.iter().copied(),
766 injections.iter().copied(),
767 imports.iter().copied(),
768 )?;
769
770 let file = File::from(content);
771
772 let source = VirtualSource::new(template_path, AssetContent::file(file.into()));
773
774 Ok(Vc::upcast(source))
775}
776
777#[turbo_tasks::function]
778pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
779 let content = &*content.await?;
780
781 let FileContent::Content(file) = content else {
782 bail!("Expected file content for file");
783 };
784
785 Ok(file.content().to_owned().cell())
786}
787
788async fn virtual_next_js_template_path(
789 project_path: FileSystemPath,
790 file: &str,
791) -> Result<FileSystemPath> {
792 debug_assert!(!file.contains('/'));
793 get_next_package(project_path)
794 .await?
795 .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
796}
797
798pub async fn load_next_js_templateon<T: DeserializeOwned>(
799 project_path: FileSystemPath,
800 path: RcStr,
801) -> Result<T> {
802 let file_path = get_next_package(project_path.clone()).await?.join(&path)?;
803
804 let content = &*file_path.read().await?;
805
806 let FileContent::Content(file) = content else {
807 bail!(
808 "Expected file content at {}",
809 file_path.value_to_string().await?
810 );
811 };
812
813 let result: T = parse_json_rope_with_source_context(file.content())?;
814
815 Ok(result)
816}
817
818pub fn styles_rule_condition() -> RuleCondition {
819 RuleCondition::any(vec![
820 RuleCondition::all(vec![
821 RuleCondition::ResourcePathEndsWith(".css".into()),
822 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
823 ]),
824 RuleCondition::all(vec![
825 RuleCondition::ResourcePathEndsWith(".sass".into()),
826 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
827 ]),
828 RuleCondition::all(vec![
829 RuleCondition::ResourcePathEndsWith(".scss".into()),
830 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
831 ]),
832 RuleCondition::all(vec![
833 RuleCondition::ContentTypeStartsWith("text/css".into()),
834 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
835 "text/css+module".into(),
836 )),
837 ]),
838 RuleCondition::all(vec![
839 RuleCondition::ContentTypeStartsWith("text/sass".into()),
840 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
841 "text/sass+module".into(),
842 )),
843 ]),
844 RuleCondition::all(vec![
845 RuleCondition::ContentTypeStartsWith("text/scss".into()),
846 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
847 "text/scss+module".into(),
848 )),
849 ]),
850 ])
851}
852pub fn module_styles_rule_condition() -> RuleCondition {
853 RuleCondition::any(vec![
854 RuleCondition::ResourcePathEndsWith(".module.css".into()),
855 RuleCondition::ResourcePathEndsWith(".module.scss".into()),
856 RuleCondition::ResourcePathEndsWith(".module.sass".into()),
857 RuleCondition::ContentTypeStartsWith("text/css+module".into()),
858 RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
859 RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
860 ])
861}