1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum NextJsVersionFamily {
10 Next12,
11 Next13,
12 Next14,
13 Next15,
14 Next16,
15}
16
17impl NextJsVersionFamily {
18 #[must_use]
20 pub const fn as_str(self) -> &'static str {
21 match self {
22 Self::Next12 => "next12",
23 Self::Next13 => "next13",
24 Self::Next14 => "next14",
25 Self::Next15 => "next15",
26 Self::Next16 => "next16",
27 }
28 }
29}
30
31impl fmt::Display for NextJsVersionFamily {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 formatter.write_str(self.as_str())
34 }
35}
36
37impl FromStr for NextJsVersionFamily {
38 type Err = NextJsRouteError;
39
40 fn from_str(input: &str) -> Result<Self, Self::Err> {
41 match normalized_label(input)?.as_str() {
42 "next12" | "12" => Ok(Self::Next12),
43 "next13" | "13" => Ok(Self::Next13),
44 "next14" | "14" => Ok(Self::Next14),
45 "next15" | "15" => Ok(Self::Next15),
46 "next16" | "16" => Ok(Self::Next16),
47 _ => Err(NextJsRouteError::UnknownLabel),
48 }
49 }
50}
51
52#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub enum NextJsRouterKind {
55 PagesRouter,
56 AppRouter,
57}
58
59impl NextJsRouterKind {
60 #[must_use]
62 pub const fn as_str(self) -> &'static str {
63 match self {
64 Self::PagesRouter => "pages-router",
65 Self::AppRouter => "app-router",
66 }
67 }
68}
69
70impl fmt::Display for NextJsRouterKind {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 formatter.write_str(self.as_str())
73 }
74}
75
76impl FromStr for NextJsRouterKind {
77 type Err = NextJsRouteError;
78
79 fn from_str(input: &str) -> Result<Self, Self::Err> {
80 match normalized_label(input)?.as_str() {
81 "pagesrouter" | "pages" => Ok(Self::PagesRouter),
82 "approuter" | "app" => Ok(Self::AppRouter),
83 _ => Err(NextJsRouteError::UnknownLabel),
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub enum NextJsDirectoryKind {
91 App,
92 Pages,
93 Components,
94 Public,
95 Styles,
96 Lib,
97 Api,
98 Middleware,
99}
100
101impl NextJsDirectoryKind {
102 #[must_use]
104 pub const fn as_str(self) -> &'static str {
105 match self {
106 Self::App => "app",
107 Self::Pages => "pages",
108 Self::Components => "components",
109 Self::Public => "public",
110 Self::Styles => "styles",
111 Self::Lib => "lib",
112 Self::Api => "api",
113 Self::Middleware => "middleware",
114 }
115 }
116}
117
118impl fmt::Display for NextJsDirectoryKind {
119 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120 formatter.write_str(self.as_str())
121 }
122}
123
124impl FromStr for NextJsDirectoryKind {
125 type Err = NextJsRouteError;
126
127 fn from_str(input: &str) -> Result<Self, Self::Err> {
128 match normalized_label(input)?.as_str() {
129 "app" => Ok(Self::App),
130 "pages" => Ok(Self::Pages),
131 "components" => Ok(Self::Components),
132 "public" => Ok(Self::Public),
133 "styles" => Ok(Self::Styles),
134 "lib" | "library" => Ok(Self::Lib),
135 "api" => Ok(Self::Api),
136 "middleware" => Ok(Self::Middleware),
137 _ => Err(NextJsRouteError::UnknownLabel),
138 }
139 }
140}
141
142#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
144pub enum NextJsFileKind {
145 Page,
146 Layout,
147 Loading,
148 Error,
149 NotFound,
150 Route,
151 Middleware,
152 Config,
153 Metadata,
154}
155
156impl NextJsFileKind {
157 #[must_use]
159 pub const fn as_str(self) -> &'static str {
160 match self {
161 Self::Page => "page",
162 Self::Layout => "layout",
163 Self::Loading => "loading",
164 Self::Error => "error",
165 Self::NotFound => "not-found",
166 Self::Route => "route",
167 Self::Middleware => "middleware",
168 Self::Config => "config",
169 Self::Metadata => "metadata",
170 }
171 }
172}
173
174impl fmt::Display for NextJsFileKind {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 formatter.write_str(self.as_str())
177 }
178}
179
180impl FromStr for NextJsFileKind {
181 type Err = NextJsRouteError;
182
183 fn from_str(input: &str) -> Result<Self, Self::Err> {
184 match normalized_label(input)?.as_str() {
185 "page" => Ok(Self::Page),
186 "layout" => Ok(Self::Layout),
187 "loading" => Ok(Self::Loading),
188 "error" => Ok(Self::Error),
189 "notfound" => Ok(Self::NotFound),
190 "route" => Ok(Self::Route),
191 "middleware" => Ok(Self::Middleware),
192 "config" => Ok(Self::Config),
193 "metadata" => Ok(Self::Metadata),
194 _ => Err(NextJsRouteError::UnknownLabel),
195 }
196 }
197}
198
199#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub enum NextJsRenderingMode {
202 Ssg,
203 Ssr,
204 Isr,
205 Csr,
206 Rsc,
207 Hybrid,
208}
209
210impl NextJsRenderingMode {
211 #[must_use]
213 pub const fn as_str(self) -> &'static str {
214 match self {
215 Self::Ssg => "ssg",
216 Self::Ssr => "ssr",
217 Self::Isr => "isr",
218 Self::Csr => "csr",
219 Self::Rsc => "rsc",
220 Self::Hybrid => "hybrid",
221 }
222 }
223}
224
225impl fmt::Display for NextJsRenderingMode {
226 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227 formatter.write_str(self.as_str())
228 }
229}
230
231impl FromStr for NextJsRenderingMode {
232 type Err = NextJsRouteError;
233
234 fn from_str(input: &str) -> Result<Self, Self::Err> {
235 match normalized_label(input)?.as_str() {
236 "ssg" => Ok(Self::Ssg),
237 "ssr" => Ok(Self::Ssr),
238 "isr" => Ok(Self::Isr),
239 "csr" => Ok(Self::Csr),
240 "rsc" => Ok(Self::Rsc),
241 "hybrid" => Ok(Self::Hybrid),
242 _ => Err(NextJsRouteError::UnknownLabel),
243 }
244 }
245}
246
247#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
249pub enum NextJsRouteKind {
250 Page,
251 ApiRoute,
252 RouteHandler,
253 Middleware,
254}
255
256impl NextJsRouteKind {
257 #[must_use]
259 pub const fn as_str(self) -> &'static str {
260 match self {
261 Self::Page => "page",
262 Self::ApiRoute => "api-route",
263 Self::RouteHandler => "route-handler",
264 Self::Middleware => "middleware",
265 }
266 }
267}
268
269impl fmt::Display for NextJsRouteKind {
270 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271 formatter.write_str(self.as_str())
272 }
273}
274
275impl FromStr for NextJsRouteKind {
276 type Err = NextJsRouteError;
277
278 fn from_str(input: &str) -> Result<Self, Self::Err> {
279 match normalized_label(input)?.as_str() {
280 "page" => Ok(Self::Page),
281 "apiroute" | "api" => Ok(Self::ApiRoute),
282 "routehandler" | "handler" => Ok(Self::RouteHandler),
283 "middleware" => Ok(Self::Middleware),
284 _ => Err(NextJsRouteError::UnknownLabel),
285 }
286 }
287}
288
289#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
291pub enum NextJsRuntimeKind {
292 NodeJs,
293 Edge,
294}
295
296impl NextJsRuntimeKind {
297 #[must_use]
299 pub const fn as_str(self) -> &'static str {
300 match self {
301 Self::NodeJs => "nodejs",
302 Self::Edge => "edge",
303 }
304 }
305}
306
307impl fmt::Display for NextJsRuntimeKind {
308 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
309 formatter.write_str(self.as_str())
310 }
311}
312
313impl FromStr for NextJsRuntimeKind {
314 type Err = NextJsRouteError;
315
316 fn from_str(input: &str) -> Result<Self, Self::Err> {
317 match normalized_label(input)?.as_str() {
318 "nodejs" | "node" => Ok(Self::NodeJs),
319 "edge" => Ok(Self::Edge),
320 _ => Err(NextJsRouteError::UnknownLabel),
321 }
322 }
323}
324
325#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
327pub enum NextJsConfigFile {
328 NextConfigJs,
329 NextConfigMjs,
330 NextConfigTs,
331}
332
333impl NextJsConfigFile {
334 #[must_use]
336 pub const fn as_str(self) -> &'static str {
337 match self {
338 Self::NextConfigJs => "next.config.js",
339 Self::NextConfigMjs => "next.config.mjs",
340 Self::NextConfigTs => "next.config.ts",
341 }
342 }
343}
344
345impl fmt::Display for NextJsConfigFile {
346 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
347 formatter.write_str(self.as_str())
348 }
349}
350
351impl FromStr for NextJsConfigFile {
352 type Err = NextJsRouteError;
353
354 fn from_str(input: &str) -> Result<Self, Self::Err> {
355 match normalized_label(input)?.as_str() {
356 "nextconfigjs" => Ok(Self::NextConfigJs),
357 "nextconfigmjs" => Ok(Self::NextConfigMjs),
358 "nextconfigts" => Ok(Self::NextConfigTs),
359 _ => Err(NextJsRouteError::UnknownLabel),
360 }
361 }
362}
363
364#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
366pub enum NextJsMetadataKind {
367 StaticMetadata,
368 GeneratedMetadata,
369 FileBasedMetadata,
370}
371
372impl NextJsMetadataKind {
373 #[must_use]
375 pub const fn as_str(self) -> &'static str {
376 match self {
377 Self::StaticMetadata => "static-metadata",
378 Self::GeneratedMetadata => "generated-metadata",
379 Self::FileBasedMetadata => "file-based-metadata",
380 }
381 }
382}
383
384impl fmt::Display for NextJsMetadataKind {
385 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386 formatter.write_str(self.as_str())
387 }
388}
389
390impl FromStr for NextJsMetadataKind {
391 type Err = NextJsRouteError;
392
393 fn from_str(input: &str) -> Result<Self, Self::Err> {
394 match normalized_label(input)?.as_str() {
395 "staticmetadata" | "static" => Ok(Self::StaticMetadata),
396 "generatedmetadata" | "generated" => Ok(Self::GeneratedMetadata),
397 "filebasedmetadata" | "filebased" => Ok(Self::FileBasedMetadata),
398 _ => Err(NextJsRouteError::UnknownLabel),
399 }
400 }
401}
402
403#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
405pub struct NextJsRouteSegment(String);
406
407impl NextJsRouteSegment {
408 pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
414 let trimmed = input.trim();
415 if trimmed.is_empty() {
416 return Err(NextJsRouteError::Empty);
417 }
418 if let Some(character) = trimmed
419 .chars()
420 .find(|character| character.is_control() || matches!(character, '/' | '\\'))
421 {
422 return Err(NextJsRouteError::InvalidCharacter { character });
423 }
424 Ok(Self(trimmed.to_string()))
425 }
426
427 #[must_use]
429 pub fn as_str(&self) -> &str {
430 &self.0
431 }
432}
433
434impl fmt::Display for NextJsRouteSegment {
435 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
436 formatter.write_str(self.as_str())
437 }
438}
439
440impl FromStr for NextJsRouteSegment {
441 type Err = NextJsRouteError;
442
443 fn from_str(input: &str) -> Result<Self, Self::Err> {
444 Self::new(input)
445 }
446}
447
448impl TryFrom<&str> for NextJsRouteSegment {
449 type Error = NextJsRouteError;
450
451 fn try_from(value: &str) -> Result<Self, Self::Error> {
452 Self::new(value)
453 }
454}
455
456#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
458pub struct NextJsDynamicSegment(String);
459
460impl NextJsDynamicSegment {
461 pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
467 let trimmed = input.trim();
468 if trimmed.is_empty() {
469 return Err(NextJsRouteError::Empty);
470 }
471 let Some(inner) = dynamic_segment_inner(trimmed) else {
472 return Err(NextJsRouteError::InvalidDynamicSegment);
473 };
474 if inner.is_empty() || !inner.chars().all(is_segment_name_character) {
475 return Err(NextJsRouteError::InvalidDynamicSegment);
476 }
477 Ok(Self(trimmed.to_string()))
478 }
479
480 #[must_use]
482 pub fn as_str(&self) -> &str {
483 &self.0
484 }
485}
486
487impl fmt::Display for NextJsDynamicSegment {
488 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
489 formatter.write_str(self.as_str())
490 }
491}
492
493impl FromStr for NextJsDynamicSegment {
494 type Err = NextJsRouteError;
495
496 fn from_str(input: &str) -> Result<Self, Self::Err> {
497 Self::new(input)
498 }
499}
500
501impl TryFrom<&str> for NextJsDynamicSegment {
502 type Error = NextJsRouteError;
503
504 fn try_from(value: &str) -> Result<Self, Self::Error> {
505 Self::new(value)
506 }
507}
508
509#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
511pub struct NextJsParallelRouteName(String);
512
513impl NextJsParallelRouteName {
514 pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
520 let trimmed = input.trim();
521 if trimmed.is_empty() {
522 return Err(NextJsRouteError::Empty);
523 }
524 let Some(name) = trimmed.strip_prefix('@') else {
525 return Err(NextJsRouteError::InvalidParallelRouteName);
526 };
527 if name.is_empty() || !name.chars().all(is_segment_name_character) {
528 return Err(NextJsRouteError::InvalidParallelRouteName);
529 }
530 Ok(Self(trimmed.to_string()))
531 }
532
533 #[must_use]
535 pub fn as_str(&self) -> &str {
536 &self.0
537 }
538}
539
540impl fmt::Display for NextJsParallelRouteName {
541 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
542 formatter.write_str(self.as_str())
543 }
544}
545
546impl FromStr for NextJsParallelRouteName {
547 type Err = NextJsRouteError;
548
549 fn from_str(input: &str) -> Result<Self, Self::Err> {
550 Self::new(input)
551 }
552}
553
554impl TryFrom<&str> for NextJsParallelRouteName {
555 type Error = NextJsRouteError;
556
557 fn try_from(value: &str) -> Result<Self, Self::Error> {
558 Self::new(value)
559 }
560}
561
562#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
564pub struct NextJsInterceptingRoutePattern(String);
565
566impl NextJsInterceptingRoutePattern {
567 pub fn new(input: &str) -> Result<Self, NextJsRouteError> {
573 let trimmed = input.trim();
574 if trimmed.is_empty() {
575 return Err(NextJsRouteError::Empty);
576 }
577 if !(trimmed.contains('(') && trimmed.contains(')')) {
578 return Err(NextJsRouteError::InvalidInterceptingRoutePattern);
579 }
580 if let Some(character) = trimmed
581 .chars()
582 .find(|character| !is_intercepting_route_character(*character))
583 {
584 return Err(NextJsRouteError::InvalidCharacter { character });
585 }
586 Ok(Self(trimmed.to_string()))
587 }
588
589 #[must_use]
591 pub fn as_str(&self) -> &str {
592 &self.0
593 }
594}
595
596impl fmt::Display for NextJsInterceptingRoutePattern {
597 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
598 formatter.write_str(self.as_str())
599 }
600}
601
602impl FromStr for NextJsInterceptingRoutePattern {
603 type Err = NextJsRouteError;
604
605 fn from_str(input: &str) -> Result<Self, Self::Err> {
606 Self::new(input)
607 }
608}
609
610impl TryFrom<&str> for NextJsInterceptingRoutePattern {
611 type Error = NextJsRouteError;
612
613 fn try_from(value: &str) -> Result<Self, Self::Error> {
614 Self::new(value)
615 }
616}
617
618#[derive(Clone, Copy, Debug, Eq, PartialEq)]
620pub enum NextJsRouteError {
621 Empty,
622 InvalidCharacter { character: char },
623 InvalidDynamicSegment,
624 InvalidParallelRouteName,
625 InvalidInterceptingRoutePattern,
626 UnknownLabel,
627}
628
629impl fmt::Display for NextJsRouteError {
630 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
631 match self {
632 Self::Empty => formatter.write_str("Next.js metadata text cannot be empty"),
633 Self::InvalidCharacter { character } => {
634 write!(
635 formatter,
636 "invalid Next.js metadata character `{character}`"
637 )
638 }
639 Self::InvalidDynamicSegment => formatter.write_str("invalid Next.js dynamic segment"),
640 Self::InvalidParallelRouteName => {
641 formatter.write_str("invalid Next.js parallel route name")
642 }
643 Self::InvalidInterceptingRoutePattern => {
644 formatter.write_str("invalid Next.js intercepting route pattern")
645 }
646 Self::UnknownLabel => formatter.write_str("unknown Next.js metadata label"),
647 }
648 }
649}
650
651impl Error for NextJsRouteError {}
652
653fn dynamic_segment_inner(input: &str) -> Option<&str> {
654 if let Some(inner) = input
655 .strip_prefix("[[...")
656 .and_then(|value| value.strip_suffix("]]"))
657 {
658 return Some(inner);
659 }
660 if let Some(inner) = input
661 .strip_prefix("[...")
662 .and_then(|value| value.strip_suffix(']'))
663 {
664 return Some(inner);
665 }
666 input
667 .strip_prefix('[')
668 .and_then(|value| value.strip_suffix(']'))
669}
670
671const fn is_segment_name_character(character: char) -> bool {
672 character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
673}
674
675const fn is_intercepting_route_character(character: char) -> bool {
676 character.is_ascii_alphanumeric()
677 || matches!(
678 character,
679 '.' | '(' | ')' | '/' | '[' | ']' | '@' | '_' | '-'
680 )
681}
682
683fn normalized_label(input: &str) -> Result<String, NextJsRouteError> {
684 let trimmed = input.trim();
685 if trimmed.is_empty() {
686 return Err(NextJsRouteError::Empty);
687 }
688 Ok(trimmed
689 .chars()
690 .filter(|character| !matches!(character, '-' | '_' | ' ' | '.'))
691 .flat_map(char::to_lowercase)
692 .collect())
693}
694
695#[cfg(test)]
696mod tests {
697 use super::{
698 NextJsConfigFile, NextJsDirectoryKind, NextJsDynamicSegment, NextJsFileKind,
699 NextJsInterceptingRoutePattern, NextJsMetadataKind, NextJsParallelRouteName,
700 NextJsRenderingMode, NextJsRouteError, NextJsRouteKind, NextJsRouteSegment,
701 NextJsRouterKind, NextJsRuntimeKind, NextJsVersionFamily,
702 };
703
704 #[test]
705 fn validates_route_segments() -> Result<(), NextJsRouteError> {
706 let segment = NextJsRouteSegment::new("blog")?;
707 assert_eq!(segment.as_str(), "blog");
708 assert_eq!(NextJsRouteSegment::new(""), Err(NextJsRouteError::Empty));
709 assert_eq!(
710 NextJsRouteSegment::new("blog/posts"),
711 Err(NextJsRouteError::InvalidCharacter { character: '/' })
712 );
713 Ok(())
714 }
715
716 #[test]
717 fn validates_dynamic_segments() -> Result<(), NextJsRouteError> {
718 assert_eq!(NextJsDynamicSegment::new("[id]")?.as_str(), "[id]");
719 assert_eq!(
720 NextJsDynamicSegment::new("[...slug]")?.as_str(),
721 "[...slug]"
722 );
723 assert_eq!(
724 NextJsDynamicSegment::new("[[...slug]]")?.as_str(),
725 "[[...slug]]"
726 );
727 assert_eq!(
728 NextJsDynamicSegment::new("[]"),
729 Err(NextJsRouteError::InvalidDynamicSegment)
730 );
731 assert_eq!(
732 NextJsDynamicSegment::new("[slug.part]"),
733 Err(NextJsRouteError::InvalidDynamicSegment)
734 );
735 Ok(())
736 }
737
738 #[test]
739 fn validates_parallel_and_intercepting_routes() -> Result<(), NextJsRouteError> {
740 assert_eq!(NextJsParallelRouteName::new("@modal")?.as_str(), "@modal");
741 assert_eq!(
742 NextJsParallelRouteName::new("modal"),
743 Err(NextJsRouteError::InvalidParallelRouteName)
744 );
745 assert_eq!(
746 NextJsParallelRouteName::new("@"),
747 Err(NextJsRouteError::InvalidParallelRouteName)
748 );
749 assert_eq!(
750 NextJsInterceptingRoutePattern::new("(.)feed")?.as_str(),
751 "(.)feed"
752 );
753 assert_eq!(
754 NextJsInterceptingRoutePattern::new("feed"),
755 Err(NextJsRouteError::InvalidInterceptingRoutePattern)
756 );
757 Ok(())
758 }
759
760 #[test]
761 fn parses_labels() -> Result<(), NextJsRouteError> {
762 assert_eq!(
763 "next15".parse::<NextJsVersionFamily>()?,
764 NextJsVersionFamily::Next15
765 );
766 assert_eq!(
767 "app-router".parse::<NextJsRouterKind>()?,
768 NextJsRouterKind::AppRouter
769 );
770 assert_eq!(
771 "components".parse::<NextJsDirectoryKind>()?,
772 NextJsDirectoryKind::Components
773 );
774 assert_eq!(
775 "not-found".parse::<NextJsFileKind>()?,
776 NextJsFileKind::NotFound
777 );
778 assert_eq!(
779 "rsc".parse::<NextJsRenderingMode>()?,
780 NextJsRenderingMode::Rsc
781 );
782 assert_eq!(
783 "api-route".parse::<NextJsRouteKind>()?,
784 NextJsRouteKind::ApiRoute
785 );
786 assert_eq!(
787 "node.js".parse::<NextJsRuntimeKind>()?,
788 NextJsRuntimeKind::NodeJs
789 );
790 assert_eq!(
791 "next.config.ts".parse::<NextJsConfigFile>()?,
792 NextJsConfigFile::NextConfigTs
793 );
794 assert_eq!(
795 "file-based-metadata".parse::<NextJsMetadataKind>()?,
796 NextJsMetadataKind::FileBasedMetadata
797 );
798 assert_eq!(NextJsRuntimeKind::Edge.to_string(), "edge");
799 Ok(())
800 }
801}