1
1
import { parseString } from "xml2js"
2
2
3
- import type {
4
- AtomElement ,
5
- AtomResult ,
6
- RSSChannel ,
7
- RSSItem ,
8
- RSSResult ,
9
- } from "../types"
3
+ import { RSS_DISPLAY_COUNT } from "../constants"
4
+ import type { AtomElement , AtomResult , RSSItem , RSSResult } from "../types"
10
5
import { isValidDate } from "../utils/date"
11
6
12
7
/**
@@ -18,108 +13,125 @@ export const fetchRSS = async (xmlUrl: string | string[]) => {
18
13
const urls = Array . isArray ( xmlUrl ) ? xmlUrl : [ xmlUrl ]
19
14
const allItems : RSSItem [ ] [ ] = [ ]
20
15
for ( const url of urls ) {
21
- const response = ( await fetchXml ( url ) ) as RSSResult | AtomResult
16
+ try {
17
+ const response = ( await fetchXml ( url ) ) as RSSResult | AtomResult
22
18
23
- if ( "rss" in response ) {
24
- const [ mainChannel ] = response . rss . channel as RSSChannel [ ]
25
- const [ source ] = mainChannel . title
26
- const [ sourceUrl ] = mainChannel . link
27
- const channelImage = mainChannel . image ? mainChannel . image [ 0 ] . url [ 0 ] : ""
19
+ if ( "rss" in response ) {
20
+ const [ mainChannel ] = response . rss . channel
21
+ const [ source ] = mainChannel . title
22
+ const [ sourceUrl ] = mainChannel . link
23
+ const channelImage = mainChannel . image
24
+ ? mainChannel . image [ 0 ] . url [ 0 ]
25
+ : ""
28
26
29
- const parsedRssItems = mainChannel . item
30
- // Filter out items with invalid dates
31
- . filter ( ( item ) => {
32
- if ( ! item . pubDate ) return false
33
- const [ pubDate ] = item . pubDate
34
- return isValidDate ( pubDate )
35
- } )
36
- // Sort by pubDate (most recent is first in array
37
- . sort ( ( a , b ) => {
38
- const dateA = new Date ( a . pubDate [ 0 ] )
39
- const dateB = new Date ( b . pubDate [ 0 ] )
40
- return dateB . getTime ( ) - dateA . getTime ( )
41
- } )
42
- // Map to RSSItem object
43
- . map ( ( item ) => {
44
- const getImgSrc = ( ) => {
45
- if ( item [ "content:encoded" ] )
46
- return item [ "content:encoded" ] [ 0 ] . match (
47
- / h t t p s ? : \/ \/ [ ^ " ] * ?\. ( j p e ? g | p n g | w e b p ) / g
48
- ) ?. [ 0 ]
49
- if ( item . enclosure ) return item . enclosure [ 0 ] . $ . url
50
- if ( item [ "media:content" ] ) return item [ "media:content" ] [ 0 ] . $ . url
51
- return channelImage
52
- }
53
- return {
54
- pubDate : item . pubDate [ 0 ] ,
55
- title : item . title [ 0 ] ,
56
- link : item . link [ 0 ] ,
57
- imgSrc : getImgSrc ( ) ,
58
- source,
59
- sourceUrl,
60
- sourceFeedUrl : url ,
61
- } as RSSItem
62
- } )
27
+ const parsedRssItems = mainChannel . item
28
+ // Filter out items with invalid dates
29
+ . filter ( ( item ) => {
30
+ if ( ! item . pubDate ) return false
31
+ const [ pubDate ] = item . pubDate
32
+ return isValidDate ( pubDate )
33
+ } )
34
+ // Sort by pubDate (most recent is first in array
35
+ . sort ( ( a , b ) => {
36
+ const dateA = new Date ( a . pubDate [ 0 ] )
37
+ const dateB = new Date ( b . pubDate [ 0 ] )
38
+ return dateB . getTime ( ) - dateA . getTime ( )
39
+ } )
40
+ // Map to RSSItem object
41
+ . map ( ( item ) => {
42
+ const getImgSrc = ( ) => {
43
+ if ( item [ "content:encoded" ] )
44
+ return item [ "content:encoded" ] [ 0 ] . match (
45
+ / h t t p s ? : \/ \/ [ ^ " ] * ?\. ( j p e ? g | p n g | w e b p ) / g
46
+ ) ?. [ 0 ]
47
+ if ( item . enclosure ) return item . enclosure [ 0 ] . $ . url
48
+ if ( item [ "media:content" ] ) return item [ "media:content" ] [ 0 ] . $ . url
49
+ return channelImage
50
+ }
51
+ return {
52
+ pubDate : item . pubDate [ 0 ] ,
53
+ title : item . title [ 0 ] ,
54
+ link : item . link [ 0 ] ,
55
+ imgSrc : getImgSrc ( ) ,
56
+ source,
57
+ sourceUrl,
58
+ sourceFeedUrl : url ,
59
+ }
60
+ } )
63
61
64
- allItems . push ( parsedRssItems )
65
- } else if ( "feed" in response ) {
66
- const [ source ] = response . feed . title
67
- const [ sourceUrl ] = response . feed . id
68
- const feedImage = response . feed . icon ?. [ 0 ]
62
+ allItems . push ( parsedRssItems )
63
+ } else if ( "feed" in response ) {
64
+ const [ source ] = response . feed . title
65
+ const [ sourceUrl ] = response . feed . id
66
+ const feedImage = response . feed . icon ?. [ 0 ]
69
67
70
- const parsedAtomItems = response . feed . entry
71
- // Filter out items with invalid dates
72
- . filter ( ( entry ) => {
73
- if ( ! entry . updated ) return false
74
- const [ published ] = entry . updated
75
- return isValidDate ( published )
76
- } )
77
- // Sort by published (most recent is first in array
78
- . sort ( ( a , b ) => {
79
- const dateA = new Date ( a . updated [ 0 ] )
80
- const dateB = new Date ( b . updated [ 0 ] )
81
- return dateB . getTime ( ) - dateA . getTime ( )
82
- } )
83
- // Map to RSSItem object
84
- . map ( ( entry ) => {
85
- const getString = ( el ?: AtomElement [ ] ) : string => {
86
- if ( ! el ) return ""
87
- const [ firstEl ] = el
88
- if ( typeof firstEl === "string" ) return firstEl
89
- return firstEl . _ || ""
90
- }
91
- const getHref = ( ) : string => {
92
- if ( ! entry . link ) {
93
- console . warn ( `No link found for RSS url: ${ url } ` )
94
- return ""
68
+ const parsedAtomItems = response . feed . entry
69
+ // Filter out items with invalid dates
70
+ . filter ( ( entry ) => {
71
+ if ( ! entry . updated ) return false
72
+ const [ published ] = entry . updated
73
+ return isValidDate ( published )
74
+ } )
75
+ // Sort by published (most recent is first in array
76
+ . sort ( ( a , b ) => {
77
+ const dateA = new Date ( a . updated [ 0 ] )
78
+ const dateB = new Date ( b . updated [ 0 ] )
79
+ return dateB . getTime ( ) - dateA . getTime ( )
80
+ } )
81
+ // Map to RSSItem object
82
+ . map ( ( entry ) => {
83
+ const getString = ( el ?: AtomElement [ ] ) : string => {
84
+ if ( ! el ) return ""
85
+ const [ firstEl ] = el
86
+ if ( typeof firstEl === "string" ) return firstEl
87
+ return firstEl . _ || ""
88
+ }
89
+ const getHref = ( ) : string => {
90
+ if ( ! entry . link ) {
91
+ console . warn ( `No link found for RSS url: ${ url } ` )
92
+ return ""
93
+ }
94
+ const link = entry . link [ 0 ]
95
+ if ( typeof link === "string" ) return link
96
+ return link . $ . href || ""
97
+ }
98
+ const getImgSrc = ( ) : string => {
99
+ const imgRegEx = / h t t p s ? : \/ \/ [ ^ " ] * ?\. ( j p e ? g | p n g | w e b p ) / g
100
+ const contentMatch = getString ( entry . content ) . match ( imgRegEx )
101
+ if ( contentMatch ) return contentMatch [ 0 ]
102
+ const summaryMatch = getString ( entry . summary ) . match ( imgRegEx )
103
+ if ( summaryMatch ) return summaryMatch [ 0 ]
104
+ return feedImage || ""
105
+ }
106
+ return {
107
+ pubDate : entry . updated [ 0 ] ,
108
+ title : getString ( entry . title ) ,
109
+ link : getHref ( ) ,
110
+ imgSrc : getImgSrc ( ) ,
111
+ source,
112
+ sourceUrl,
113
+ sourceFeedUrl : url ,
95
114
}
96
- const link = entry . link [ 0 ]
97
- if ( typeof link === "string" ) return link
98
- return link . $ . href || ""
99
- }
100
- const getImgSrc = ( ) : string => {
101
- const imgRegEx = / h t t p s ? : \/ \/ [ ^ " ] * ?\. ( j p e ? g | p n g | w e b p ) / g
102
- const contentMatch = getString ( entry . content ) . match ( imgRegEx )
103
- if ( contentMatch ) return contentMatch [ 0 ]
104
- const summaryMatch = getString ( entry . summary ) . match ( imgRegEx )
105
- if ( summaryMatch ) return summaryMatch [ 0 ]
106
- return feedImage || ""
107
- }
108
- return {
109
- pubDate : entry . updated [ 0 ] ,
110
- title : getString ( entry . title ) ,
111
- link : getHref ( ) ,
112
- imgSrc : getImgSrc ( ) ,
113
- source,
114
- sourceUrl,
115
- sourceFeedUrl : url ,
116
- } as RSSItem
117
- } )
115
+ } )
118
116
119
- allItems . push ( parsedAtomItems )
117
+ allItems . push ( parsedAtomItems )
118
+ } else {
119
+ throw new Error (
120
+ `Error parsing XML, invalid RSSResult or AtomResult type: ${ url } `
121
+ )
122
+ }
123
+ } catch ( error ) {
124
+ console . error ( error instanceof Error ? error . message : error )
125
+ // Do not break build for single fetch failure
126
+ continue
120
127
}
121
128
}
122
- return allItems as RSSItem [ ] [ ]
129
+
130
+ // Only break build if insufficient number of items fetched
131
+ if ( allItems . length < RSS_DISPLAY_COUNT )
132
+ throw new Error ( "Insufficient number of RSS items fetched" )
133
+
134
+ return allItems
123
135
}
124
136
125
137
/**
@@ -135,16 +147,12 @@ export const fetchXml = async (url: string) => {
135
147
credentials : "omit" , // Don't send or receive cookies
136
148
} )
137
149
const xml = await response . text ( )
138
- let returnObject : Record < string , unknown > = { }
139
- parseString ( xml , ( err , result ) => {
140
- if ( err ) {
141
- throw err // Throw the error to be caught by the outer try-catch
142
- }
143
- returnObject = result
150
+ return await new Promise < Record < string , unknown > > ( ( resolve , reject ) => {
151
+ parseString ( xml , ( err , result ) => {
152
+ err ? reject ( err ) : resolve ( result )
153
+ } )
144
154
} )
145
- return returnObject
146
155
} catch ( error ) {
147
- console . error ( "Error fetching or parsing XML:" , url , error )
148
- throw error
156
+ throw new Error ( `Error fetching or parsing XML: ${ url } ` )
149
157
}
150
158
}
0 commit comments