webring 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +107 -30
- package/dist/cache.d.ts +1 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +6 -2
- package/dist/cache.js.map +1 -0
- package/dist/fetch.d.ts +1 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +1 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +5 -48
- package/dist/index.test.js.map +1 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +27 -7
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +1 -0
- package/dist/util.js.map +1 -0
- package/package.json +22 -3
- package/src/__snapshots__/index.test.ts.snap +56 -0
- package/src/cache.ts +82 -0
- package/src/fetch.ts +53 -0
- package/src/index.test.ts +70 -0
- package/src/index.ts +28 -0
- package/src/types.ts +99 -0
- package/src/util.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.1](https://github.com/shepherdjerred/webring/compare/v1.1.0...v1.1.1) (2024-07-07)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **deps:** update dependency astro to v4.11.5 ([a61f733](https://github.com/shepherdjerred/webring/commit/a61f733cbd3f7bd659d4a608f58473885a5bcc61))
|
|
14
|
+
* **deps:** update dependency remeda to v2.2.2 ([97a6c06](https://github.com/shepherdjerred/webring/commit/97a6c064138e0c204e231efb2ca45defd82b61f9))
|
|
15
|
+
* **deps:** update dependency remeda to v2.3.0 ([7a4c8d5](https://github.com/shepherdjerred/webring/commit/7a4c8d51ad5722be0a8dced10eb0b14a279b0bf6))
|
|
16
|
+
* update snapshot ([c73cdf7](https://github.com/shepherdjerred/webring/commit/c73cdf779433e6e7e8c5f2beee3fb2aefec9a0e0))
|
|
17
|
+
* use import instead of triple-slash ([971a77e](https://github.com/shepherdjerred/webring/commit/971a77ecd0c612850faeb9d16f7775d3e7ca7253))
|
|
18
|
+
|
|
19
|
+
## [1.1.0](https://github.com/shepherdjerred/webring/compare/v1.0.3...v1.1.0) (2024-06-28)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* source mappings ([095fe2b](https://github.com/shepherdjerred/webring/commit/095fe2be44e25547271730a5611d4710609cdf8d))
|
|
25
|
+
|
|
8
26
|
## [1.0.3](https://github.com/shepherdjerred/webring/compare/v1.0.2...v1.0.3) (2024-06-24)
|
|
9
27
|
|
|
10
28
|
|
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.rawgit.com/shepherdjerred/webring/main/assets/logo-dark.png">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://cdn.rawgit.com/shepherdjerred/webring/main/assets/logo-light.png">
|
|
5
|
+
<img alt="webring logo" src="https://cdn.rawgit.com/shepherdjerred/webring/main/assets/logo-light.png" height=150>
|
|
6
|
+
</picture>
|
|
2
7
|
|
|
3
8
|
[](https://www.npmjs.com/package/webring)
|
|
4
9
|
|
|
5
|
-
`webring`
|
|
10
|
+
`webring` fetches the latest updates from your favorite RSS feeds.
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
This project is actively maintained. If you have a feature request or need help, please [create an issue](https://github.com/shepherdjerred/webring/issues/new).
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
- https://git.sr.ht/~sircmpwn/openring
|
|
14
|
+
</div>
|
|
11
15
|
|
|
12
16
|
## Installation
|
|
13
17
|
|
|
@@ -15,13 +19,92 @@ Inspired by:
|
|
|
15
19
|
npm i webring
|
|
16
20
|
```
|
|
17
21
|
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Written in TypeScript
|
|
25
|
+
- Caching
|
|
26
|
+
- HTML sanitization and truncation
|
|
27
|
+
|
|
18
28
|
## Quick Start
|
|
19
29
|
|
|
20
|
-
This library is
|
|
30
|
+
This library is intended to be used with a static site generator. I use this with [Astro](https://astro.build/) on my [personal website](https://github.com/shepherdjerred/sjer.red/blob/1220ebef2e43956ba385402ed8529870e9084de8/src/components/BlogWebring.astro#L17-L22).
|
|
21
31
|
|
|
22
32
|
```typescript
|
|
23
33
|
import { run } from "webring";
|
|
24
|
-
|
|
34
|
+
|
|
35
|
+
const result = await run({
|
|
36
|
+
sources: [
|
|
37
|
+
{
|
|
38
|
+
url: "https://drewdevault.com/blog/index.xml",
|
|
39
|
+
title: "Drew DeVault",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
url: "https://danluu.com/atom.xml",
|
|
43
|
+
title: "Dan Luu",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
url: "https://jakelazaroff.com/rss.xml",
|
|
47
|
+
title: "Jake Lazaroff",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(result);
|
|
53
|
+
// [
|
|
54
|
+
// {
|
|
55
|
+
// title: 'A discussion of discussions on AI bias',
|
|
56
|
+
// url: 'https://danluu.com/ai-bias/',
|
|
57
|
+
// date: 2024-06-16T00:00:00.000Z,
|
|
58
|
+
// source: { url: 'https://danluu.com/atom.xml', title: 'Dan Luu' },
|
|
59
|
+
// preview: `There've been regular viral stories about ML/AI bias with LLMs and generative AI for the past couple years. One thing I find interesting about discussions of bias is how different the reaction is in the LLM and generative AI case when compared to "classical" bugs in cases where there's a clear bug. ...`
|
|
60
|
+
// },
|
|
61
|
+
// {
|
|
62
|
+
// title: 'Writing a Unix clone in about a month',
|
|
63
|
+
// url: 'https://drewdevault.com/2024/05/24/2024-05-24-Bunnix.html',
|
|
64
|
+
// date: 2024-05-24T00:00:00.000Z,
|
|
65
|
+
// source: {
|
|
66
|
+
// url: 'https://drewdevault.com/blog/index.xml',
|
|
67
|
+
// title: 'Drew DeVault'
|
|
68
|
+
// },
|
|
69
|
+
// preview: 'I needed a bit of a break from “real work” recently, so I started a new programming project that was low-stakes and purely recreational. On April 21st, I set out to see how much of a Unix-like operating system for x86_64 targets that I could put together in about a month. The result is Bunnix. Not i...'
|
|
70
|
+
// },
|
|
71
|
+
// {
|
|
72
|
+
// title: 'The Web Component Success Story',
|
|
73
|
+
// url: 'https://jakelazaroff.com/words/the-web-component-success-story/',
|
|
74
|
+
// date: 2024-01-29T00:00:00.000Z,
|
|
75
|
+
// source: { url: 'https://jakelazaroff.com/rss.xml', title: 'Jake Lazaroff' },
|
|
76
|
+
// preview: "Web components won't take web development by storm, or show us the One True Way to build websites. What they will do is let us collectively build a rich ecosystem of dynamic components that work with any web stack."
|
|
77
|
+
// }
|
|
78
|
+
// ]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
`webring` is configured by passing in a `Configuration` object into the `run` method.
|
|
84
|
+
|
|
85
|
+
All possible configuration values can be seen by looking at the [`typedoc` site](https://shepherdjerred.github.io/webring/types/Configuration.html).
|
|
86
|
+
|
|
87
|
+
## Example
|
|
88
|
+
|
|
89
|
+
An example of using this project with Astro is located in `example`. The relevant file is [`src/pages/blog/[...slug].astro`](https://github.com/shepherdjerred/webring/blob/971a77ecd0c612850faeb9d16f7775d3e7ca7253/example/src/pages/blog/%5B...slug%5D.astro#L18).
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
---
|
|
93
|
+
import { type CollectionEntry, getCollection } from "astro:content";
|
|
94
|
+
import BlogPost from "../../layouts/BlogPost.astro";
|
|
95
|
+
import { type Configuration, type Result, run } from "webring";
|
|
96
|
+
|
|
97
|
+
export async function getStaticPaths() {
|
|
98
|
+
const posts = await getCollection("blog");
|
|
99
|
+
return posts.map((post) => ({
|
|
100
|
+
params: { slug: post.slug },
|
|
101
|
+
props: post,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
type Props = CollectionEntry<"blog">;
|
|
105
|
+
|
|
106
|
+
const post = Astro.props;
|
|
107
|
+
const { Content } = await post.render();
|
|
25
108
|
|
|
26
109
|
export const config: Configuration = {
|
|
27
110
|
sources: [
|
|
@@ -38,39 +121,33 @@ export const config: Configuration = {
|
|
|
38
121
|
title: "Jake Lazaroff",
|
|
39
122
|
},
|
|
40
123
|
],
|
|
41
|
-
// the output will return the three most recent posts from the above sources
|
|
42
124
|
number: 3,
|
|
43
|
-
// the output will return santized HTML truncated to 300 characters
|
|
44
125
|
truncate: 300,
|
|
45
|
-
// if this is defined, we'll cache the results
|
|
46
126
|
cache: {
|
|
47
|
-
// the file to use as a cache
|
|
48
127
|
cache_file: "webring.json",
|
|
49
|
-
// how long the cache should remain valid for
|
|
50
128
|
cache_duration_minutes: 60,
|
|
51
129
|
},
|
|
52
130
|
};
|
|
53
131
|
|
|
54
|
-
// type Result = {
|
|
55
|
-
// title: string,
|
|
56
|
-
// url: string,
|
|
57
|
-
// date: Date,
|
|
58
|
-
// source: {
|
|
59
|
-
// url: string,
|
|
60
|
-
// title: string,
|
|
61
|
-
// },
|
|
62
|
-
// // this will be undefined if the RSS feed is empty
|
|
63
|
-
// preview?: string
|
|
64
|
-
// }[]
|
|
65
132
|
export const result: Result = await run(config);
|
|
133
|
+
---
|
|
66
134
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
135
|
+
<BlogPost {...post.data}>
|
|
136
|
+
<Content />
|
|
137
|
+
<h2>Posts from blogs I read</h2>
|
|
138
|
+
<ul>
|
|
139
|
+
{
|
|
140
|
+
result.map((post) => (
|
|
141
|
+
<li>
|
|
142
|
+
<a href={post.url}>{post.title}</a>
|
|
143
|
+
</li>
|
|
144
|
+
))
|
|
145
|
+
}
|
|
146
|
+
</ul>
|
|
147
|
+
</BlogPost>
|
|
71
148
|
```
|
|
72
149
|
|
|
73
|
-
|
|
150
|
+
## Inspiration
|
|
74
151
|
|
|
75
|
-
|
|
76
|
-
|
|
152
|
+
- https://github.com/lukehsiao/openring-rs
|
|
153
|
+
- https://git.sr.ht/~sircmpwn/openring
|
package/dist/cache.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { type Cache, type Result, type ResultEntry, type Source, type CachedConfiguration } from "./types.js";
|
|
2
2
|
export declare function fetchAllCached(config: CachedConfiguration): Promise<Result>;
|
|
3
3
|
export declare function fetchWithCache(source: Source, cache: Cache, config: CachedConfiguration): Promise<ResultEntry | undefined>;
|
|
4
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,KAAK,EACV,KAAK,MAAM,EACX,KAAK,WAAW,EAEhB,KAAK,MAAM,EACX,KAAK,mBAAmB,EAGzB,MAAM,YAAY,CAAC;AAyCpB,wBAAsB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAQjF;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAgBlC"}
|
package/dist/cache.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as R from "remeda";
|
|
2
2
|
import { CacheSchema, } from "./types.js";
|
|
3
3
|
import { fetch } from "./fetch.js";
|
|
4
|
-
import fs from "fs/promises";
|
|
5
4
|
import { asyncMapFilterUndefined } from "./util.js";
|
|
5
|
+
import fs from "fs/promises";
|
|
6
6
|
async function loadCache({ cache_file }) {
|
|
7
7
|
try {
|
|
8
8
|
await fs.access(cache_file);
|
|
@@ -14,7 +14,10 @@ async function loadCache({ cache_file }) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
async function saveCache({ cache_file }, cache) {
|
|
17
|
-
|
|
17
|
+
const dir = cache_file.split("/").slice(0, -1).join("/");
|
|
18
|
+
if (dir !== "") {
|
|
19
|
+
await fs.mkdir(cache_file.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
20
|
+
}
|
|
18
21
|
await fs.writeFile(cache_file, JSON.stringify(cache));
|
|
19
22
|
}
|
|
20
23
|
function toCacheEntry(result, now) {
|
|
@@ -52,3 +55,4 @@ export async function fetchWithCache(source, cache, config) {
|
|
|
52
55
|
console.log(`Fetching ${source.url}`);
|
|
53
56
|
return fetch(source, config.truncate);
|
|
54
57
|
}
|
|
58
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
package/dist/fetch.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAmB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAOhG,wBAAsB,QAAQ,CAAC,MAAM,EAAE,aAAa;;;;;;;;;KAEnD;AAED,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsC5F"}
|
package/dist/fetch.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,YAAY,CAAC;AAChC,OAAO,YAAY,MAAM,eAAe,CAAC;AACzC,OAAO,QAAQ,MAAM,eAAe,CAAC;AACrC,OAAO,EAAiC,eAAe,EAAsB,MAAM,YAAY,CAAC;AAChG,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAEpD,sFAAsF;AACtF,MAAM,UAAU,GAA4B,QAA8C,CAAC;AAE3F,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAqB;IAClD,OAAO,MAAM,uBAAuB,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,MAAc,EAAE,MAAc;IACxD,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAE/C,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,CACtB,IAAI,CAAC,KAAK,EACV,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAC5C,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,EACjD,CAAC,CAAC,OAAO,EAAE,EACX,CAAC,CAAC,KAAK,EAAE,CACV,CAAC;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC5C,CAAC;QAED,MAAM,OAAO,GACX,SAAS,CAAC,cAAc,IAAI,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,WAAW,IAAI,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAEzG,OAAO;YACL,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,GAAG,EAAE,SAAS,CAAC,IAAI;YACnB,IAAI,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;YAC9B,MAAM;YACN,OAAO,EAAE,OAAO;gBACd,CAAC,CAAC,UAAU,CACR,YAAY,CAAC,OAAO,EAAE;oBACpB,oBAAoB,EAAE,KAAK;iBAC5B,CAAC,EACF,MAAM,CACP;gBACH,CAAC,CAAC,SAAS;SACd,CAAC;IACJ,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,kBAAkB,MAAM,CAAC,GAAG,KAAK,CAAW,EAAE,CAAC,CAAC;QAC9D,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAA6B,MAAM,YAAY,CAAC;AAExF,wBAAsB,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAoBhE;AAED,cAAc,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,cAAc,IAAI,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAmC,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,MAAqB;IAC7C,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,yBAAyB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAEtE,IAAI,MAAc,CAAC;IACnB,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC;QACxD,MAAM,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC/B,MAAM,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CACvB,MAAM,EACN,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAC3C,CAAC,CAAC,OAAO,EAAE,EACX,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CACtB,CAAC;IAEF,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,cAAc,YAAY,CAAC"}
|
package/dist/index.test.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
|
package/dist/index.test.js
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from "os";
|
|
|
4
4
|
import { mkdtemp } from "fs/promises";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
// TODO: intercept network requests
|
|
7
|
-
test("it should fetch an RSS feed without caching", async () => {
|
|
7
|
+
test("it should fetch an RSS feed without caching", { timeout: 30000 }, async () => {
|
|
8
8
|
const config = {
|
|
9
9
|
sources: [
|
|
10
10
|
{
|
|
@@ -18,7 +18,7 @@ test("it should fetch an RSS feed without caching", async () => {
|
|
|
18
18
|
const result = await run(config);
|
|
19
19
|
expect(result).toMatchSnapshot();
|
|
20
20
|
});
|
|
21
|
-
test("it should fetch several RSS feeds", async () => {
|
|
21
|
+
test("it should fetch several RSS feeds", { timeout: 30000 }, async () => {
|
|
22
22
|
const config = {
|
|
23
23
|
sources: [
|
|
24
24
|
{
|
|
@@ -29,58 +29,14 @@ test("it should fetch several RSS feeds", async () => {
|
|
|
29
29
|
url: "https://danluu.com/atom.xml",
|
|
30
30
|
title: "Dan Luu",
|
|
31
31
|
},
|
|
32
|
-
{
|
|
33
|
-
url: "https://jakelazaroff.com/rss.xml",
|
|
34
|
-
title: "Jake Lazaroff",
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
url: "https://awesomekling.github.io/feed.xml",
|
|
38
|
-
title: "Andreas Kling",
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
url: "https://xeiaso.net/blog.rss",
|
|
42
|
-
title: "Xe Iaso",
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
url: "https://ciechanow.ski/atom.xml",
|
|
46
|
-
title: "Bartosz Ciechanowski",
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
url: "https://explained-from-first-principles.com/feed.xml",
|
|
50
|
-
title: "Explained From First Principles",
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
url: "http://www.aaronsw.com/2002/feeds/pgessays.rss",
|
|
54
|
-
title: "Paul Graham",
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
url: "https://samwho.dev/rss.xml",
|
|
58
|
-
title: "Sam Rose",
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
url: "https://rachelbythebay.com/w/atom.xml",
|
|
62
|
-
title: "Rachel Kroll",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
url: "https://brr.fyi/feed.xml",
|
|
66
|
-
title: "brr.fyi",
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
url: "https://devblogs.microsoft.com/oldnewthing/feed",
|
|
70
|
-
title: "The Old New Thing",
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
url: "https://ludic.mataroa.blog/rss/",
|
|
74
|
-
title: "Ludicity",
|
|
75
|
-
},
|
|
76
32
|
],
|
|
77
|
-
number:
|
|
33
|
+
number: 3,
|
|
78
34
|
truncate: 300,
|
|
79
35
|
};
|
|
80
36
|
const result = await run(config);
|
|
81
37
|
expect(result).toMatchSnapshot();
|
|
82
38
|
});
|
|
83
|
-
test("it should fetch an RSS feed with caching", async () => {
|
|
39
|
+
test("it should fetch an RSS feed with caching", { timeout: 30000 }, async () => {
|
|
84
40
|
const config = {
|
|
85
41
|
sources: [
|
|
86
42
|
{
|
|
@@ -104,3 +60,4 @@ async function createTempDir() {
|
|
|
104
60
|
const dir = join(ostmpdir, "unit-test-");
|
|
105
61
|
return await mkdtemp(dir);
|
|
106
62
|
}
|
|
63
|
+
//# sourceMappingURL=index.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,mCAAmC;AACnC,IAAI,CAAC,6CAA6C,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;IACjF,MAAM,MAAM,GAAkB;QAC5B,OAAO,EAAE;YACP;gBACE,KAAK,EAAE,iBAAiB;gBACxB,GAAG,EAAE,0BAA0B;aAChC;SACF;QACD,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,GAAG;KACd,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,EAAE,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mCAAmC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;IACvE,MAAM,MAAM,GAAkB;QAC5B,OAAO,EAAE;YACP;gBACE,GAAG,EAAE,wCAAwC;gBAC7C,KAAK,EAAE,cAAc;aACtB;YACD;gBACE,GAAG,EAAE,6BAA6B;gBAClC,KAAK,EAAE,SAAS;aACjB;SACF;QACD,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,GAAG;KACd,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,EAAE,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0CAA0C,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,EAAE;IAC9E,MAAM,MAAM,GAAkB;QAC5B,OAAO,EAAE;YACP;gBACE,KAAK,EAAE,iBAAiB;gBACxB,GAAG,EAAE,0BAA0B;aAChC;SACF;QACD,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,GAAG;QACb,KAAK,EAAE;YACL,UAAU,EAAE,GAAG,MAAM,aAAa,EAAE,aAAa;YACjD,sBAAsB,EAAE,CAAC;SAC1B;KACF,CAAC;IAEF,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,EAAE,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,oDAAoD;AACpD,KAAK,UAAU,aAAa;IAC1B,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC;IAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACzC,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export type Source = z.infer<typeof SourceSchema>;
|
|
3
|
+
/** An RSS source */
|
|
3
4
|
declare const SourceSchema: z.ZodObject<{
|
|
5
|
+
/** The URL of an RSS feed */
|
|
4
6
|
url: z.ZodString;
|
|
7
|
+
/** A title to describe the feed */
|
|
5
8
|
title: z.ZodString;
|
|
6
9
|
}, "strip", z.ZodTypeAny, {
|
|
7
10
|
url: string;
|
|
@@ -11,8 +14,11 @@ declare const SourceSchema: z.ZodObject<{
|
|
|
11
14
|
title: string;
|
|
12
15
|
}>;
|
|
13
16
|
export type CacheConfiguration = z.infer<typeof CacheConfigurationSchema>;
|
|
17
|
+
/** Configuration for the cache */
|
|
14
18
|
declare const CacheConfigurationSchema: z.ZodObject<{
|
|
19
|
+
/** How long to cache a result for */
|
|
15
20
|
cache_duration_minutes: z.ZodDefault<z.ZodNumber>;
|
|
21
|
+
/** The location of a file to use as a cache */
|
|
16
22
|
cache_file: z.ZodDefault<z.ZodString>;
|
|
17
23
|
}, "strip", z.ZodTypeAny, {
|
|
18
24
|
cache_duration_minutes: number;
|
|
@@ -22,9 +28,13 @@ declare const CacheConfigurationSchema: z.ZodObject<{
|
|
|
22
28
|
cache_file?: string | undefined;
|
|
23
29
|
}>;
|
|
24
30
|
export type Configuration = z.infer<typeof ConfigurationSchema>;
|
|
31
|
+
/** A configuration object with caching possibly configured */
|
|
25
32
|
declare const ConfigurationSchema: z.ZodObject<{
|
|
33
|
+
/** A list of sources to fetch */
|
|
26
34
|
sources: z.ZodArray<z.ZodObject<{
|
|
35
|
+
/** The URL of an RSS feed */
|
|
27
36
|
url: z.ZodString;
|
|
37
|
+
/** A title to describe the feed */
|
|
28
38
|
title: z.ZodString;
|
|
29
39
|
}, "strip", z.ZodTypeAny, {
|
|
30
40
|
url: string;
|
|
@@ -33,10 +43,15 @@ declare const ConfigurationSchema: z.ZodObject<{
|
|
|
33
43
|
url: string;
|
|
34
44
|
title: string;
|
|
35
45
|
}>, "many">;
|
|
46
|
+
/** Return the n latest updates from the source list. */
|
|
36
47
|
number: z.ZodDefault<z.ZodNumber>;
|
|
48
|
+
/** How many words the preview field should be truncated to in characters after HTML has been sanitized and parsed. */
|
|
37
49
|
truncate: z.ZodDefault<z.ZodNumber>;
|
|
50
|
+
/** Configuration for the cache */
|
|
38
51
|
cache: z.ZodOptional<z.ZodObject<{
|
|
52
|
+
/** How long to cache a result for */
|
|
39
53
|
cache_duration_minutes: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
/** The location of a file to use as a cache */
|
|
40
55
|
cache_file: z.ZodDefault<z.ZodString>;
|
|
41
56
|
}, "strip", z.ZodTypeAny, {
|
|
42
57
|
cache_duration_minutes: number;
|
|
@@ -69,9 +84,13 @@ declare const ConfigurationSchema: z.ZodObject<{
|
|
|
69
84
|
} | undefined;
|
|
70
85
|
}>;
|
|
71
86
|
export type CachedConfiguration = z.infer<typeof CachedConfigurationSchema>;
|
|
87
|
+
/** A configuration object with caching definitely configured */
|
|
72
88
|
export declare const CachedConfigurationSchema: z.ZodObject<z.objectUtil.extendShape<{
|
|
89
|
+
/** A list of sources to fetch */
|
|
73
90
|
sources: z.ZodArray<z.ZodObject<{
|
|
91
|
+
/** The URL of an RSS feed */
|
|
74
92
|
url: z.ZodString;
|
|
93
|
+
/** A title to describe the feed */
|
|
75
94
|
title: z.ZodString;
|
|
76
95
|
}, "strip", z.ZodTypeAny, {
|
|
77
96
|
url: string;
|
|
@@ -80,10 +99,15 @@ export declare const CachedConfigurationSchema: z.ZodObject<z.objectUtil.extendS
|
|
|
80
99
|
url: string;
|
|
81
100
|
title: string;
|
|
82
101
|
}>, "many">;
|
|
102
|
+
/** Return the n latest updates from the source list. */
|
|
83
103
|
number: z.ZodDefault<z.ZodNumber>;
|
|
104
|
+
/** How many words the preview field should be truncated to in characters after HTML has been sanitized and parsed. */
|
|
84
105
|
truncate: z.ZodDefault<z.ZodNumber>;
|
|
106
|
+
/** Configuration for the cache */
|
|
85
107
|
cache: z.ZodOptional<z.ZodObject<{
|
|
108
|
+
/** How long to cache a result for */
|
|
86
109
|
cache_duration_minutes: z.ZodDefault<z.ZodNumber>;
|
|
110
|
+
/** The location of a file to use as a cache */
|
|
87
111
|
cache_file: z.ZodDefault<z.ZodString>;
|
|
88
112
|
}, "strip", z.ZodTypeAny, {
|
|
89
113
|
cache_duration_minutes: number;
|
|
@@ -93,8 +117,11 @@ export declare const CachedConfigurationSchema: z.ZodObject<z.objectUtil.extendS
|
|
|
93
117
|
cache_file?: string | undefined;
|
|
94
118
|
}>>;
|
|
95
119
|
}, {
|
|
120
|
+
/** Configuration for the cache */
|
|
96
121
|
cache: z.ZodObject<{
|
|
122
|
+
/** How long to cache a result for */
|
|
97
123
|
cache_duration_minutes: z.ZodDefault<z.ZodNumber>;
|
|
124
|
+
/** The location of a file to use as a cache */
|
|
98
125
|
cache_file: z.ZodDefault<z.ZodString>;
|
|
99
126
|
}, "strip", z.ZodTypeAny, {
|
|
100
127
|
cache_duration_minutes: number;
|
|
@@ -127,12 +154,19 @@ export declare const CachedConfigurationSchema: z.ZodObject<z.objectUtil.extendS
|
|
|
127
154
|
truncate?: number | undefined;
|
|
128
155
|
}>;
|
|
129
156
|
export type ResultEntry = z.infer<typeof ResultEntrySchema>;
|
|
157
|
+
/** A single entry from an RSS feed */
|
|
130
158
|
declare const ResultEntrySchema: z.ZodObject<{
|
|
159
|
+
/** The title of the entry */
|
|
131
160
|
title: z.ZodString;
|
|
161
|
+
/** A direct link to the entry */
|
|
132
162
|
url: z.ZodString;
|
|
163
|
+
/** The date of the entry */
|
|
133
164
|
date: z.ZodDate;
|
|
165
|
+
/** The source the entry is from */
|
|
134
166
|
source: z.ZodObject<{
|
|
167
|
+
/** The URL of an RSS feed */
|
|
135
168
|
url: z.ZodString;
|
|
169
|
+
/** A title to describe the feed */
|
|
136
170
|
title: z.ZodString;
|
|
137
171
|
}, "strip", z.ZodTypeAny, {
|
|
138
172
|
url: string;
|
|
@@ -141,6 +175,7 @@ declare const ResultEntrySchema: z.ZodObject<{
|
|
|
141
175
|
url: string;
|
|
142
176
|
title: string;
|
|
143
177
|
}>;
|
|
178
|
+
/** A preview of the entry. This may contain sanitized HTML. */
|
|
144
179
|
preview: z.ZodOptional<z.ZodString>;
|
|
145
180
|
}, "strip", z.ZodTypeAny, {
|
|
146
181
|
url: string;
|
|
@@ -162,12 +197,19 @@ declare const ResultEntrySchema: z.ZodObject<{
|
|
|
162
197
|
preview?: string | undefined;
|
|
163
198
|
}>;
|
|
164
199
|
export type Result = z.infer<typeof ResultSchema>;
|
|
200
|
+
/** A list of results */
|
|
165
201
|
declare const ResultSchema: z.ZodArray<z.ZodObject<{
|
|
202
|
+
/** The title of the entry */
|
|
166
203
|
title: z.ZodString;
|
|
204
|
+
/** A direct link to the entry */
|
|
167
205
|
url: z.ZodString;
|
|
206
|
+
/** The date of the entry */
|
|
168
207
|
date: z.ZodDate;
|
|
208
|
+
/** The source the entry is from */
|
|
169
209
|
source: z.ZodObject<{
|
|
210
|
+
/** The URL of an RSS feed */
|
|
170
211
|
url: z.ZodString;
|
|
212
|
+
/** A title to describe the feed */
|
|
171
213
|
title: z.ZodString;
|
|
172
214
|
}, "strip", z.ZodTypeAny, {
|
|
173
215
|
url: string;
|
|
@@ -176,6 +218,7 @@ declare const ResultSchema: z.ZodArray<z.ZodObject<{
|
|
|
176
218
|
url: string;
|
|
177
219
|
title: string;
|
|
178
220
|
}>;
|
|
221
|
+
/** A preview of the entry. This may contain sanitized HTML. */
|
|
179
222
|
preview: z.ZodOptional<z.ZodString>;
|
|
180
223
|
}, "strip", z.ZodTypeAny, {
|
|
181
224
|
url: string;
|
|
@@ -197,14 +240,23 @@ declare const ResultSchema: z.ZodArray<z.ZodObject<{
|
|
|
197
240
|
preview?: string | undefined;
|
|
198
241
|
}>, "many">;
|
|
199
242
|
export type CacheEntry = z.infer<typeof CacheEntrySchema>;
|
|
243
|
+
/** A single cache entry */
|
|
200
244
|
export declare const CacheEntrySchema: z.ZodObject<{
|
|
245
|
+
/** The time a source was last checked */
|
|
201
246
|
timestamp: z.ZodDate;
|
|
247
|
+
/** The data from the source */
|
|
202
248
|
data: z.ZodObject<{
|
|
249
|
+
/** The title of the entry */
|
|
203
250
|
title: z.ZodString;
|
|
251
|
+
/** A direct link to the entry */
|
|
204
252
|
url: z.ZodString;
|
|
253
|
+
/** The date of the entry */
|
|
205
254
|
date: z.ZodDate;
|
|
255
|
+
/** The source the entry is from */
|
|
206
256
|
source: z.ZodObject<{
|
|
257
|
+
/** The URL of an RSS feed */
|
|
207
258
|
url: z.ZodString;
|
|
259
|
+
/** A title to describe the feed */
|
|
208
260
|
title: z.ZodString;
|
|
209
261
|
}, "strip", z.ZodTypeAny, {
|
|
210
262
|
url: string;
|
|
@@ -213,6 +265,7 @@ export declare const CacheEntrySchema: z.ZodObject<{
|
|
|
213
265
|
url: string;
|
|
214
266
|
title: string;
|
|
215
267
|
}>;
|
|
268
|
+
/** A preview of the entry. This may contain sanitized HTML. */
|
|
216
269
|
preview: z.ZodOptional<z.ZodString>;
|
|
217
270
|
}, "strip", z.ZodTypeAny, {
|
|
218
271
|
url: string;
|
|
@@ -259,14 +312,23 @@ export declare const CacheEntrySchema: z.ZodObject<{
|
|
|
259
312
|
};
|
|
260
313
|
}>;
|
|
261
314
|
export type Cache = z.infer<typeof CacheSchema>;
|
|
315
|
+
/** A mapping of source URLs to cache entries */
|
|
262
316
|
export declare const CacheSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
317
|
+
/** The time a source was last checked */
|
|
263
318
|
timestamp: z.ZodDate;
|
|
319
|
+
/** The data from the source */
|
|
264
320
|
data: z.ZodObject<{
|
|
321
|
+
/** The title of the entry */
|
|
265
322
|
title: z.ZodString;
|
|
323
|
+
/** A direct link to the entry */
|
|
266
324
|
url: z.ZodString;
|
|
325
|
+
/** The date of the entry */
|
|
267
326
|
date: z.ZodDate;
|
|
327
|
+
/** The source the entry is from */
|
|
268
328
|
source: z.ZodObject<{
|
|
329
|
+
/** The URL of an RSS feed */
|
|
269
330
|
url: z.ZodString;
|
|
331
|
+
/** A title to describe the feed */
|
|
270
332
|
title: z.ZodString;
|
|
271
333
|
}, "strip", z.ZodTypeAny, {
|
|
272
334
|
url: string;
|
|
@@ -275,6 +337,7 @@ export declare const CacheSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
|
275
337
|
url: string;
|
|
276
338
|
title: string;
|
|
277
339
|
}>;
|
|
340
|
+
/** A preview of the entry. This may contain sanitized HTML. */
|
|
278
341
|
preview: z.ZodOptional<z.ZodString>;
|
|
279
342
|
}, "strip", z.ZodTypeAny, {
|
|
280
343
|
url: string;
|
|
@@ -320,6 +383,7 @@ export declare const CacheSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
|
320
383
|
preview?: string | undefined;
|
|
321
384
|
};
|
|
322
385
|
}>>;
|
|
386
|
+
/** The expected format fetched RSS feed entries */
|
|
323
387
|
export declare const FeedEntrySchema: z.ZodEffects<z.ZodObject<{
|
|
324
388
|
title: z.ZodString;
|
|
325
389
|
link: z.ZodString;
|
|
@@ -366,3 +430,4 @@ export declare const FeedEntrySchema: z.ZodEffects<z.ZodObject<{
|
|
|
366
430
|
description?: string | undefined;
|
|
367
431
|
}>;
|
|
368
432
|
export {};
|
|
433
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -1,40 +1,59 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** An RSS source */
|
|
2
3
|
const SourceSchema = z.object({
|
|
3
|
-
|
|
4
|
+
/** The URL of an RSS feed */
|
|
4
5
|
url: z.string(),
|
|
5
|
-
|
|
6
|
-
title: z.string(),
|
|
6
|
+
/** A title to describe the feed */
|
|
7
|
+
title: z.string().describe("A title for the feed"),
|
|
7
8
|
});
|
|
9
|
+
/** Configuration for the cache */
|
|
8
10
|
const CacheConfigurationSchema = z.object({
|
|
9
|
-
|
|
11
|
+
/** How long to cache a result for */
|
|
10
12
|
cache_duration_minutes: z.number().default(60),
|
|
13
|
+
/** The location of a file to use as a cache */
|
|
11
14
|
cache_file: z.string().default("cache.json"),
|
|
12
15
|
});
|
|
16
|
+
/** A configuration object with caching possibly configured */
|
|
13
17
|
const ConfigurationSchema = z.object({
|
|
14
|
-
|
|
18
|
+
/** A list of sources to fetch */
|
|
15
19
|
sources: SourceSchema.array(),
|
|
16
|
-
|
|
20
|
+
/** Return the n latest updates from the source list. */
|
|
17
21
|
number: z.number().default(3),
|
|
18
|
-
|
|
22
|
+
/** How many words the preview field should be truncated to in characters after HTML has been sanitized and parsed. */
|
|
19
23
|
truncate: z.number().default(300),
|
|
24
|
+
/** Configuration for the cache */
|
|
20
25
|
cache: CacheConfigurationSchema.optional(),
|
|
21
26
|
});
|
|
27
|
+
/** A configuration object with caching definitely configured */
|
|
22
28
|
export const CachedConfigurationSchema = ConfigurationSchema.extend({
|
|
29
|
+
/** Configuration for the cache */
|
|
23
30
|
cache: CacheConfigurationSchema,
|
|
24
31
|
});
|
|
32
|
+
/** A single entry from an RSS feed */
|
|
25
33
|
const ResultEntrySchema = z.object({
|
|
34
|
+
/** The title of the entry */
|
|
26
35
|
title: z.string(),
|
|
36
|
+
/** A direct link to the entry */
|
|
27
37
|
url: z.string(),
|
|
38
|
+
/** The date of the entry */
|
|
28
39
|
date: z.coerce.date(),
|
|
40
|
+
/** The source the entry is from */
|
|
29
41
|
source: SourceSchema,
|
|
42
|
+
/** A preview of the entry. This may contain sanitized HTML. */
|
|
30
43
|
preview: z.string().optional(),
|
|
31
44
|
});
|
|
45
|
+
/** A list of results */
|
|
32
46
|
const ResultSchema = z.array(ResultEntrySchema);
|
|
47
|
+
/** A single cache entry */
|
|
33
48
|
export const CacheEntrySchema = z.object({
|
|
49
|
+
/** The time a source was last checked */
|
|
34
50
|
timestamp: z.coerce.date(),
|
|
51
|
+
/** The data from the source */
|
|
35
52
|
data: ResultEntrySchema,
|
|
36
53
|
});
|
|
54
|
+
/** A mapping of source URLs to cache entries */
|
|
37
55
|
export const CacheSchema = z.record(CacheEntrySchema);
|
|
56
|
+
/** The expected format fetched RSS feed entries */
|
|
38
57
|
export const FeedEntrySchema = z
|
|
39
58
|
.object({
|
|
40
59
|
title: z.string(),
|
|
@@ -61,3 +80,4 @@ export const FeedEntrySchema = z
|
|
|
61
80
|
"content:encoded": entry["content:encoded"],
|
|
62
81
|
};
|
|
63
82
|
});
|
|
83
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,oBAAoB;AACpB,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,6BAA6B;IAC7B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,mCAAmC;IACnC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;CACnD,CAAC,CAAC;AAGH,kCAAkC;AAClC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,qCAAqC;IACrC,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9C,+CAA+C;IAC/C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;CAC7C,CAAC,CAAC;AAGH,8DAA8D;AAC9D,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,iCAAiC;IACjC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE;IAC7B,wDAAwD;IACxD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7B,sHAAsH;IACtH,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;IACjC,kCAAkC;IAClC,KAAK,EAAE,wBAAwB,CAAC,QAAQ,EAAE;CAC3C,CAAC,CAAC;AAGH,gEAAgE;AAChE,MAAM,CAAC,MAAM,yBAAyB,GAAG,mBAAmB,CAAC,MAAM,CAAC;IAClE,kCAAkC;IAClC,KAAK,EAAE,wBAAwB;CAChC,CAAC,CAAC;AAGH,sCAAsC;AACtC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,6BAA6B;IAC7B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,iCAAiC;IACjC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,4BAA4B;IAC5B,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE;IACrB,mCAAmC;IACnC,MAAM,EAAE,YAAY;IACpB,+DAA+D;IAC/D,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAC;AAGH,wBAAwB;AACxB,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;AAGhD,2BAA2B;AAC3B,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,yCAAyC;IACzC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE;IAC1B,+BAA+B;IAC/B,IAAI,EAAE,iBAAiB;CACxB,CAAC,CAAC;AAGH,gDAAgD;AAChD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEtD,mDAAmD;AACnD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IACnC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IACnC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACxC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC;KACD,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;IACnB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC;IAC5C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI;QACJ,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,CAAC;KAC5C,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
package/dist/util.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function asyncMapFilterUndefined<T, U>(input: T[], fn: (x: T) => Promise<U | undefined>): Promise<U[]>;
|
|
2
2
|
export declare function asyncMap<T, U>(input: T[], fn: (x: T) => Promise<U>): Promise<U[]>;
|
|
3
3
|
export declare function filterUndefined<T>(input: (T | undefined)[]): T[];
|
|
4
|
+
//# sourceMappingURL=util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAGA,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAGlH;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAOvF;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAKhE"}
|
package/dist/util.js
CHANGED
package/dist/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAE5B,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAO,KAAU,EAAE,EAAoC;IAClG,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1C,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAO,KAAU,EAAE,EAAwB;IACvE,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,CACrB,KAAK,EACL,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAC1B,CAAC;IAEF,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,KAAwB;IACzD,OAAO,CAAC,CAAC,IAAI,CACX,KAAK,EACL,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CACpC,CAAC;AACX,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webring",
|
|
3
|
+
"description": "Collect the latest RSS items from your favorite feeds.",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Jerred Shepherd",
|
|
6
|
+
"email": "npm@sjer.red",
|
|
7
|
+
"url": "https://sjer.red"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/shepherdjerred/webring",
|
|
10
|
+
"license": "GPL-3.0-only",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/shepherdjerred/webring.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/shepherdjerred/webring/issues"
|
|
17
|
+
},
|
|
3
18
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
19
|
+
"version": "1.1.1",
|
|
5
20
|
"scripts": {
|
|
6
21
|
"prepare": "husky",
|
|
7
22
|
"lint": "eslint src",
|
|
8
23
|
"build": "tsc",
|
|
9
|
-
"
|
|
24
|
+
"watch": "tsc -w",
|
|
25
|
+
"test": "vitest --disable-console-intercept",
|
|
26
|
+
"typedoc": "typedoc src/index.ts"
|
|
10
27
|
},
|
|
11
28
|
"main": "dist/index.js",
|
|
12
29
|
"types": "dist/index.d.ts",
|
|
@@ -23,7 +40,6 @@
|
|
|
23
40
|
"@eslint/js": "^9.4.0",
|
|
24
41
|
"@tsconfig/node20": "^20.1.4",
|
|
25
42
|
"@tsconfig/strictest": "^2.0.5",
|
|
26
|
-
"@types/eslint__js": "^8.42.3",
|
|
27
43
|
"@types/node": "^20.14.0",
|
|
28
44
|
"@types/sanitize-html": "^2.11.0",
|
|
29
45
|
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
|
@@ -32,6 +48,8 @@
|
|
|
32
48
|
"husky": "^9.0.11",
|
|
33
49
|
"lint-staged": "^15.2.5",
|
|
34
50
|
"prettier": "^3.3.0",
|
|
51
|
+
"typedoc": "^0.26.3",
|
|
52
|
+
"typedoc-plugin-zod": "^1.2.0",
|
|
35
53
|
"typescript": "^5.4.5",
|
|
36
54
|
"typescript-eslint": "^7.11.0",
|
|
37
55
|
"vitest": "^1.6.0"
|
|
@@ -50,6 +68,7 @@
|
|
|
50
68
|
},
|
|
51
69
|
"files": [
|
|
52
70
|
"dist",
|
|
71
|
+
"src",
|
|
53
72
|
"package.json",
|
|
54
73
|
"README.md",
|
|
55
74
|
"LICENSE",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`it should fetch an RSS feed with caching 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"date": 2024-07-01T00:00:00.000Z,
|
|
7
|
+
"preview": "TIL: Using Twoslash with Shiki and Astro",
|
|
8
|
+
"source": {
|
|
9
|
+
"title": "Jerred Shepherd",
|
|
10
|
+
"url": "https://sjer.red/rss.xml",
|
|
11
|
+
},
|
|
12
|
+
"title": "TIL: Using Twoslash with Shiki and Astro",
|
|
13
|
+
"url": "https://sjer.red/blog/til/2024-07-01/",
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
exports[`it should fetch an RSS feed without caching 1`] = `
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
"date": 2024-07-01T00:00:00.000Z,
|
|
22
|
+
"preview": "TIL: Using Twoslash with Shiki and Astro",
|
|
23
|
+
"source": {
|
|
24
|
+
"title": "Jerred Shepherd",
|
|
25
|
+
"url": "https://sjer.red/rss.xml",
|
|
26
|
+
},
|
|
27
|
+
"title": "TIL: Using Twoslash with Shiki and Astro",
|
|
28
|
+
"url": "https://sjer.red/blog/til/2024-07-01/",
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
exports[`it should fetch several RSS feeds 1`] = `
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
"date": 2024-06-16T00:00:00.000Z,
|
|
37
|
+
"preview": "There've been regular viral stories about ML/AI bias with LLMs and generative AI for the past couple years. One thing I find interesting about discussions of bias is how different the reaction is in the LLM and generative AI case when compared to "classical" bugs in cases where there's a clear bug. ...",
|
|
38
|
+
"source": {
|
|
39
|
+
"title": "Dan Luu",
|
|
40
|
+
"url": "https://danluu.com/atom.xml",
|
|
41
|
+
},
|
|
42
|
+
"title": "A discussion of discussions on AI bias",
|
|
43
|
+
"url": "https://danluu.com/ai-bias/",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"date": 2024-05-24T00:00:00.000Z,
|
|
47
|
+
"preview": "I needed a bit of a break from “real work” recently, so I started a new programming project that was low-stakes and purely recreational. On April 21st, I set out to see how much of a Unix-like operating system for x86_64 targets that I could put together in about a month. The result is Bunnix. Not i...",
|
|
48
|
+
"source": {
|
|
49
|
+
"title": "Drew DeVault",
|
|
50
|
+
"url": "https://drewdevault.com/blog/index.xml",
|
|
51
|
+
},
|
|
52
|
+
"title": "Writing a Unix clone in about a month",
|
|
53
|
+
"url": "https://drewdevault.com/2024/05/24/2024-05-24-Bunnix.html",
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
`;
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as R from "remeda";
|
|
2
|
+
import {
|
|
3
|
+
type Cache,
|
|
4
|
+
type Result,
|
|
5
|
+
type ResultEntry,
|
|
6
|
+
type CacheEntry,
|
|
7
|
+
type Source,
|
|
8
|
+
type CachedConfiguration,
|
|
9
|
+
CacheSchema,
|
|
10
|
+
type CacheConfiguration,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
import { fetch } from "./fetch.js";
|
|
13
|
+
import { asyncMapFilterUndefined } from "./util.js";
|
|
14
|
+
import fs from "fs/promises";
|
|
15
|
+
|
|
16
|
+
async function loadCache({ cache_file }: CacheConfiguration): Promise<Cache> {
|
|
17
|
+
try {
|
|
18
|
+
await fs.access(cache_file);
|
|
19
|
+
const cacheFile = await fs.readFile(cache_file);
|
|
20
|
+
return CacheSchema.parse(JSON.parse(cacheFile.toString()));
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function saveCache({ cache_file }: CacheConfiguration, cache: Cache) {
|
|
27
|
+
const dir = cache_file.split("/").slice(0, -1).join("/");
|
|
28
|
+
if (dir !== "") {
|
|
29
|
+
await fs.mkdir(cache_file.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
await fs.writeFile(cache_file, JSON.stringify(cache));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toCacheEntry(result: ResultEntry, now: Date): [string, CacheEntry] {
|
|
35
|
+
return [result.source.url, { timestamp: now, data: result }];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toCache(results: ResultEntry[], now: Date): Cache {
|
|
39
|
+
return R.pipe(
|
|
40
|
+
results,
|
|
41
|
+
R.map((result) => toCacheEntry(result, now)),
|
|
42
|
+
R.fromEntries(),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function updateCache(results: ResultEntry[], config: CachedConfiguration): Promise<void> {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const updatedCache = toCache(results, now);
|
|
49
|
+
return saveCache(config.cache, updatedCache);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function fetchAllCached(config: CachedConfiguration): Promise<Result> {
|
|
53
|
+
const cache = await loadCache(config.cache);
|
|
54
|
+
|
|
55
|
+
const results = await asyncMapFilterUndefined(config.sources, (source) => fetchWithCache(source, cache, config));
|
|
56
|
+
|
|
57
|
+
await updateCache(results, config);
|
|
58
|
+
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchWithCache(
|
|
63
|
+
source: Source,
|
|
64
|
+
cache: Cache,
|
|
65
|
+
config: CachedConfiguration,
|
|
66
|
+
): Promise<ResultEntry | undefined> {
|
|
67
|
+
const cacheEntry = cache[source.url];
|
|
68
|
+
if (cacheEntry) {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
if (now.getTime() - cacheEntry.timestamp.getTime() < config.cache.cache_duration_minutes * 60 * 1000) {
|
|
71
|
+
console.log(`Cache entry found for ${source.url}.`);
|
|
72
|
+
return Promise.resolve(cacheEntry.data);
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`Cache entry for ${source.url} is too old.`);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`No cache entry for ${source.url}.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Fetching ${source.url}`);
|
|
81
|
+
return fetch(source, config.truncate);
|
|
82
|
+
}
|
package/src/fetch.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Parser from "rss-parser";
|
|
2
|
+
import sanitizeHtml from "sanitize-html";
|
|
3
|
+
import truncate from "truncate-html";
|
|
4
|
+
import { type Source, type ResultEntry, FeedEntrySchema, type Configuration } from "./types.js";
|
|
5
|
+
import * as R from "remeda";
|
|
6
|
+
import { asyncMapFilterUndefined } from "./util.js";
|
|
7
|
+
|
|
8
|
+
// for some reason, TypeScript does not infer the type of the default export correctly
|
|
9
|
+
const truncateFn: typeof truncate.default = truncate as unknown as typeof truncate.default;
|
|
10
|
+
|
|
11
|
+
export async function fetchAll(config: Configuration) {
|
|
12
|
+
return await asyncMapFilterUndefined(config.sources, (source) => fetch(source, config.truncate));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function fetch(source: Source, length: number): Promise<ResultEntry | undefined> {
|
|
16
|
+
const parser = new Parser();
|
|
17
|
+
try {
|
|
18
|
+
const feed = await parser.parseURL(source.url);
|
|
19
|
+
|
|
20
|
+
const firstItem = R.pipe(
|
|
21
|
+
feed.items,
|
|
22
|
+
R.map((item) => FeedEntrySchema.parse(item)),
|
|
23
|
+
R.sortBy((item) => new Date(item.date).getTime()),
|
|
24
|
+
R.reverse(),
|
|
25
|
+
R.first(),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (!firstItem) {
|
|
29
|
+
throw new Error("no items found in feed");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const preview =
|
|
33
|
+
firstItem.contentSnippet ?? firstItem.content ?? firstItem.description ?? firstItem["content:encoded"];
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
title: firstItem.title,
|
|
37
|
+
url: firstItem.link,
|
|
38
|
+
date: new Date(firstItem.date),
|
|
39
|
+
source,
|
|
40
|
+
preview: preview
|
|
41
|
+
? truncateFn(
|
|
42
|
+
sanitizeHtml(preview, {
|
|
43
|
+
parseStyleAttributes: false,
|
|
44
|
+
}),
|
|
45
|
+
length,
|
|
46
|
+
)
|
|
47
|
+
: undefined,
|
|
48
|
+
};
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(`Error fetching ${source.url}: ${e as string}`);
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import type { Configuration } from "./types.js";
|
|
3
|
+
import { run } from "./index.js";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { mkdtemp } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
8
|
+
// TODO: intercept network requests
|
|
9
|
+
test("it should fetch an RSS feed without caching", { timeout: 30000 }, async () => {
|
|
10
|
+
const config: Configuration = {
|
|
11
|
+
sources: [
|
|
12
|
+
{
|
|
13
|
+
title: "Jerred Shepherd",
|
|
14
|
+
url: "https://sjer.red/rss.xml",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
number: 1,
|
|
18
|
+
truncate: 300,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const result = await run(config);
|
|
22
|
+
expect(result).toMatchSnapshot();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("it should fetch several RSS feeds", { timeout: 30000 }, async () => {
|
|
26
|
+
const config: Configuration = {
|
|
27
|
+
sources: [
|
|
28
|
+
{
|
|
29
|
+
url: "https://drewdevault.com/blog/index.xml",
|
|
30
|
+
title: "Drew DeVault",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
url: "https://danluu.com/atom.xml",
|
|
34
|
+
title: "Dan Luu",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
number: 3,
|
|
38
|
+
truncate: 300,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = await run(config);
|
|
42
|
+
expect(result).toMatchSnapshot();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("it should fetch an RSS feed with caching", { timeout: 30000 }, async () => {
|
|
46
|
+
const config: Configuration = {
|
|
47
|
+
sources: [
|
|
48
|
+
{
|
|
49
|
+
title: "Jerred Shepherd",
|
|
50
|
+
url: "https://sjer.red/rss.xml",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
number: 1,
|
|
54
|
+
truncate: 300,
|
|
55
|
+
cache: {
|
|
56
|
+
cache_file: `${await createTempDir()}/cache.json`,
|
|
57
|
+
cache_duration_minutes: 1,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const result = await run(config);
|
|
62
|
+
expect(result).toMatchSnapshot();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// https://sdorra.dev/posts/2024-02-12-vitest-tmpdir
|
|
66
|
+
async function createTempDir() {
|
|
67
|
+
const ostmpdir = tmpdir();
|
|
68
|
+
const dir = join(ostmpdir, "unit-test-");
|
|
69
|
+
return await mkdtemp(dir);
|
|
70
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as R from "remeda";
|
|
2
|
+
import { fetchAllCached as fetchAllCached } from "./cache.js";
|
|
3
|
+
import { fetchAll as fetchAllUncached } from "./fetch.js";
|
|
4
|
+
import { type Configuration, type Result, CachedConfigurationSchema } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export async function run(config: Configuration): Promise<Result> {
|
|
7
|
+
const { success, data } = CachedConfigurationSchema.safeParse(config);
|
|
8
|
+
|
|
9
|
+
let result: Result;
|
|
10
|
+
if (success) {
|
|
11
|
+
console.log(`Using cache at ${data.cache.cache_file}.`);
|
|
12
|
+
result = await fetchAllCached(data);
|
|
13
|
+
} else {
|
|
14
|
+
console.log("Cache disabled.");
|
|
15
|
+
result = await fetchAllUncached(config);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const topResults = R.pipe(
|
|
19
|
+
result,
|
|
20
|
+
R.sortBy((result) => result.date.getTime()),
|
|
21
|
+
R.reverse(),
|
|
22
|
+
R.take(config.number),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return topResults;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export * from "./types.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export type Source = z.infer<typeof SourceSchema>;
|
|
4
|
+
/** An RSS source */
|
|
5
|
+
const SourceSchema = z.object({
|
|
6
|
+
/** The URL of an RSS feed */
|
|
7
|
+
url: z.string(),
|
|
8
|
+
/** A title to describe the feed */
|
|
9
|
+
title: z.string().describe("A title for the feed"),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type CacheConfiguration = z.infer<typeof CacheConfigurationSchema>;
|
|
13
|
+
/** Configuration for the cache */
|
|
14
|
+
const CacheConfigurationSchema = z.object({
|
|
15
|
+
/** How long to cache a result for */
|
|
16
|
+
cache_duration_minutes: z.number().default(60),
|
|
17
|
+
/** The location of a file to use as a cache */
|
|
18
|
+
cache_file: z.string().default("cache.json"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type Configuration = z.infer<typeof ConfigurationSchema>;
|
|
22
|
+
/** A configuration object with caching possibly configured */
|
|
23
|
+
const ConfigurationSchema = z.object({
|
|
24
|
+
/** A list of sources to fetch */
|
|
25
|
+
sources: SourceSchema.array(),
|
|
26
|
+
/** Return the n latest updates from the source list. */
|
|
27
|
+
number: z.number().default(3),
|
|
28
|
+
/** How many words the preview field should be truncated to in characters after HTML has been sanitized and parsed. */
|
|
29
|
+
truncate: z.number().default(300),
|
|
30
|
+
/** Configuration for the cache */
|
|
31
|
+
cache: CacheConfigurationSchema.optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type CachedConfiguration = z.infer<typeof CachedConfigurationSchema>;
|
|
35
|
+
/** A configuration object with caching definitely configured */
|
|
36
|
+
export const CachedConfigurationSchema = ConfigurationSchema.extend({
|
|
37
|
+
/** Configuration for the cache */
|
|
38
|
+
cache: CacheConfigurationSchema,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type ResultEntry = z.infer<typeof ResultEntrySchema>;
|
|
42
|
+
/** A single entry from an RSS feed */
|
|
43
|
+
const ResultEntrySchema = z.object({
|
|
44
|
+
/** The title of the entry */
|
|
45
|
+
title: z.string(),
|
|
46
|
+
/** A direct link to the entry */
|
|
47
|
+
url: z.string(),
|
|
48
|
+
/** The date of the entry */
|
|
49
|
+
date: z.coerce.date(),
|
|
50
|
+
/** The source the entry is from */
|
|
51
|
+
source: SourceSchema,
|
|
52
|
+
/** A preview of the entry. This may contain sanitized HTML. */
|
|
53
|
+
preview: z.string().optional(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type Result = z.infer<typeof ResultSchema>;
|
|
57
|
+
/** A list of results */
|
|
58
|
+
const ResultSchema = z.array(ResultEntrySchema);
|
|
59
|
+
|
|
60
|
+
export type CacheEntry = z.infer<typeof CacheEntrySchema>;
|
|
61
|
+
/** A single cache entry */
|
|
62
|
+
export const CacheEntrySchema = z.object({
|
|
63
|
+
/** The time a source was last checked */
|
|
64
|
+
timestamp: z.coerce.date(),
|
|
65
|
+
/** The data from the source */
|
|
66
|
+
data: ResultEntrySchema,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type Cache = z.infer<typeof CacheSchema>;
|
|
70
|
+
/** A mapping of source URLs to cache entries */
|
|
71
|
+
export const CacheSchema = z.record(CacheEntrySchema);
|
|
72
|
+
|
|
73
|
+
/** The expected format fetched RSS feed entries */
|
|
74
|
+
export const FeedEntrySchema = z
|
|
75
|
+
.object({
|
|
76
|
+
title: z.string(),
|
|
77
|
+
link: z.string(),
|
|
78
|
+
isoDate: z.coerce.date().optional(),
|
|
79
|
+
pubDate: z.coerce.date().optional(),
|
|
80
|
+
content: z.string().optional(),
|
|
81
|
+
contentSnippet: z.string().optional(),
|
|
82
|
+
"content:encoded": z.string().optional(),
|
|
83
|
+
description: z.string().optional(),
|
|
84
|
+
})
|
|
85
|
+
.transform((entry) => {
|
|
86
|
+
const date = entry.isoDate ?? entry.pubDate;
|
|
87
|
+
if (!date) {
|
|
88
|
+
throw new Error("no date found in feed entry");
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
title: entry.title,
|
|
92
|
+
link: entry.link,
|
|
93
|
+
date,
|
|
94
|
+
content: entry.content,
|
|
95
|
+
contentSnippet: entry.contentSnippet,
|
|
96
|
+
description: entry.description,
|
|
97
|
+
"content:encoded": entry["content:encoded"],
|
|
98
|
+
};
|
|
99
|
+
});
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as R from "remeda";
|
|
2
|
+
|
|
3
|
+
// run an async map operation, filtering out undefined results
|
|
4
|
+
export async function asyncMapFilterUndefined<T, U>(input: T[], fn: (x: T) => Promise<U | undefined>): Promise<U[]> {
|
|
5
|
+
const results = await asyncMap(input, fn);
|
|
6
|
+
return filterUndefined(results);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function asyncMap<T, U>(input: T[], fn: (x: T) => Promise<U>): Promise<U[]> {
|
|
10
|
+
const promises = R.pipe(
|
|
11
|
+
input,
|
|
12
|
+
R.map((item) => fn(item)),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return Promise.all(promises);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function filterUndefined<T>(input: (T | undefined)[]): T[] {
|
|
19
|
+
return R.pipe(
|
|
20
|
+
input,
|
|
21
|
+
R.filter((result) => result !== undefined),
|
|
22
|
+
) as T[];
|
|
23
|
+
}
|