webring 1.0.2 → 1.1.0
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 +14 -0
- package/README.md +31 -10
- package/dist/cache.d.ts +1 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +2 -0
- 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 +1 -0
- package/dist/index.test.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- 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 +2 -2
- package/src/__snapshots__/index.test.ts.snap +146 -0
- package/src/cache.ts +79 -0
- package/src/fetch.ts +53 -0
- package/src/index.test.ts +114 -0
- package/src/index.ts +28 -0
- package/src/types.ts +81 -0
- package/src/util.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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.0](https://github.com/shepherdjerred/webring/compare/v1.0.3...v1.1.0) (2024-06-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* source mappings ([095fe2b](https://github.com/shepherdjerred/webring/commit/095fe2be44e25547271730a5611d4710609cdf8d))
|
|
14
|
+
|
|
15
|
+
## [1.0.3](https://github.com/shepherdjerred/webring/compare/v1.0.2...v1.0.3) (2024-06-24)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* create dir to cache ([a30645c](https://github.com/shepherdjerred/webring/commit/a30645c11d2afe91f7802c91b2c82eef9a97c717))
|
|
21
|
+
|
|
8
22
|
## [1.0.2](https://github.com/shepherdjerred/webring/compare/v1.0.1...v1.0.2) (2024-06-21)
|
|
9
23
|
|
|
10
24
|
|
package/README.md
CHANGED
|
@@ -17,13 +17,13 @@ npm i webring
|
|
|
17
17
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
20
|
-
This library is meant to be used with static site generators. It is framework agnostic.
|
|
20
|
+
This library is meant to be used with static site generators. It is framework agnostic. I use this with [Astro](https://astro.build/) on my [personal website](https://github.com/shepherdjerred/sjer.red/blob/main/src/components/BlogWebring.astro#L17-L22).
|
|
21
21
|
|
|
22
22
|
```typescript
|
|
23
|
-
import {
|
|
23
|
+
import { run } from "webring";
|
|
24
|
+
import { type Configuration, type Result } from "webring";
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
const config: Configuration = {
|
|
26
|
+
export const config: Configuration = {
|
|
27
27
|
sources: [
|
|
28
28
|
{
|
|
29
29
|
url: "https://drewdevault.com/blog/index.xml",
|
|
@@ -38,18 +38,39 @@ const config: Configuration = {
|
|
|
38
38
|
title: "Jake Lazaroff",
|
|
39
39
|
},
|
|
40
40
|
],
|
|
41
|
+
// the output will return the three most recent posts from the above sources
|
|
41
42
|
number: 3,
|
|
42
|
-
|
|
43
|
+
// the output will return santized HTML truncated to 300 characters
|
|
43
44
|
truncate: 300,
|
|
45
|
+
// if this is defined, we'll cache the results
|
|
46
|
+
cache: {
|
|
47
|
+
// the file to use as a cache
|
|
48
|
+
cache_file: "webring.json",
|
|
49
|
+
// how long the cache should remain valid for
|
|
50
|
+
cache_duration_minutes: 60,
|
|
51
|
+
},
|
|
44
52
|
};
|
|
45
53
|
|
|
46
|
-
//
|
|
47
|
-
|
|
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
|
+
export const result: Result = await run(config);
|
|
48
66
|
|
|
49
|
-
// do something with the results
|
|
50
67
|
result.map((entry) => {
|
|
51
|
-
|
|
68
|
+
// do something with the results
|
|
69
|
+
// for example, you might render each item as an HTML block
|
|
52
70
|
});
|
|
53
71
|
```
|
|
54
72
|
|
|
55
|
-
|
|
73
|
+
Here's what I do for my blog:
|
|
74
|
+
|
|
75
|
+
1. Run `map` against the resulting array ([code](https://github.com/shepherdjerred/sjer.red/blob/f72b2b75bf0722ba8ff0fdd45f31c02c2ee5089d/src/components/BlogWebring.astro#L17-L22)).
|
|
76
|
+
1. Render a component for each entry ([code](https://github.com/shepherdjerred/sjer.red/blob/f72b2b75bf0722ba8ff0fdd45f31c02c2ee5089d/src/components/WebringEntry.astro)).
|
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;AAsCpB,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
|
@@ -14,6 +14,7 @@ async function loadCache({ cache_file }) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
async function saveCache({ cache_file }, cache) {
|
|
17
|
+
await fs.mkdir(cache_file.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
17
18
|
await fs.writeFile(cache_file, JSON.stringify(cache));
|
|
18
19
|
}
|
|
19
20
|
function toCacheEntry(result, now) {
|
|
@@ -51,3 +52,4 @@ export async function fetchWithCache(source, cache, config) {
|
|
|
51
52
|
console.log(`Fetching ${source.url}`);
|
|
52
53
|
return fetch(source, config.truncate);
|
|
53
54
|
}
|
|
55
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAOL,WAAW,GAEZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAEpD,KAAK,UAAU,SAAS,CAAC,EAAE,UAAU,EAAsB;IACzD,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC5B,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAChD,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,EAAE,UAAU,EAAsB,EAAE,KAAY;IACvE,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,MAAM,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,YAAY,CAAC,MAAmB,EAAE,GAAS;IAClD,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,OAAO,CAAC,OAAsB,EAAE,GAAS;IAChD,OAAO,CAAC,CAAC,IAAI,CACX,OAAO,EACP,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAC5C,CAAC,CAAC,WAAW,EAAE,CAChB,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,OAAsB,EAAE,MAA2B;IACtE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC3C,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAA2B;IAC9D,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAEjH,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAEnC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAc,EACd,KAAY,EACZ,MAA2B;IAE3B,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,OAAO,EAAE,GAAG,UAAU,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,sBAAsB,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;YACrG,OAAO,CAAC,GAAG,CAAC,yBAAyB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;YACpD,OAAO,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,YAAY,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IACtC,OAAO,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;AACxC,CAAC"}
|
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,QAAA,MAAM,YAAY;;;;;;;;;EAKhB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,QAAA,MAAM,wBAAwB;;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQvB,CAAC;AAGH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC5E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEpC,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,QAAA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMrB,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAA6B,CAAC;AAEhD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAG3B,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAChD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAA6B,CAAC;AAEtD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyBxB,CAAC"}
|
package/dist/types.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webring",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"prepare": "husky",
|
|
7
7
|
"lint": "eslint src",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"@eslint/js": "^9.4.0",
|
|
24
24
|
"@tsconfig/node20": "^20.1.4",
|
|
25
25
|
"@tsconfig/strictest": "^2.0.5",
|
|
26
|
-
"@types/eslint__js": "^8.42.3",
|
|
27
26
|
"@types/node": "^20.14.0",
|
|
28
27
|
"@types/sanitize-html": "^2.11.0",
|
|
29
28
|
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
|
@@ -50,6 +49,7 @@
|
|
|
50
49
|
},
|
|
51
50
|
"files": [
|
|
52
51
|
"dist",
|
|
52
|
+
"src",
|
|
53
53
|
"package.json",
|
|
54
54
|
"README.md",
|
|
55
55
|
"LICENSE",
|
|
@@ -0,0 +1,146 @@
|
|
|
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-06-05T00:00:00.000Z,
|
|
7
|
+
"preview": "TIL: Asymmetric Cryptography in Go",
|
|
8
|
+
"source": {
|
|
9
|
+
"title": "Jerred Shepherd",
|
|
10
|
+
"url": "https://sjer.red/rss.xml",
|
|
11
|
+
},
|
|
12
|
+
"title": "TIL: Asymmetric Cryptography in Go",
|
|
13
|
+
"url": "https://sjer.red/blog/til/2024-06-05/",
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
exports[`it should fetch an RSS feed without caching 1`] = `
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
"date": 2024-06-05T00:00:00.000Z,
|
|
22
|
+
"preview": "TIL: Asymmetric Cryptography in Go",
|
|
23
|
+
"source": {
|
|
24
|
+
"title": "Jerred Shepherd",
|
|
25
|
+
"url": "https://sjer.red/rss.xml",
|
|
26
|
+
},
|
|
27
|
+
"title": "TIL: Asymmetric Cryptography in Go",
|
|
28
|
+
"url": "https://sjer.red/blog/til/2024-06-05/",
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
exports[`it should fetch several RSS feeds 1`] = `
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
"date": 2024-06-20T14:00:00.000Z,
|
|
37
|
+
"preview": "Looking for constructors that take a character count. The post How to convert between different types of counted-string string types appeared first on The Old New Thing.",
|
|
38
|
+
"source": {
|
|
39
|
+
"title": "The Old New Thing",
|
|
40
|
+
"url": "https://devblogs.microsoft.com/oldnewthing/feed",
|
|
41
|
+
},
|
|
42
|
+
"title": "How to convert between different types of counted-string string types",
|
|
43
|
+
"url": "https://devblogs.microsoft.com/oldnewthing/20240620-00/?p=109922",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"date": 2024-06-19T00:00:00.000Z,
|
|
47
|
+
"preview": "The recent innovations in the AI space, most notably those such as GPT-4, obviously have far-reaching implications for society, ranging from the utopian eliminating of drudgery, to the dystopian damage to the livelihood of artists in a capitalist society, to existential threats to humanity itself. I...",
|
|
48
|
+
"source": {
|
|
49
|
+
"title": "Ludicity",
|
|
50
|
+
"url": "https://ludic.mataroa.blog/rss/",
|
|
51
|
+
},
|
|
52
|
+
"title": "I Will Fucking Piledrive You If You Mention AI Again",
|
|
53
|
+
"url": "https://ludic.mataroa.blog/blog/i-will-fucking-piledrive-you-if-you-mention-ai-again/",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"date": 2024-06-19T00:00:00.000Z,
|
|
57
|
+
"preview": "I love this meetup so much",
|
|
58
|
+
"source": {
|
|
59
|
+
"title": "Xe Iaso",
|
|
60
|
+
"url": "https://xeiaso.net/blog.rss",
|
|
61
|
+
},
|
|
62
|
+
"title": "AI Tinkerers Ottawa v2.5.0 trip report",
|
|
63
|
+
"url": "https://xeiaso.net/notes/2024/ait-ottawa-2.5.0/",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"date": 2024-06-08T04:30:00.000Z,
|
|
67
|
+
"preview": "Fresh water from snow, at 70 below!",
|
|
68
|
+
"source": {
|
|
69
|
+
"title": "brr.fyi",
|
|
70
|
+
"url": "https://brr.fyi/feed.xml",
|
|
71
|
+
},
|
|
72
|
+
"title": "South Pole Water Infrastructure",
|
|
73
|
+
"url": "https://brr.fyi/posts/south-pole-water-infrastructure",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"date": 2024-06-01T00:00:00.000Z,
|
|
77
|
+
"preview": ".dog-line { display: flex; flex-wrap: nowrap; flex-direction: row; width: 100%; height: 10rem; margin-top: 2rem; margin-bottom: 2rem; } .dog-line img { flex-grow: 1; height: auto; margin: 0; padding: 0; object-fit: contain; } .dog-grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-gap...",
|
|
78
|
+
"source": {
|
|
79
|
+
"title": "Sam Rose",
|
|
80
|
+
"url": "https://samwho.dev/rss.xml",
|
|
81
|
+
},
|
|
82
|
+
"title": "A Commitment to Art and Dogs",
|
|
83
|
+
"url": "https://samwho.dev/dogs/",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"date": 2024-05-31T00:00:00.000Z,
|
|
87
|
+
"preview": "This is an archive of some posts in a forum thread titled "Beware of Bioware" in a now defunct forum, with comments from that forum as well as blog comments from a now defunct blog that archived that made the first attempt to archive this content. The original posts were deleted shortly after being ...",
|
|
88
|
+
"source": {
|
|
89
|
+
"title": "Dan Luu",
|
|
90
|
+
"url": "https://danluu.com/atom.xml",
|
|
91
|
+
},
|
|
92
|
+
"title": "Work-life balance at Bioware",
|
|
93
|
+
"url": "https://danluu.com/bioware/",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"date": 2024-05-24T00:00:00.000Z,
|
|
97
|
+
"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...",
|
|
98
|
+
"source": {
|
|
99
|
+
"title": "Drew DeVault",
|
|
100
|
+
"url": "https://drewdevault.com/blog/index.xml",
|
|
101
|
+
},
|
|
102
|
+
"title": "Writing a Unix clone in about a month",
|
|
103
|
+
"url": "https://drewdevault.com/2024/05/24/2024-05-24-Bunnix.html",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"date": 2024-02-27T12:00:00.000Z,
|
|
107
|
+
"preview": "The dream of soaring in the sky like a bird has captivated the human mind for ages. Although many failed, some eventually succeeded in achieving that goal. These days we take air transportation for granted, but the physics of flight can still be puzzling. In this article we’ll investigate what makes...",
|
|
108
|
+
"source": {
|
|
109
|
+
"title": "Bartosz Ciechanowski",
|
|
110
|
+
"url": "https://ciechanow.ski/atom.xml",
|
|
111
|
+
},
|
|
112
|
+
"title": "Airfoil",
|
|
113
|
+
"url": "https://ciechanow.ski/airfoil/",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"date": 2024-01-29T00:00:00.000Z,
|
|
117
|
+
"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.",
|
|
118
|
+
"source": {
|
|
119
|
+
"title": "Jake Lazaroff",
|
|
120
|
+
"url": "https://jakelazaroff.com/rss.xml",
|
|
121
|
+
},
|
|
122
|
+
"title": "The Web Component Success Story",
|
|
123
|
+
"url": "https://jakelazaroff.com/words/the-web-component-success-story/",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"date": 2023-04-07T00:00:00.000Z,
|
|
127
|
+
"preview": "In early 2019, some months after completing a rehab program for drug addiction, I was in a very open-minded headspace where I wanted to challenge myself and find ways to improve as a person. Drugs had filled my life with secrecy and lies, but that life was over. Although I was unsure of my next step...",
|
|
128
|
+
"source": {
|
|
129
|
+
"title": "Andreas Kling",
|
|
130
|
+
"url": "https://awesomekling.github.io/feed.xml",
|
|
131
|
+
},
|
|
132
|
+
"title": "Making myself uncomfortable again",
|
|
133
|
+
"url": "https://awesomekling.github.io/Making-myself-uncomfortable-again/",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"date": 2022-09-17T00:00:00.000Z,
|
|
137
|
+
"preview": undefined,
|
|
138
|
+
"source": {
|
|
139
|
+
"title": "Explained From First Principles",
|
|
140
|
+
"url": "https://explained-from-first-principles.com/feed.xml",
|
|
141
|
+
},
|
|
142
|
+
"title": "Number theory explained from first principles",
|
|
143
|
+
"url": "https://explained-from-first-principles.com/number-theory/",
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
`;
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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 fs from "fs/promises";
|
|
14
|
+
import { asyncMapFilterUndefined } from "./util.js";
|
|
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
|
+
await fs.mkdir(cache_file.split("/").slice(0, -1).join("/"), { recursive: true });
|
|
28
|
+
await fs.writeFile(cache_file, JSON.stringify(cache));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toCacheEntry(result: ResultEntry, now: Date): [string, CacheEntry] {
|
|
32
|
+
return [result.source.url, { timestamp: now, data: result }];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toCache(results: ResultEntry[], now: Date): Cache {
|
|
36
|
+
return R.pipe(
|
|
37
|
+
results,
|
|
38
|
+
R.map((result) => toCacheEntry(result, now)),
|
|
39
|
+
R.fromEntries(),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateCache(results: ResultEntry[], config: CachedConfiguration): Promise<void> {
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const updatedCache = toCache(results, now);
|
|
46
|
+
return saveCache(config.cache, updatedCache);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function fetchAllCached(config: CachedConfiguration): Promise<Result> {
|
|
50
|
+
const cache = await loadCache(config.cache);
|
|
51
|
+
|
|
52
|
+
const results = await asyncMapFilterUndefined(config.sources, (source) => fetchWithCache(source, cache, config));
|
|
53
|
+
|
|
54
|
+
await updateCache(results, config);
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function fetchWithCache(
|
|
60
|
+
source: Source,
|
|
61
|
+
cache: Cache,
|
|
62
|
+
config: CachedConfiguration,
|
|
63
|
+
): Promise<ResultEntry | undefined> {
|
|
64
|
+
const cacheEntry = cache[source.url];
|
|
65
|
+
if (cacheEntry) {
|
|
66
|
+
const now = new Date();
|
|
67
|
+
if (now.getTime() - cacheEntry.timestamp.getTime() < config.cache.cache_duration_minutes * 60 * 1000) {
|
|
68
|
+
console.log(`Cache entry found for ${source.url}.`);
|
|
69
|
+
return Promise.resolve(cacheEntry.data);
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`Cache entry for ${source.url} is too old.`);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`No cache entry for ${source.url}.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`Fetching ${source.url}`);
|
|
78
|
+
return fetch(source, config.truncate);
|
|
79
|
+
}
|
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,114 @@
|
|
|
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", 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", 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
|
+
url: "https://jakelazaroff.com/rss.xml",
|
|
38
|
+
title: "Jake Lazaroff",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
url: "https://awesomekling.github.io/feed.xml",
|
|
42
|
+
title: "Andreas Kling",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
url: "https://xeiaso.net/blog.rss",
|
|
46
|
+
title: "Xe Iaso",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
url: "https://ciechanow.ski/atom.xml",
|
|
50
|
+
title: "Bartosz Ciechanowski",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
url: "https://explained-from-first-principles.com/feed.xml",
|
|
54
|
+
title: "Explained From First Principles",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
url: "http://www.aaronsw.com/2002/feeds/pgessays.rss",
|
|
58
|
+
title: "Paul Graham",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
url: "https://samwho.dev/rss.xml",
|
|
62
|
+
title: "Sam Rose",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
url: "https://rachelbythebay.com/w/atom.xml",
|
|
66
|
+
title: "Rachel Kroll",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
url: "https://brr.fyi/feed.xml",
|
|
70
|
+
title: "brr.fyi",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
url: "https://devblogs.microsoft.com/oldnewthing/feed",
|
|
74
|
+
title: "The Old New Thing",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
url: "https://ludic.mataroa.blog/rss/",
|
|
78
|
+
title: "Ludicity",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
number: 20,
|
|
82
|
+
truncate: 300,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = await run(config);
|
|
86
|
+
expect(result).toMatchSnapshot();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("it should fetch an RSS feed with caching", async () => {
|
|
90
|
+
const config: Configuration = {
|
|
91
|
+
sources: [
|
|
92
|
+
{
|
|
93
|
+
title: "Jerred Shepherd",
|
|
94
|
+
url: "https://sjer.red/rss.xml",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
number: 1,
|
|
98
|
+
truncate: 300,
|
|
99
|
+
cache: {
|
|
100
|
+
cache_file: `${await createTempDir()}/cache.json`,
|
|
101
|
+
cache_duration_minutes: 1,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = await run(config);
|
|
106
|
+
expect(result).toMatchSnapshot();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// https://sdorra.dev/posts/2024-02-12-vitest-tmpdir
|
|
110
|
+
async function createTempDir() {
|
|
111
|
+
const ostmpdir = tmpdir();
|
|
112
|
+
const dir = join(ostmpdir, "unit-test-");
|
|
113
|
+
return await mkdtemp(dir);
|
|
114
|
+
}
|
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,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export type Source = z.infer<typeof SourceSchema>;
|
|
4
|
+
const SourceSchema = z.object({
|
|
5
|
+
// the url of the feed
|
|
6
|
+
url: z.string(),
|
|
7
|
+
// a title for the feed
|
|
8
|
+
title: z.string(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type CacheConfiguration = z.infer<typeof CacheConfigurationSchema>;
|
|
12
|
+
const CacheConfigurationSchema = z.object({
|
|
13
|
+
// how long to cache a results for
|
|
14
|
+
cache_duration_minutes: z.number().default(60),
|
|
15
|
+
cache_file: z.string().default("cache.json"),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type Configuration = z.infer<typeof ConfigurationSchema>;
|
|
19
|
+
const ConfigurationSchema = z.object({
|
|
20
|
+
// list of sources to fetch
|
|
21
|
+
sources: SourceSchema.array(),
|
|
22
|
+
// how many entries to return
|
|
23
|
+
number: z.number().default(3),
|
|
24
|
+
// how many words to truncate the preview to
|
|
25
|
+
truncate: z.number().default(300),
|
|
26
|
+
cache: CacheConfigurationSchema.optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// CachedConfiguration is the same as Configuration but cache is not optional
|
|
30
|
+
export type CachedConfiguration = z.infer<typeof CachedConfigurationSchema>;
|
|
31
|
+
export const CachedConfigurationSchema = ConfigurationSchema.extend({
|
|
32
|
+
cache: CacheConfigurationSchema,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type ResultEntry = z.infer<typeof ResultEntrySchema>;
|
|
36
|
+
const ResultEntrySchema = z.object({
|
|
37
|
+
title: z.string(),
|
|
38
|
+
url: z.string(),
|
|
39
|
+
date: z.coerce.date(),
|
|
40
|
+
source: SourceSchema,
|
|
41
|
+
preview: z.string().optional(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type Result = z.infer<typeof ResultSchema>;
|
|
45
|
+
const ResultSchema = z.array(ResultEntrySchema);
|
|
46
|
+
|
|
47
|
+
export type CacheEntry = z.infer<typeof CacheEntrySchema>;
|
|
48
|
+
export const CacheEntrySchema = z.object({
|
|
49
|
+
timestamp: z.coerce.date(),
|
|
50
|
+
data: ResultEntrySchema,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type Cache = z.infer<typeof CacheSchema>;
|
|
54
|
+
export const CacheSchema = z.record(CacheEntrySchema);
|
|
55
|
+
|
|
56
|
+
export const FeedEntrySchema = z
|
|
57
|
+
.object({
|
|
58
|
+
title: z.string(),
|
|
59
|
+
link: z.string(),
|
|
60
|
+
isoDate: z.coerce.date().optional(),
|
|
61
|
+
pubDate: z.coerce.date().optional(),
|
|
62
|
+
content: z.string().optional(),
|
|
63
|
+
contentSnippet: z.string().optional(),
|
|
64
|
+
"content:encoded": z.string().optional(),
|
|
65
|
+
description: z.string().optional(),
|
|
66
|
+
})
|
|
67
|
+
.transform((entry) => {
|
|
68
|
+
const date = entry.isoDate ?? entry.pubDate;
|
|
69
|
+
if (!date) {
|
|
70
|
+
throw new Error("no date found in feed entry");
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
title: entry.title,
|
|
74
|
+
link: entry.link,
|
|
75
|
+
date,
|
|
76
|
+
content: entry.content,
|
|
77
|
+
contentSnippet: entry.contentSnippet,
|
|
78
|
+
description: entry.description,
|
|
79
|
+
"content:encoded": entry["content:encoded"],
|
|
80
|
+
};
|
|
81
|
+
});
|
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
|
+
}
|