webring 0.0.1 → 0.3.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 +17 -0
- package/README.md +38 -9
- package/package.json +17 -6
- package/.github/dependabot.yml +0 -12
- package/.prettierrc.json +0 -3
- package/.vscode/extensions.json +0 -3
- package/Earthfile +0 -1
- package/dist/cache.d.ts +0 -3
- package/dist/cache.js +0 -24
- package/dist/fetch.d.ts +0 -2
- package/dist/fetch.js +0 -27
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -21
- package/dist/types.d.ts +0 -284
- package/dist/types.js +0 -50
- package/eslint.config.js +0 -17
- package/src/cache.ts +0 -48
- package/src/fetch.ts +0 -38
- package/src/index.ts +0 -26
- package/src/types.ts +0 -63
- package/tsconfig.json +0 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0](https://github.com/shepherdjerred/webring/compare/v0.2.0...v0.3.0) (2024-06-03)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* allow filename to be configurable ([cc7bb5f](https://github.com/shepherdjerred/webring/commit/cc7bb5f3139f306952d03568fc63cc9fcbfaad5e))
|
|
14
|
+
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
* Initial release
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# webring
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/webring)
|
|
4
|
+
|
|
3
5
|
`webring` gathers the latest posts from your favorite RSS feeds so that you can embed them on your site.
|
|
4
6
|
|
|
5
7
|
Inspired by:
|
|
@@ -7,18 +9,45 @@ Inspired by:
|
|
|
7
9
|
- https://github.com/lukehsiao/openring-rs
|
|
8
10
|
- https://git.sr.ht/~sircmpwn/openring
|
|
9
11
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
This library is meant to be used with static site generators. It is framework agnostic.
|
|
13
|
-
|
|
14
|
-
### Astro
|
|
12
|
+
## Installation
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
```bash
|
|
15
|
+
npm i webring
|
|
16
|
+
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## Quick Start
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
This library is meant to be used with static site generators. It is framework agnostic.
|
|
21
21
|
|
|
22
22
|
```typescript
|
|
23
|
-
|
|
23
|
+
import { type Configuration, run } from "webring";
|
|
24
|
+
|
|
25
|
+
// create a configuration object
|
|
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
|
+
number: 3,
|
|
42
|
+
cache_duration_minutes: 60,
|
|
43
|
+
truncate: 300,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// run the application
|
|
47
|
+
const result = await run(config);
|
|
48
|
+
|
|
49
|
+
// do something with the results
|
|
50
|
+
result.map((entry) => {
|
|
51
|
+
console.log(entry);
|
|
52
|
+
});
|
|
24
53
|
```
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webring",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "eslint .",
|
|
7
7
|
"build": "tsc",
|
|
8
|
-
"test": ""
|
|
8
|
+
"test": "",
|
|
9
|
+
"prepare": "husky"
|
|
9
10
|
},
|
|
10
11
|
"main": "dist/index.js",
|
|
11
12
|
"types": "dist/index.d.ts",
|
|
@@ -17,11 +18,13 @@
|
|
|
17
18
|
"zod": "^3.23.8"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
|
-
"@
|
|
21
|
+
"@commitlint/cli": "^19.3.0",
|
|
22
|
+
"@commitlint/config-conventional": "^19.2.2",
|
|
23
|
+
"@eslint/js": "^9.4.0",
|
|
21
24
|
"@tsconfig/node20": "^20.1.4",
|
|
22
25
|
"@tsconfig/strictest": "^2.0.5",
|
|
23
26
|
"@types/eslint__js": "^8.42.3",
|
|
24
|
-
"@types/node": "^20.
|
|
27
|
+
"@types/node": "^20.13.0",
|
|
25
28
|
"@types/sanitize-html": "^2.11.0",
|
|
26
29
|
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
|
27
30
|
"@typescript-eslint/parser": "^7.11.0",
|
|
@@ -33,7 +36,15 @@
|
|
|
33
36
|
"typescript-eslint": "^7.11.0"
|
|
34
37
|
},
|
|
35
38
|
"lint-staged": {
|
|
36
|
-
"*.{
|
|
39
|
+
"*.{ts,tsx}": "eslint --cache --fix",
|
|
37
40
|
"*": "prettier --ignore-unknown --write"
|
|
38
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"package.json",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"package-lock.json",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
]
|
|
39
50
|
}
|
package/.github/dependabot.yml
DELETED
package/.prettierrc.json
DELETED
package/.vscode/extensions.json
DELETED
package/Earthfile
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION 0.9
|
package/dist/cache.d.ts
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import type { Configuration, Cache, Result, ResultEntry, Source } from "./types.js";
|
|
2
|
-
export declare function runWithCache(config: Configuration, cache: Cache): Promise<[Result, Cache]>;
|
|
3
|
-
export declare function fetchWithCache(source: Source, cache: Cache, config: Configuration): Promise<ResultEntry | undefined>;
|
package/dist/cache.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import * as R from "remeda";
|
|
2
|
-
export async function runWithCache(config, cache) {
|
|
3
|
-
const promises = R.pipe(config.sources, R.map((source) => fetchWithCache(source, cache, config)), R.filter((result) => result !== undefined));
|
|
4
|
-
const results = await Promise.all(promises);
|
|
5
|
-
const definedResults = results.filter((result) => result !== undefined);
|
|
6
|
-
const updatedCache = R.pipe(definedResults, R.map((result) => [result.source.url, { timestamp: new Date(), data: result }]), R.fromEntries());
|
|
7
|
-
const topResults = R.pipe(definedResults, R.sortBy((result) => result.date.getTime()), R.reverse(), R.take(config.number));
|
|
8
|
-
return [topResults, updatedCache];
|
|
9
|
-
}
|
|
10
|
-
export async function fetchWithCache(source, cache, config) {
|
|
11
|
-
const cacheEntry = cache[source.url];
|
|
12
|
-
if (cacheEntry) {
|
|
13
|
-
const now = new Date();
|
|
14
|
-
if (now.getTime() - cacheEntry.timestamp.getTime() < config.cache_duration_minutes * 60 * 1000) {
|
|
15
|
-
console.log(`Cache entry found for ${source.url}`);
|
|
16
|
-
return Promise.resolve(cacheEntry.data);
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
console.log(`Cache entry for ${source.url} is too old`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
console.log(`No cache entry for ${source.url}`);
|
|
23
|
-
return fetch(source, config.truncate);
|
|
24
|
-
}
|
package/dist/fetch.d.ts
DELETED
package/dist/fetch.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import Parser from "rss-parser";
|
|
2
|
-
import sanitizeHtml from "sanitize-html";
|
|
3
|
-
import truncate from "truncate-html";
|
|
4
|
-
import { FeedEntrySchema } from "./types.js";
|
|
5
|
-
import * as R from "remeda";
|
|
6
|
-
export async function fetch(source, length) {
|
|
7
|
-
const parser = new Parser();
|
|
8
|
-
try {
|
|
9
|
-
const feed = await parser.parseURL(source.url);
|
|
10
|
-
const firstItem = R.pipe(feed.items, R.map((item) => FeedEntrySchema.parse(item)), R.sortBy((item) => new Date(item.date).getTime()), R.reverse(), R.first());
|
|
11
|
-
if (!firstItem) {
|
|
12
|
-
throw new Error("no items found in feed");
|
|
13
|
-
}
|
|
14
|
-
const preview = firstItem.contentSnippet ?? firstItem.content ?? firstItem.description ?? firstItem["content:encoded"];
|
|
15
|
-
return {
|
|
16
|
-
title: firstItem.title,
|
|
17
|
-
url: firstItem.link,
|
|
18
|
-
date: new Date(firstItem.date),
|
|
19
|
-
source,
|
|
20
|
-
preview: preview ? truncate(sanitizeHtml(preview), length) : undefined,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
catch (e) {
|
|
24
|
-
console.error(`Error fetching ${source.url}: ${e}`);
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { runWithCache } from "./cache.js";
|
|
2
|
-
import { CacheSchema } from "./types.js";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
export async function run(config) {
|
|
5
|
-
const cacheFilename = "cache.json";
|
|
6
|
-
const currentDir = process.cwd();
|
|
7
|
-
const fullFilename = `${currentDir}/${cacheFilename}`;
|
|
8
|
-
let cacheObject = {};
|
|
9
|
-
try {
|
|
10
|
-
const cacheFile = await fs.readFile(fullFilename);
|
|
11
|
-
cacheObject = CacheSchema.parse(JSON.parse(cacheFile.toString()));
|
|
12
|
-
}
|
|
13
|
-
catch (e) {
|
|
14
|
-
console.error("Error reading cache file:", e);
|
|
15
|
-
throw e;
|
|
16
|
-
}
|
|
17
|
-
const [result, updatedCache] = await runWithCache(config, cacheObject);
|
|
18
|
-
// write the updated cache to cache.json
|
|
19
|
-
await fs.writeFile(fullFilename, JSON.stringify(updatedCache));
|
|
20
|
-
return result;
|
|
21
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
export type Source = z.infer<typeof SourceSchema>;
|
|
3
|
-
declare const SourceSchema: z.ZodObject<{
|
|
4
|
-
url: z.ZodString;
|
|
5
|
-
title: z.ZodString;
|
|
6
|
-
}, "strip", z.ZodTypeAny, {
|
|
7
|
-
url: string;
|
|
8
|
-
title: string;
|
|
9
|
-
}, {
|
|
10
|
-
url: string;
|
|
11
|
-
title: string;
|
|
12
|
-
}>;
|
|
13
|
-
export type Configuration = z.infer<typeof ConfigurationSchema>;
|
|
14
|
-
declare const ConfigurationSchema: z.ZodObject<{
|
|
15
|
-
sources: z.ZodArray<z.ZodObject<{
|
|
16
|
-
url: z.ZodString;
|
|
17
|
-
title: z.ZodString;
|
|
18
|
-
}, "strip", z.ZodTypeAny, {
|
|
19
|
-
url: string;
|
|
20
|
-
title: string;
|
|
21
|
-
}, {
|
|
22
|
-
url: string;
|
|
23
|
-
title: string;
|
|
24
|
-
}>, "many">;
|
|
25
|
-
number: z.ZodNumber;
|
|
26
|
-
cache_duration_minutes: z.ZodNumber;
|
|
27
|
-
truncate: z.ZodNumber;
|
|
28
|
-
}, "strip", z.ZodTypeAny, {
|
|
29
|
-
number: number;
|
|
30
|
-
sources: {
|
|
31
|
-
url: string;
|
|
32
|
-
title: string;
|
|
33
|
-
}[];
|
|
34
|
-
cache_duration_minutes: number;
|
|
35
|
-
truncate: number;
|
|
36
|
-
}, {
|
|
37
|
-
number: number;
|
|
38
|
-
sources: {
|
|
39
|
-
url: string;
|
|
40
|
-
title: string;
|
|
41
|
-
}[];
|
|
42
|
-
cache_duration_minutes: number;
|
|
43
|
-
truncate: number;
|
|
44
|
-
}>;
|
|
45
|
-
export type ResultEntry = z.infer<typeof ResultEntrySchema>;
|
|
46
|
-
declare const ResultEntrySchema: z.ZodObject<{
|
|
47
|
-
title: z.ZodString;
|
|
48
|
-
url: z.ZodString;
|
|
49
|
-
date: z.ZodDate;
|
|
50
|
-
source: z.ZodObject<{
|
|
51
|
-
url: z.ZodString;
|
|
52
|
-
title: z.ZodString;
|
|
53
|
-
}, "strip", z.ZodTypeAny, {
|
|
54
|
-
url: string;
|
|
55
|
-
title: string;
|
|
56
|
-
}, {
|
|
57
|
-
url: string;
|
|
58
|
-
title: string;
|
|
59
|
-
}>;
|
|
60
|
-
preview: z.ZodOptional<z.ZodString>;
|
|
61
|
-
}, "strip", z.ZodTypeAny, {
|
|
62
|
-
url: string;
|
|
63
|
-
title: string;
|
|
64
|
-
date: Date;
|
|
65
|
-
source: {
|
|
66
|
-
url: string;
|
|
67
|
-
title: string;
|
|
68
|
-
};
|
|
69
|
-
preview?: string | undefined;
|
|
70
|
-
}, {
|
|
71
|
-
url: string;
|
|
72
|
-
title: string;
|
|
73
|
-
date: Date;
|
|
74
|
-
source: {
|
|
75
|
-
url: string;
|
|
76
|
-
title: string;
|
|
77
|
-
};
|
|
78
|
-
preview?: string | undefined;
|
|
79
|
-
}>;
|
|
80
|
-
export type Result = z.infer<typeof ResultSchema>;
|
|
81
|
-
declare const ResultSchema: z.ZodArray<z.ZodObject<{
|
|
82
|
-
title: z.ZodString;
|
|
83
|
-
url: z.ZodString;
|
|
84
|
-
date: z.ZodDate;
|
|
85
|
-
source: z.ZodObject<{
|
|
86
|
-
url: z.ZodString;
|
|
87
|
-
title: z.ZodString;
|
|
88
|
-
}, "strip", z.ZodTypeAny, {
|
|
89
|
-
url: string;
|
|
90
|
-
title: string;
|
|
91
|
-
}, {
|
|
92
|
-
url: string;
|
|
93
|
-
title: string;
|
|
94
|
-
}>;
|
|
95
|
-
preview: z.ZodOptional<z.ZodString>;
|
|
96
|
-
}, "strip", z.ZodTypeAny, {
|
|
97
|
-
url: string;
|
|
98
|
-
title: string;
|
|
99
|
-
date: Date;
|
|
100
|
-
source: {
|
|
101
|
-
url: string;
|
|
102
|
-
title: string;
|
|
103
|
-
};
|
|
104
|
-
preview?: string | undefined;
|
|
105
|
-
}, {
|
|
106
|
-
url: string;
|
|
107
|
-
title: string;
|
|
108
|
-
date: Date;
|
|
109
|
-
source: {
|
|
110
|
-
url: string;
|
|
111
|
-
title: string;
|
|
112
|
-
};
|
|
113
|
-
preview?: string | undefined;
|
|
114
|
-
}>, "many">;
|
|
115
|
-
export type CacheEntry = z.infer<typeof CacheEntrySchema>;
|
|
116
|
-
export declare const CacheEntrySchema: z.ZodObject<{
|
|
117
|
-
timestamp: z.ZodDate;
|
|
118
|
-
data: z.ZodObject<{
|
|
119
|
-
title: z.ZodString;
|
|
120
|
-
url: z.ZodString;
|
|
121
|
-
date: z.ZodDate;
|
|
122
|
-
source: z.ZodObject<{
|
|
123
|
-
url: z.ZodString;
|
|
124
|
-
title: z.ZodString;
|
|
125
|
-
}, "strip", z.ZodTypeAny, {
|
|
126
|
-
url: string;
|
|
127
|
-
title: string;
|
|
128
|
-
}, {
|
|
129
|
-
url: string;
|
|
130
|
-
title: string;
|
|
131
|
-
}>;
|
|
132
|
-
preview: z.ZodOptional<z.ZodString>;
|
|
133
|
-
}, "strip", z.ZodTypeAny, {
|
|
134
|
-
url: string;
|
|
135
|
-
title: string;
|
|
136
|
-
date: Date;
|
|
137
|
-
source: {
|
|
138
|
-
url: string;
|
|
139
|
-
title: string;
|
|
140
|
-
};
|
|
141
|
-
preview?: string | undefined;
|
|
142
|
-
}, {
|
|
143
|
-
url: string;
|
|
144
|
-
title: string;
|
|
145
|
-
date: Date;
|
|
146
|
-
source: {
|
|
147
|
-
url: string;
|
|
148
|
-
title: string;
|
|
149
|
-
};
|
|
150
|
-
preview?: string | undefined;
|
|
151
|
-
}>;
|
|
152
|
-
}, "strip", z.ZodTypeAny, {
|
|
153
|
-
timestamp: Date;
|
|
154
|
-
data: {
|
|
155
|
-
url: string;
|
|
156
|
-
title: string;
|
|
157
|
-
date: Date;
|
|
158
|
-
source: {
|
|
159
|
-
url: string;
|
|
160
|
-
title: string;
|
|
161
|
-
};
|
|
162
|
-
preview?: string | undefined;
|
|
163
|
-
};
|
|
164
|
-
}, {
|
|
165
|
-
timestamp: Date;
|
|
166
|
-
data: {
|
|
167
|
-
url: string;
|
|
168
|
-
title: string;
|
|
169
|
-
date: Date;
|
|
170
|
-
source: {
|
|
171
|
-
url: string;
|
|
172
|
-
title: string;
|
|
173
|
-
};
|
|
174
|
-
preview?: string | undefined;
|
|
175
|
-
};
|
|
176
|
-
}>;
|
|
177
|
-
export type Cache = z.infer<typeof CacheSchema>;
|
|
178
|
-
export declare const CacheSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
179
|
-
timestamp: z.ZodDate;
|
|
180
|
-
data: z.ZodObject<{
|
|
181
|
-
title: z.ZodString;
|
|
182
|
-
url: z.ZodString;
|
|
183
|
-
date: z.ZodDate;
|
|
184
|
-
source: z.ZodObject<{
|
|
185
|
-
url: z.ZodString;
|
|
186
|
-
title: z.ZodString;
|
|
187
|
-
}, "strip", z.ZodTypeAny, {
|
|
188
|
-
url: string;
|
|
189
|
-
title: string;
|
|
190
|
-
}, {
|
|
191
|
-
url: string;
|
|
192
|
-
title: string;
|
|
193
|
-
}>;
|
|
194
|
-
preview: z.ZodOptional<z.ZodString>;
|
|
195
|
-
}, "strip", z.ZodTypeAny, {
|
|
196
|
-
url: string;
|
|
197
|
-
title: string;
|
|
198
|
-
date: Date;
|
|
199
|
-
source: {
|
|
200
|
-
url: string;
|
|
201
|
-
title: string;
|
|
202
|
-
};
|
|
203
|
-
preview?: string | undefined;
|
|
204
|
-
}, {
|
|
205
|
-
url: string;
|
|
206
|
-
title: string;
|
|
207
|
-
date: Date;
|
|
208
|
-
source: {
|
|
209
|
-
url: string;
|
|
210
|
-
title: string;
|
|
211
|
-
};
|
|
212
|
-
preview?: string | undefined;
|
|
213
|
-
}>;
|
|
214
|
-
}, "strip", z.ZodTypeAny, {
|
|
215
|
-
timestamp: Date;
|
|
216
|
-
data: {
|
|
217
|
-
url: string;
|
|
218
|
-
title: string;
|
|
219
|
-
date: Date;
|
|
220
|
-
source: {
|
|
221
|
-
url: string;
|
|
222
|
-
title: string;
|
|
223
|
-
};
|
|
224
|
-
preview?: string | undefined;
|
|
225
|
-
};
|
|
226
|
-
}, {
|
|
227
|
-
timestamp: Date;
|
|
228
|
-
data: {
|
|
229
|
-
url: string;
|
|
230
|
-
title: string;
|
|
231
|
-
date: Date;
|
|
232
|
-
source: {
|
|
233
|
-
url: string;
|
|
234
|
-
title: string;
|
|
235
|
-
};
|
|
236
|
-
preview?: string | undefined;
|
|
237
|
-
};
|
|
238
|
-
}>>;
|
|
239
|
-
export declare const FeedEntrySchema: z.ZodEffects<z.ZodObject<{
|
|
240
|
-
title: z.ZodString;
|
|
241
|
-
link: z.ZodString;
|
|
242
|
-
isoDate: z.ZodOptional<z.ZodDate>;
|
|
243
|
-
pubDate: z.ZodOptional<z.ZodDate>;
|
|
244
|
-
content: z.ZodOptional<z.ZodString>;
|
|
245
|
-
contentSnippet: z.ZodOptional<z.ZodString>;
|
|
246
|
-
"content:encoded": z.ZodOptional<z.ZodString>;
|
|
247
|
-
description: z.ZodOptional<z.ZodString>;
|
|
248
|
-
}, "strip", z.ZodTypeAny, {
|
|
249
|
-
title: string;
|
|
250
|
-
link: string;
|
|
251
|
-
isoDate?: Date | undefined;
|
|
252
|
-
pubDate?: Date | undefined;
|
|
253
|
-
content?: string | undefined;
|
|
254
|
-
contentSnippet?: string | undefined;
|
|
255
|
-
"content:encoded"?: string | undefined;
|
|
256
|
-
description?: string | undefined;
|
|
257
|
-
}, {
|
|
258
|
-
title: string;
|
|
259
|
-
link: string;
|
|
260
|
-
isoDate?: Date | undefined;
|
|
261
|
-
pubDate?: Date | undefined;
|
|
262
|
-
content?: string | undefined;
|
|
263
|
-
contentSnippet?: string | undefined;
|
|
264
|
-
"content:encoded"?: string | undefined;
|
|
265
|
-
description?: string | undefined;
|
|
266
|
-
}>, {
|
|
267
|
-
title: string;
|
|
268
|
-
link: string;
|
|
269
|
-
date: Date;
|
|
270
|
-
content: string | undefined;
|
|
271
|
-
contentSnippet: string | undefined;
|
|
272
|
-
description: string | undefined;
|
|
273
|
-
"content:encoded": string | undefined;
|
|
274
|
-
}, {
|
|
275
|
-
title: string;
|
|
276
|
-
link: string;
|
|
277
|
-
isoDate?: Date | undefined;
|
|
278
|
-
pubDate?: Date | undefined;
|
|
279
|
-
content?: string | undefined;
|
|
280
|
-
contentSnippet?: string | undefined;
|
|
281
|
-
"content:encoded"?: string | undefined;
|
|
282
|
-
description?: string | undefined;
|
|
283
|
-
}>;
|
|
284
|
-
export {};
|
package/dist/types.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
const SourceSchema = z.object({
|
|
3
|
-
url: z.string(),
|
|
4
|
-
title: z.string(),
|
|
5
|
-
});
|
|
6
|
-
const ConfigurationSchema = z.object({
|
|
7
|
-
sources: SourceSchema.array(),
|
|
8
|
-
number: z.number(),
|
|
9
|
-
cache_duration_minutes: z.number(),
|
|
10
|
-
truncate: z.number(),
|
|
11
|
-
});
|
|
12
|
-
const ResultEntrySchema = z.object({
|
|
13
|
-
title: z.string(),
|
|
14
|
-
url: z.string(),
|
|
15
|
-
date: z.coerce.date(),
|
|
16
|
-
source: SourceSchema,
|
|
17
|
-
preview: z.string().optional(),
|
|
18
|
-
});
|
|
19
|
-
const ResultSchema = z.array(ResultEntrySchema);
|
|
20
|
-
export const CacheEntrySchema = z.object({
|
|
21
|
-
timestamp: z.coerce.date(),
|
|
22
|
-
data: ResultEntrySchema,
|
|
23
|
-
});
|
|
24
|
-
export const CacheSchema = z.record(CacheEntrySchema);
|
|
25
|
-
export const FeedEntrySchema = z
|
|
26
|
-
.object({
|
|
27
|
-
title: z.string(),
|
|
28
|
-
link: z.string(),
|
|
29
|
-
isoDate: z.coerce.date().optional(),
|
|
30
|
-
pubDate: z.coerce.date().optional(),
|
|
31
|
-
content: z.string().optional(),
|
|
32
|
-
contentSnippet: z.string().optional(),
|
|
33
|
-
"content:encoded": z.string().optional(),
|
|
34
|
-
description: z.string().optional(),
|
|
35
|
-
})
|
|
36
|
-
.transform((entry) => {
|
|
37
|
-
const date = entry.isoDate ?? entry.pubDate;
|
|
38
|
-
if (!date) {
|
|
39
|
-
throw new Error("no date found in feed entry");
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
title: entry.title,
|
|
43
|
-
link: entry.link,
|
|
44
|
-
date,
|
|
45
|
-
content: entry.content,
|
|
46
|
-
contentSnippet: entry.contentSnippet,
|
|
47
|
-
description: entry.description,
|
|
48
|
-
"content:encoded": entry["content:encoded"],
|
|
49
|
-
};
|
|
50
|
-
});
|
package/eslint.config.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import eslint from "@eslint/js";
|
|
2
|
-
import tseslint from "typescript-eslint";
|
|
3
|
-
|
|
4
|
-
export default tseslint.config(
|
|
5
|
-
eslint.configs.recommended,
|
|
6
|
-
...tseslint.configs.strictTypeChecked,
|
|
7
|
-
...tseslint.configs.stylisticTypeChecked,
|
|
8
|
-
{
|
|
9
|
-
languageOptions: {
|
|
10
|
-
parserOptions: {
|
|
11
|
-
project: true,
|
|
12
|
-
tsconfigRootDir: import.meta.dirname,
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
ignores: ["*.js"],
|
|
16
|
-
},
|
|
17
|
-
);
|
package/src/cache.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import * as R from "remeda";
|
|
2
|
-
import type { Configuration, Cache, Result, ResultEntry, CacheEntry, Source } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export async function runWithCache(config: Configuration, cache: Cache): Promise<[Result, Cache]> {
|
|
5
|
-
const promises = R.pipe(
|
|
6
|
-
config.sources,
|
|
7
|
-
R.map((source) => fetchWithCache(source, cache, config)),
|
|
8
|
-
R.filter((result) => result !== undefined),
|
|
9
|
-
);
|
|
10
|
-
const results = await Promise.all(promises);
|
|
11
|
-
|
|
12
|
-
const definedResults = results.filter((result) => result !== undefined) as ResultEntry[];
|
|
13
|
-
|
|
14
|
-
const updatedCache: Cache = R.pipe(
|
|
15
|
-
definedResults,
|
|
16
|
-
R.map((result): [string, CacheEntry] => [result.source.url, { timestamp: new Date(), data: result }]),
|
|
17
|
-
R.fromEntries(),
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
const topResults = R.pipe(
|
|
21
|
-
definedResults,
|
|
22
|
-
R.sortBy((result) => result.date.getTime()),
|
|
23
|
-
R.reverse(),
|
|
24
|
-
R.take(config.number),
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
return [topResults, updatedCache];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function fetchWithCache(
|
|
31
|
-
source: Source,
|
|
32
|
-
cache: Cache,
|
|
33
|
-
config: Configuration,
|
|
34
|
-
): Promise<ResultEntry | undefined> {
|
|
35
|
-
const cacheEntry = cache[source.url];
|
|
36
|
-
if (cacheEntry) {
|
|
37
|
-
const now = new Date();
|
|
38
|
-
if (now.getTime() - cacheEntry.timestamp.getTime() < config.cache_duration_minutes * 60 * 1000) {
|
|
39
|
-
console.log(`Cache entry found for ${source.url}`);
|
|
40
|
-
return Promise.resolve(cacheEntry.data);
|
|
41
|
-
} else {
|
|
42
|
-
console.log(`Cache entry for ${source.url} is too old`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.log(`No cache entry for ${source.url}`);
|
|
47
|
-
return fetch(source, config.truncate);
|
|
48
|
-
}
|
package/src/fetch.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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 } from "./types.js";
|
|
5
|
-
import * as R from "remeda";
|
|
6
|
-
|
|
7
|
-
export async function fetch(source: Source, length: number): Promise<ResultEntry | undefined> {
|
|
8
|
-
const parser = new Parser();
|
|
9
|
-
try {
|
|
10
|
-
const feed = await parser.parseURL(source.url);
|
|
11
|
-
|
|
12
|
-
const firstItem = R.pipe(
|
|
13
|
-
feed.items,
|
|
14
|
-
R.map((item) => FeedEntrySchema.parse(item)),
|
|
15
|
-
R.sortBy((item) => new Date(item.date).getTime()),
|
|
16
|
-
R.reverse(),
|
|
17
|
-
R.first(),
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
if (!firstItem) {
|
|
21
|
-
throw new Error("no items found in feed");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const preview =
|
|
25
|
-
firstItem.contentSnippet ?? firstItem.content ?? firstItem.description ?? firstItem["content:encoded"];
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
title: firstItem.title,
|
|
29
|
-
url: firstItem.link,
|
|
30
|
-
date: new Date(firstItem.date),
|
|
31
|
-
source,
|
|
32
|
-
preview: preview ? truncate(sanitizeHtml(preview), length) : undefined,
|
|
33
|
-
};
|
|
34
|
-
} catch (e) {
|
|
35
|
-
console.error(`Error fetching ${source.url}: ${e as string}`);
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { runWithCache } from "./cache.js";
|
|
2
|
-
import { type Configuration, type Result, type Cache, CacheSchema } from "./types.js";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
|
|
5
|
-
export async function run(config: Configuration): Promise<Result> {
|
|
6
|
-
const cacheFilename = "cache.json";
|
|
7
|
-
const currentDir = process.cwd();
|
|
8
|
-
const fullFilename = `${currentDir}/${cacheFilename}`;
|
|
9
|
-
|
|
10
|
-
let cacheObject: Cache = {};
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const cacheFile = await fs.readFile(fullFilename);
|
|
14
|
-
cacheObject = CacheSchema.parse(JSON.parse(cacheFile.toString()));
|
|
15
|
-
} catch (e) {
|
|
16
|
-
console.error("Error reading cache file:", e);
|
|
17
|
-
throw e;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const [result, updatedCache] = await runWithCache(config, cacheObject);
|
|
21
|
-
|
|
22
|
-
// write the updated cache to cache.json
|
|
23
|
-
await fs.writeFile(fullFilename, JSON.stringify(updatedCache));
|
|
24
|
-
|
|
25
|
-
return result;
|
|
26
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export type Source = z.infer<typeof SourceSchema>;
|
|
4
|
-
const SourceSchema = z.object({
|
|
5
|
-
url: z.string(),
|
|
6
|
-
title: z.string(),
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
export type Configuration = z.infer<typeof ConfigurationSchema>;
|
|
10
|
-
const ConfigurationSchema = z.object({
|
|
11
|
-
sources: SourceSchema.array(),
|
|
12
|
-
number: z.number(),
|
|
13
|
-
cache_duration_minutes: z.number(),
|
|
14
|
-
truncate: z.number(),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
export type ResultEntry = z.infer<typeof ResultEntrySchema>;
|
|
18
|
-
const ResultEntrySchema = z.object({
|
|
19
|
-
title: z.string(),
|
|
20
|
-
url: z.string(),
|
|
21
|
-
date: z.coerce.date(),
|
|
22
|
-
source: SourceSchema,
|
|
23
|
-
preview: z.string().optional(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export type Result = z.infer<typeof ResultSchema>;
|
|
27
|
-
const ResultSchema = z.array(ResultEntrySchema);
|
|
28
|
-
|
|
29
|
-
export type CacheEntry = z.infer<typeof CacheEntrySchema>;
|
|
30
|
-
export const CacheEntrySchema = z.object({
|
|
31
|
-
timestamp: z.coerce.date(),
|
|
32
|
-
data: ResultEntrySchema,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export type Cache = z.infer<typeof CacheSchema>;
|
|
36
|
-
export const CacheSchema = z.record(CacheEntrySchema);
|
|
37
|
-
|
|
38
|
-
export const FeedEntrySchema = z
|
|
39
|
-
.object({
|
|
40
|
-
title: z.string(),
|
|
41
|
-
link: z.string(),
|
|
42
|
-
isoDate: z.coerce.date().optional(),
|
|
43
|
-
pubDate: z.coerce.date().optional(),
|
|
44
|
-
content: z.string().optional(),
|
|
45
|
-
contentSnippet: z.string().optional(),
|
|
46
|
-
"content:encoded": z.string().optional(),
|
|
47
|
-
description: z.string().optional(),
|
|
48
|
-
})
|
|
49
|
-
.transform((entry) => {
|
|
50
|
-
const date = entry.isoDate ?? entry.pubDate;
|
|
51
|
-
if (!date) {
|
|
52
|
-
throw new Error("no date found in feed entry");
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
title: entry.title,
|
|
56
|
-
link: entry.link,
|
|
57
|
-
date,
|
|
58
|
-
content: entry.content,
|
|
59
|
-
contentSnippet: entry.contentSnippet,
|
|
60
|
-
description: entry.description,
|
|
61
|
-
"content:encoded": entry["content:encoded"],
|
|
62
|
-
};
|
|
63
|
-
});
|