webring 0.3.0 → 1.0.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 CHANGED
@@ -5,13 +5,26 @@ 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
- ## [0.3.0](https://github.com/shepherdjerred/webring/compare/v0.2.0...v0.3.0) (2024-06-03)
8
+ ## [1.0.1](https://github.com/shepherdjerred/webring/compare/v1.0.0...v1.0.1) (2024-06-03)
9
9
 
10
10
 
11
- ### Features
11
+ ### Bug Fixes
12
+
13
+ * add tests, fix import ([3f811a1](https://github.com/shepherdjerred/webring/commit/3f811a18fae9186795f5d34cb0d4bbdd20f2a5df))
14
+
15
+ ## [1.0.0](https://github.com/shepherdjerred/webring/compare/v0.3.0...v1.0.0) (2024-06-03)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
12
19
 
13
- * allow filename to be configurable ([cc7bb5f](https://github.com/shepherdjerred/webring/commit/cc7bb5f3139f306952d03568fc63cc9fcbfaad5e))
20
+ * add link
14
21
 
15
- ## [Unreleased]
22
+ ### Documentation
23
+
24
+ * add link ([1a5d327](https://github.com/shepherdjerred/webring/commit/1a5d327a002785809e84aab70b19d18dd135f78b))
25
+
26
+ ## [0.3.0](https://github.com/shepherdjerred/webring/compare/v0.2.0...v0.3.0) (2024-06-03)
27
+
28
+ ### Features
16
29
 
17
- * Initial release
30
+ - allow filename to be configurable ([cc7bb5f](https://github.com/shepherdjerred/webring/commit/cc7bb5f3139f306952d03568fc63cc9fcbfaad5e))
package/README.md CHANGED
@@ -51,3 +51,5 @@ result.map((entry) => {
51
51
  console.log(entry);
52
52
  });
53
53
  ```
54
+
55
+ I use this with Astro on my [personal website](https://github.com/shepherdjerred/sjer.red/blob/main/src/components/BlogWebring.astro#L17-L22).
@@ -0,0 +1,3 @@
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 ADDED
@@ -0,0 +1,25 @@
1
+ import * as R from "remeda";
2
+ import { fetch } from "./fetch.js";
3
+ export async function runWithCache(config, cache) {
4
+ const promises = R.pipe(config.sources, R.map((source) => fetchWithCache(source, cache, config)));
5
+ const results = await Promise.all(promises);
6
+ const definedResults = results.filter((result) => result !== undefined);
7
+ const updatedCache = R.pipe(definedResults, R.map((result) => [result.source.url, { timestamp: new Date(), data: result }]), R.fromEntries());
8
+ const topResults = R.pipe(definedResults, R.sortBy((result) => result.date.getTime()), R.reverse(), R.take(config.number));
9
+ return [topResults, updatedCache];
10
+ }
11
+ export async function fetchWithCache(source, cache, config) {
12
+ const cacheEntry = cache[source.url];
13
+ if (cacheEntry) {
14
+ const now = new Date();
15
+ if (now.getTime() - cacheEntry.timestamp.getTime() < config.cache_duration_minutes * 60 * 1000) {
16
+ console.log(`Cache entry found for ${source.url}`);
17
+ return Promise.resolve(cacheEntry.data);
18
+ }
19
+ else {
20
+ console.log(`Cache entry for ${source.url} is too old`);
21
+ }
22
+ }
23
+ console.log(`No cache entry for ${source.url}`);
24
+ return fetch(source, config.truncate);
25
+ }
@@ -0,0 +1,2 @@
1
+ import { type Source, type ResultEntry } from "./types.js";
2
+ export declare function fetch(source: Source, length: number): Promise<ResultEntry | undefined>;
package/dist/fetch.js ADDED
@@ -0,0 +1,29 @@
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
+ // for some reason, TypeScript does not infer the type of the default export correctly
7
+ const truncateFn = truncate;
8
+ export async function fetch(source, length) {
9
+ const parser = new Parser();
10
+ try {
11
+ const feed = await parser.parseURL(source.url);
12
+ const firstItem = R.pipe(feed.items, R.map((item) => FeedEntrySchema.parse(item)), R.sortBy((item) => new Date(item.date).getTime()), R.reverse(), R.first());
13
+ if (!firstItem) {
14
+ throw new Error("no items found in feed");
15
+ }
16
+ const preview = firstItem.contentSnippet ?? firstItem.content ?? firstItem.description ?? firstItem["content:encoded"];
17
+ return {
18
+ title: firstItem.title,
19
+ url: firstItem.link,
20
+ date: new Date(firstItem.date),
21
+ source,
22
+ preview: preview ? truncateFn(sanitizeHtml(preview), length) : undefined,
23
+ };
24
+ }
25
+ catch (e) {
26
+ console.error(`Error fetching ${source.url}: ${e}`);
27
+ return undefined;
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ import { type Configuration, type Result } from "./types.js";
2
+ export declare function run(config: Configuration): Promise<Result>;
3
+ export * from "./types.js";
4
+ export * from "./cache.js";
5
+ export * from "./fetch.js";
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
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 = config.cache_file;
6
+ let cacheObject = {};
7
+ // check if cache.json exists
8
+ try {
9
+ await fs.access(cacheFilename);
10
+ }
11
+ catch (e) {
12
+ await fs.writeFile(cacheFilename, JSON.stringify({}));
13
+ }
14
+ try {
15
+ const cacheFile = await fs.readFile(cacheFilename);
16
+ cacheObject = CacheSchema.parse(JSON.parse(cacheFile.toString()));
17
+ }
18
+ catch (e) {
19
+ console.error("Error reading cache file:", e);
20
+ throw e;
21
+ }
22
+ const [result, updatedCache] = await runWithCache(config, cacheObject);
23
+ // write the updated cache to cache.json
24
+ await fs.writeFile(cacheFilename, JSON.stringify(updatedCache));
25
+ return result;
26
+ }
27
+ export * from "./types.js";
28
+ export * from "./cache.js";
29
+ export * from "./fetch.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ // sum.test.js
2
+ import { expect, test } from "vitest";
3
+ import { run } from "./index.js";
4
+ test("it should work", async () => {
5
+ const config = {
6
+ sources: [
7
+ {
8
+ title: "Jerred Shepherd",
9
+ url: "https://sjer.red/rss.xml",
10
+ },
11
+ ],
12
+ number: 1,
13
+ cache_duration_minutes: 0,
14
+ truncate: 300,
15
+ cache_file: "cache.json",
16
+ };
17
+ const result = await run(config);
18
+ expect(result).toMatchSnapshot();
19
+ });
@@ -0,0 +1,287 @@
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.ZodDefault<z.ZodNumber>;
26
+ cache_duration_minutes: z.ZodDefault<z.ZodNumber>;
27
+ truncate: z.ZodDefault<z.ZodNumber>;
28
+ cache_file: z.ZodDefault<z.ZodString>;
29
+ }, "strip", z.ZodTypeAny, {
30
+ number: number;
31
+ sources: {
32
+ url: string;
33
+ title: string;
34
+ }[];
35
+ cache_duration_minutes: number;
36
+ truncate: number;
37
+ cache_file: string;
38
+ }, {
39
+ sources: {
40
+ url: string;
41
+ title: string;
42
+ }[];
43
+ number?: number | undefined;
44
+ cache_duration_minutes?: number | undefined;
45
+ truncate?: number | undefined;
46
+ cache_file?: string | undefined;
47
+ }>;
48
+ export type ResultEntry = z.infer<typeof ResultEntrySchema>;
49
+ declare const ResultEntrySchema: z.ZodObject<{
50
+ title: z.ZodString;
51
+ url: z.ZodString;
52
+ date: z.ZodDate;
53
+ source: z.ZodObject<{
54
+ url: z.ZodString;
55
+ title: z.ZodString;
56
+ }, "strip", z.ZodTypeAny, {
57
+ url: string;
58
+ title: string;
59
+ }, {
60
+ url: string;
61
+ title: string;
62
+ }>;
63
+ preview: z.ZodOptional<z.ZodString>;
64
+ }, "strip", z.ZodTypeAny, {
65
+ url: string;
66
+ title: string;
67
+ date: Date;
68
+ source: {
69
+ url: string;
70
+ title: string;
71
+ };
72
+ preview?: string | undefined;
73
+ }, {
74
+ url: string;
75
+ title: string;
76
+ date: Date;
77
+ source: {
78
+ url: string;
79
+ title: string;
80
+ };
81
+ preview?: string | undefined;
82
+ }>;
83
+ export type Result = z.infer<typeof ResultSchema>;
84
+ declare const ResultSchema: z.ZodArray<z.ZodObject<{
85
+ title: z.ZodString;
86
+ url: z.ZodString;
87
+ date: z.ZodDate;
88
+ source: z.ZodObject<{
89
+ url: z.ZodString;
90
+ title: z.ZodString;
91
+ }, "strip", z.ZodTypeAny, {
92
+ url: string;
93
+ title: string;
94
+ }, {
95
+ url: string;
96
+ title: string;
97
+ }>;
98
+ preview: z.ZodOptional<z.ZodString>;
99
+ }, "strip", z.ZodTypeAny, {
100
+ url: string;
101
+ title: string;
102
+ date: Date;
103
+ source: {
104
+ url: string;
105
+ title: string;
106
+ };
107
+ preview?: string | undefined;
108
+ }, {
109
+ url: string;
110
+ title: string;
111
+ date: Date;
112
+ source: {
113
+ url: string;
114
+ title: string;
115
+ };
116
+ preview?: string | undefined;
117
+ }>, "many">;
118
+ export type CacheEntry = z.infer<typeof CacheEntrySchema>;
119
+ export declare const CacheEntrySchema: z.ZodObject<{
120
+ timestamp: z.ZodDate;
121
+ data: z.ZodObject<{
122
+ title: z.ZodString;
123
+ url: z.ZodString;
124
+ date: z.ZodDate;
125
+ source: z.ZodObject<{
126
+ url: z.ZodString;
127
+ title: z.ZodString;
128
+ }, "strip", z.ZodTypeAny, {
129
+ url: string;
130
+ title: string;
131
+ }, {
132
+ url: string;
133
+ title: string;
134
+ }>;
135
+ preview: z.ZodOptional<z.ZodString>;
136
+ }, "strip", z.ZodTypeAny, {
137
+ url: string;
138
+ title: string;
139
+ date: Date;
140
+ source: {
141
+ url: string;
142
+ title: string;
143
+ };
144
+ preview?: string | undefined;
145
+ }, {
146
+ url: string;
147
+ title: string;
148
+ date: Date;
149
+ source: {
150
+ url: string;
151
+ title: string;
152
+ };
153
+ preview?: string | undefined;
154
+ }>;
155
+ }, "strip", z.ZodTypeAny, {
156
+ timestamp: Date;
157
+ data: {
158
+ url: string;
159
+ title: string;
160
+ date: Date;
161
+ source: {
162
+ url: string;
163
+ title: string;
164
+ };
165
+ preview?: string | undefined;
166
+ };
167
+ }, {
168
+ timestamp: Date;
169
+ data: {
170
+ url: string;
171
+ title: string;
172
+ date: Date;
173
+ source: {
174
+ url: string;
175
+ title: string;
176
+ };
177
+ preview?: string | undefined;
178
+ };
179
+ }>;
180
+ export type Cache = z.infer<typeof CacheSchema>;
181
+ export declare const CacheSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
182
+ timestamp: z.ZodDate;
183
+ data: z.ZodObject<{
184
+ title: z.ZodString;
185
+ url: z.ZodString;
186
+ date: z.ZodDate;
187
+ source: z.ZodObject<{
188
+ url: z.ZodString;
189
+ title: z.ZodString;
190
+ }, "strip", z.ZodTypeAny, {
191
+ url: string;
192
+ title: string;
193
+ }, {
194
+ url: string;
195
+ title: string;
196
+ }>;
197
+ preview: z.ZodOptional<z.ZodString>;
198
+ }, "strip", z.ZodTypeAny, {
199
+ url: string;
200
+ title: string;
201
+ date: Date;
202
+ source: {
203
+ url: string;
204
+ title: string;
205
+ };
206
+ preview?: string | undefined;
207
+ }, {
208
+ url: string;
209
+ title: string;
210
+ date: Date;
211
+ source: {
212
+ url: string;
213
+ title: string;
214
+ };
215
+ preview?: string | undefined;
216
+ }>;
217
+ }, "strip", z.ZodTypeAny, {
218
+ timestamp: Date;
219
+ data: {
220
+ url: string;
221
+ title: string;
222
+ date: Date;
223
+ source: {
224
+ url: string;
225
+ title: string;
226
+ };
227
+ preview?: string | undefined;
228
+ };
229
+ }, {
230
+ timestamp: Date;
231
+ data: {
232
+ url: string;
233
+ title: string;
234
+ date: Date;
235
+ source: {
236
+ url: string;
237
+ title: string;
238
+ };
239
+ preview?: string | undefined;
240
+ };
241
+ }>>;
242
+ export declare const FeedEntrySchema: z.ZodEffects<z.ZodObject<{
243
+ title: z.ZodString;
244
+ link: z.ZodString;
245
+ isoDate: z.ZodOptional<z.ZodDate>;
246
+ pubDate: z.ZodOptional<z.ZodDate>;
247
+ content: z.ZodOptional<z.ZodString>;
248
+ contentSnippet: z.ZodOptional<z.ZodString>;
249
+ "content:encoded": z.ZodOptional<z.ZodString>;
250
+ description: z.ZodOptional<z.ZodString>;
251
+ }, "strip", z.ZodTypeAny, {
252
+ title: string;
253
+ link: string;
254
+ isoDate?: Date | undefined;
255
+ pubDate?: Date | undefined;
256
+ content?: string | undefined;
257
+ contentSnippet?: string | undefined;
258
+ "content:encoded"?: string | undefined;
259
+ description?: string | undefined;
260
+ }, {
261
+ title: string;
262
+ link: string;
263
+ isoDate?: Date | undefined;
264
+ pubDate?: Date | undefined;
265
+ content?: string | undefined;
266
+ contentSnippet?: string | undefined;
267
+ "content:encoded"?: string | undefined;
268
+ description?: string | undefined;
269
+ }>, {
270
+ title: string;
271
+ link: string;
272
+ date: Date;
273
+ content: string | undefined;
274
+ contentSnippet: string | undefined;
275
+ description: string | undefined;
276
+ "content:encoded": string | undefined;
277
+ }, {
278
+ title: string;
279
+ link: string;
280
+ isoDate?: Date | undefined;
281
+ pubDate?: Date | undefined;
282
+ content?: string | undefined;
283
+ contentSnippet?: string | undefined;
284
+ "content:encoded"?: string | undefined;
285
+ description?: string | undefined;
286
+ }>;
287
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ const SourceSchema = z.object({
3
+ // the url of the feed
4
+ url: z.string(),
5
+ // a title for the feed
6
+ title: z.string(),
7
+ });
8
+ const ConfigurationSchema = z.object({
9
+ // list of sources to fetch
10
+ sources: SourceSchema.array(),
11
+ // how many entries to return
12
+ number: z.number().default(3),
13
+ // how long to cache a results for
14
+ cache_duration_minutes: z.number().default(60),
15
+ // how many words to truncate the preview to
16
+ truncate: z.number().default(300),
17
+ cache_file: z.string().default("cache.json"),
18
+ });
19
+ const ResultEntrySchema = z.object({
20
+ title: z.string(),
21
+ url: z.string(),
22
+ date: z.coerce.date(),
23
+ source: SourceSchema,
24
+ preview: z.string().optional(),
25
+ });
26
+ const ResultSchema = z.array(ResultEntrySchema);
27
+ export const CacheEntrySchema = z.object({
28
+ timestamp: z.coerce.date(),
29
+ data: ResultEntrySchema,
30
+ });
31
+ export const CacheSchema = z.record(CacheEntrySchema);
32
+ export const FeedEntrySchema = z
33
+ .object({
34
+ title: z.string(),
35
+ link: z.string(),
36
+ isoDate: z.coerce.date().optional(),
37
+ pubDate: z.coerce.date().optional(),
38
+ content: z.string().optional(),
39
+ contentSnippet: z.string().optional(),
40
+ "content:encoded": z.string().optional(),
41
+ description: z.string().optional(),
42
+ })
43
+ .transform((entry) => {
44
+ const date = entry.isoDate ?? entry.pubDate;
45
+ if (!date) {
46
+ throw new Error("no date found in feed entry");
47
+ }
48
+ return {
49
+ title: entry.title,
50
+ link: entry.link,
51
+ date,
52
+ content: entry.content,
53
+ contentSnippet: entry.contentSnippet,
54
+ description: entry.description,
55
+ "content:encoded": entry["content:encoded"],
56
+ };
57
+ });
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "webring",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "1.0.1",
5
5
  "scripts": {
6
- "lint": "eslint .",
6
+ "prepare": "husky",
7
+ "lint": "eslint src",
7
8
  "build": "tsc",
8
- "test": "",
9
- "prepare": "husky"
9
+ "test": "vitest"
10
10
  },
11
11
  "main": "dist/index.js",
12
12
  "types": "dist/index.d.ts",
13
13
  "dependencies": {
14
- "remeda": "^2.0.0",
14
+ "remeda": "^2.0.1",
15
15
  "rss-parser": "^3.13.0",
16
16
  "sanitize-html": "^2.13.0",
17
17
  "truncate-html": "^1.1.1",
@@ -24,21 +24,30 @@
24
24
  "@tsconfig/node20": "^20.1.4",
25
25
  "@tsconfig/strictest": "^2.0.5",
26
26
  "@types/eslint__js": "^8.42.3",
27
- "@types/node": "^20.13.0",
27
+ "@types/node": "^20.14.0",
28
28
  "@types/sanitize-html": "^2.11.0",
29
29
  "@typescript-eslint/eslint-plugin": "^7.11.0",
30
30
  "@typescript-eslint/parser": "^7.11.0",
31
31
  "eslint": "^8.57.0",
32
32
  "husky": "^9.0.11",
33
33
  "lint-staged": "^15.2.5",
34
- "prettier": "^3.2.5",
34
+ "prettier": "^3.3.0",
35
35
  "typescript": "^5.4.5",
36
- "typescript-eslint": "^7.11.0"
36
+ "typescript-eslint": "^7.11.0",
37
+ "vitest": "^1.6.0"
37
38
  },
38
39
  "lint-staged": {
39
40
  "*.{ts,tsx}": "eslint --cache --fix",
40
41
  "*": "prettier --ignore-unknown --write"
41
42
  },
43
+ "commitlint": {
44
+ "extends": [
45
+ "@commitlint/config-conventional"
46
+ ]
47
+ },
48
+ "prettier": {
49
+ "printWidth": 120
50
+ },
42
51
  "files": [
43
52
  "dist",
44
53
  "package.json",