webring 1.1.0 → 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 +11 -0
- package/README.md +107 -30
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +5 -2
- package/dist/cache.js.map +1 -1
- package/dist/index.test.js +4 -48
- package/dist/index.test.js.map +1 -1
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -7
- package/dist/types.js.map +1 -1
- package/package.json +21 -2
- package/src/__snapshots__/index.test.ts.snap +12 -102
- package/src/cache.ts +5 -2
- package/src/index.test.ts +4 -48
- package/src/types.ts +26 -8
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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
|
+
|
|
8
19
|
## [1.1.0](https://github.com/shepherdjerred/webring/compare/v1.0.3...v1.1.0) (2024-06-28)
|
|
9
20
|
|
|
10
21
|
|
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.map
CHANGED
|
@@ -1 +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;
|
|
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) {
|
package/dist/cache.js.map
CHANGED
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
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
|
{
|
package/dist/index.test.js.map
CHANGED
|
@@ -1 +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,KAAK,IAAI,EAAE;
|
|
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;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +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
|
|
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(),
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5B,
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
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.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",
|
|
@@ -31,6 +48,8 @@
|
|
|
31
48
|
"husky": "^9.0.11",
|
|
32
49
|
"lint-staged": "^15.2.5",
|
|
33
50
|
"prettier": "^3.3.0",
|
|
51
|
+
"typedoc": "^0.26.3",
|
|
52
|
+
"typedoc-plugin-zod": "^1.2.0",
|
|
34
53
|
"typescript": "^5.4.5",
|
|
35
54
|
"typescript-eslint": "^7.11.0",
|
|
36
55
|
"vitest": "^1.6.0"
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
exports[`it should fetch an RSS feed with caching 1`] = `
|
|
4
4
|
[
|
|
5
5
|
{
|
|
6
|
-
"date": 2024-
|
|
7
|
-
"preview": "TIL:
|
|
6
|
+
"date": 2024-07-01T00:00:00.000Z,
|
|
7
|
+
"preview": "TIL: Using Twoslash with Shiki and Astro",
|
|
8
8
|
"source": {
|
|
9
9
|
"title": "Jerred Shepherd",
|
|
10
10
|
"url": "https://sjer.red/rss.xml",
|
|
11
11
|
},
|
|
12
|
-
"title": "TIL:
|
|
13
|
-
"url": "https://sjer.red/blog/til/2024-
|
|
12
|
+
"title": "TIL: Using Twoslash with Shiki and Astro",
|
|
13
|
+
"url": "https://sjer.red/blog/til/2024-07-01/",
|
|
14
14
|
},
|
|
15
15
|
]
|
|
16
16
|
`;
|
|
@@ -18,14 +18,14 @@ exports[`it should fetch an RSS feed with caching 1`] = `
|
|
|
18
18
|
exports[`it should fetch an RSS feed without caching 1`] = `
|
|
19
19
|
[
|
|
20
20
|
{
|
|
21
|
-
"date": 2024-
|
|
22
|
-
"preview": "TIL:
|
|
21
|
+
"date": 2024-07-01T00:00:00.000Z,
|
|
22
|
+
"preview": "TIL: Using Twoslash with Shiki and Astro",
|
|
23
23
|
"source": {
|
|
24
24
|
"title": "Jerred Shepherd",
|
|
25
25
|
"url": "https://sjer.red/rss.xml",
|
|
26
26
|
},
|
|
27
|
-
"title": "TIL:
|
|
28
|
-
"url": "https://sjer.red/blog/til/2024-
|
|
27
|
+
"title": "TIL: Using Twoslash with Shiki and Astro",
|
|
28
|
+
"url": "https://sjer.red/blog/til/2024-07-01/",
|
|
29
29
|
},
|
|
30
30
|
]
|
|
31
31
|
`;
|
|
@@ -33,64 +33,14 @@ exports[`it should fetch an RSS feed without caching 1`] = `
|
|
|
33
33
|
exports[`it should fetch several RSS feeds 1`] = `
|
|
34
34
|
[
|
|
35
35
|
{
|
|
36
|
-
"date": 2024-06-
|
|
37
|
-
"preview": "
|
|
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 ...",
|
|
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. ...",
|
|
88
38
|
"source": {
|
|
89
39
|
"title": "Dan Luu",
|
|
90
40
|
"url": "https://danluu.com/atom.xml",
|
|
91
41
|
},
|
|
92
|
-
"title": "
|
|
93
|
-
"url": "https://danluu.com/
|
|
42
|
+
"title": "A discussion of discussions on AI bias",
|
|
43
|
+
"url": "https://danluu.com/ai-bias/",
|
|
94
44
|
},
|
|
95
45
|
{
|
|
96
46
|
"date": 2024-05-24T00:00:00.000Z,
|
|
@@ -102,45 +52,5 @@ exports[`it should fetch several RSS feeds 1`] = `
|
|
|
102
52
|
"title": "Writing a Unix clone in about a month",
|
|
103
53
|
"url": "https://drewdevault.com/2024/05/24/2024-05-24-Bunnix.html",
|
|
104
54
|
},
|
|
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
55
|
]
|
|
146
56
|
`;
|
package/src/cache.ts
CHANGED
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
type CacheConfiguration,
|
|
11
11
|
} from "./types.js";
|
|
12
12
|
import { fetch } from "./fetch.js";
|
|
13
|
-
import fs from "fs/promises";
|
|
14
13
|
import { asyncMapFilterUndefined } from "./util.js";
|
|
14
|
+
import fs from "fs/promises";
|
|
15
15
|
|
|
16
16
|
async function loadCache({ cache_file }: CacheConfiguration): Promise<Cache> {
|
|
17
17
|
try {
|
|
@@ -24,7 +24,10 @@ async function loadCache({ cache_file }: CacheConfiguration): Promise<Cache> {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async function saveCache({ cache_file }: CacheConfiguration, cache: Cache) {
|
|
27
|
-
|
|
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
|
+
}
|
|
28
31
|
await fs.writeFile(cache_file, JSON.stringify(cache));
|
|
29
32
|
}
|
|
30
33
|
|
package/src/index.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdtemp } from "fs/promises";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
|
|
8
8
|
// TODO: intercept network requests
|
|
9
|
-
test("it should fetch an RSS feed without caching", async () => {
|
|
9
|
+
test("it should fetch an RSS feed without caching", { timeout: 30000 }, async () => {
|
|
10
10
|
const config: Configuration = {
|
|
11
11
|
sources: [
|
|
12
12
|
{
|
|
@@ -22,7 +22,7 @@ test("it should fetch an RSS feed without caching", async () => {
|
|
|
22
22
|
expect(result).toMatchSnapshot();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test("it should fetch several RSS feeds", async () => {
|
|
25
|
+
test("it should fetch several RSS feeds", { timeout: 30000 }, async () => {
|
|
26
26
|
const config: Configuration = {
|
|
27
27
|
sources: [
|
|
28
28
|
{
|
|
@@ -33,52 +33,8 @@ test("it should fetch several RSS feeds", async () => {
|
|
|
33
33
|
url: "https://danluu.com/atom.xml",
|
|
34
34
|
title: "Dan Luu",
|
|
35
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
36
|
],
|
|
81
|
-
number:
|
|
37
|
+
number: 3,
|
|
82
38
|
truncate: 300,
|
|
83
39
|
};
|
|
84
40
|
|
|
@@ -86,7 +42,7 @@ test("it should fetch several RSS feeds", async () => {
|
|
|
86
42
|
expect(result).toMatchSnapshot();
|
|
87
43
|
});
|
|
88
44
|
|
|
89
|
-
test("it should fetch an RSS feed with caching", async () => {
|
|
45
|
+
test("it should fetch an RSS feed with caching", { timeout: 30000 }, async () => {
|
|
90
46
|
const config: Configuration = {
|
|
91
47
|
sources: [
|
|
92
48
|
{
|
package/src/types.ts
CHANGED
|
@@ -1,58 +1,76 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export type Source = z.infer<typeof SourceSchema>;
|
|
4
|
+
/** An RSS source */
|
|
4
5
|
const SourceSchema = z.object({
|
|
5
|
-
|
|
6
|
+
/** The URL of an RSS feed */
|
|
6
7
|
url: z.string(),
|
|
7
|
-
|
|
8
|
-
title: z.string(),
|
|
8
|
+
/** A title to describe the feed */
|
|
9
|
+
title: z.string().describe("A title for the feed"),
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
export type CacheConfiguration = z.infer<typeof CacheConfigurationSchema>;
|
|
13
|
+
/** Configuration for the cache */
|
|
12
14
|
const CacheConfigurationSchema = z.object({
|
|
13
|
-
|
|
15
|
+
/** How long to cache a result for */
|
|
14
16
|
cache_duration_minutes: z.number().default(60),
|
|
17
|
+
/** The location of a file to use as a cache */
|
|
15
18
|
cache_file: z.string().default("cache.json"),
|
|
16
19
|
});
|
|
17
20
|
|
|
18
21
|
export type Configuration = z.infer<typeof ConfigurationSchema>;
|
|
22
|
+
/** A configuration object with caching possibly configured */
|
|
19
23
|
const ConfigurationSchema = z.object({
|
|
20
|
-
|
|
24
|
+
/** A list of sources to fetch */
|
|
21
25
|
sources: SourceSchema.array(),
|
|
22
|
-
|
|
26
|
+
/** Return the n latest updates from the source list. */
|
|
23
27
|
number: z.number().default(3),
|
|
24
|
-
|
|
28
|
+
/** How many words the preview field should be truncated to in characters after HTML has been sanitized and parsed. */
|
|
25
29
|
truncate: z.number().default(300),
|
|
30
|
+
/** Configuration for the cache */
|
|
26
31
|
cache: CacheConfigurationSchema.optional(),
|
|
27
32
|
});
|
|
28
33
|
|
|
29
|
-
// CachedConfiguration is the same as Configuration but cache is not optional
|
|
30
34
|
export type CachedConfiguration = z.infer<typeof CachedConfigurationSchema>;
|
|
35
|
+
/** A configuration object with caching definitely configured */
|
|
31
36
|
export const CachedConfigurationSchema = ConfigurationSchema.extend({
|
|
37
|
+
/** Configuration for the cache */
|
|
32
38
|
cache: CacheConfigurationSchema,
|
|
33
39
|
});
|
|
34
40
|
|
|
35
41
|
export type ResultEntry = z.infer<typeof ResultEntrySchema>;
|
|
42
|
+
/** A single entry from an RSS feed */
|
|
36
43
|
const ResultEntrySchema = z.object({
|
|
44
|
+
/** The title of the entry */
|
|
37
45
|
title: z.string(),
|
|
46
|
+
/** A direct link to the entry */
|
|
38
47
|
url: z.string(),
|
|
48
|
+
/** The date of the entry */
|
|
39
49
|
date: z.coerce.date(),
|
|
50
|
+
/** The source the entry is from */
|
|
40
51
|
source: SourceSchema,
|
|
52
|
+
/** A preview of the entry. This may contain sanitized HTML. */
|
|
41
53
|
preview: z.string().optional(),
|
|
42
54
|
});
|
|
43
55
|
|
|
44
56
|
export type Result = z.infer<typeof ResultSchema>;
|
|
57
|
+
/** A list of results */
|
|
45
58
|
const ResultSchema = z.array(ResultEntrySchema);
|
|
46
59
|
|
|
47
60
|
export type CacheEntry = z.infer<typeof CacheEntrySchema>;
|
|
61
|
+
/** A single cache entry */
|
|
48
62
|
export const CacheEntrySchema = z.object({
|
|
63
|
+
/** The time a source was last checked */
|
|
49
64
|
timestamp: z.coerce.date(),
|
|
65
|
+
/** The data from the source */
|
|
50
66
|
data: ResultEntrySchema,
|
|
51
67
|
});
|
|
52
68
|
|
|
53
69
|
export type Cache = z.infer<typeof CacheSchema>;
|
|
70
|
+
/** A mapping of source URLs to cache entries */
|
|
54
71
|
export const CacheSchema = z.record(CacheEntrySchema);
|
|
55
72
|
|
|
73
|
+
/** The expected format fetched RSS feed entries */
|
|
56
74
|
export const FeedEntrySchema = z
|
|
57
75
|
.object({
|
|
58
76
|
title: z.string(),
|