webring 1.5.1 → 1.6.0-dev.891
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 +104 -135
- package/bun.lock +565 -305
- package/dist/cache.d.ts +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +19 -16
- package/dist/cache.js.map +1 -1
- package/dist/fetch.d.ts +1 -1
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +28 -7
- package/dist/fetch.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -14
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +14 -14
- package/dist/index.test.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +7 -1
- package/dist/util.js.map +1 -1
- package/package.json +19 -28
- package/src/__snapshots__/index.test.ts.snap +7 -0
- package/src/cache.ts +39 -22
- package/src/fetch.ts +46 -10
- package/src/index.test.ts +16 -16
- package/src/index.ts +25 -17
- package/src/types.ts +1 -1
- package/src/util.ts +15 -6
- package/LICENSE +0 -674
package/src/fetch.ts
CHANGED
|
@@ -1,19 +1,46 @@
|
|
|
1
1
|
import Parser from "rss-parser";
|
|
2
2
|
import sanitizeHtml from "sanitize-html";
|
|
3
3
|
import * as truncateHtml from "truncate-html";
|
|
4
|
-
import {
|
|
4
|
+
import { z } from "zod/v3";
|
|
5
|
+
import {
|
|
6
|
+
type Source,
|
|
7
|
+
type ResultEntry,
|
|
8
|
+
FeedEntrySchema,
|
|
9
|
+
type Configuration,
|
|
10
|
+
} from "./types.ts";
|
|
5
11
|
import * as R from "remeda";
|
|
6
|
-
import { asyncMapFilterUndefined } from "./util.
|
|
12
|
+
import { asyncMapFilterUndefined } from "./util.ts";
|
|
7
13
|
|
|
8
14
|
// Handle ESM/CommonJS interop - truncate-html exports differently in different environments
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const TruncateFnSchema = z
|
|
16
|
+
.function()
|
|
17
|
+
.args(z.string(), z.number().optional())
|
|
18
|
+
.returns(z.string());
|
|
19
|
+
const TruncateModuleSchema = z.object({ default: TruncateFnSchema });
|
|
20
|
+
|
|
21
|
+
function truncate(html: string, length?: number): string {
|
|
22
|
+
const mod = truncateHtml as unknown;
|
|
23
|
+
const fnResult = TruncateFnSchema.safeParse(mod);
|
|
24
|
+
if (fnResult.success) {
|
|
25
|
+
return fnResult.data(html, length);
|
|
26
|
+
}
|
|
27
|
+
const modResult = TruncateModuleSchema.safeParse(mod);
|
|
28
|
+
if (modResult.success) {
|
|
29
|
+
return modResult.data.default(html, length);
|
|
30
|
+
}
|
|
31
|
+
throw new Error("truncate-html module could not be resolved");
|
|
32
|
+
}
|
|
11
33
|
|
|
12
34
|
export async function fetchAll(config: Configuration) {
|
|
13
|
-
return await asyncMapFilterUndefined(config.sources, (source) =>
|
|
35
|
+
return await asyncMapFilterUndefined(config.sources, (source) =>
|
|
36
|
+
fetch(source, config.truncate),
|
|
37
|
+
);
|
|
14
38
|
}
|
|
15
39
|
|
|
16
|
-
export async function fetch(
|
|
40
|
+
export async function fetch(
|
|
41
|
+
source: Source,
|
|
42
|
+
length: number,
|
|
43
|
+
): Promise<ResultEntry | undefined> {
|
|
17
44
|
const parser = new Parser();
|
|
18
45
|
|
|
19
46
|
try {
|
|
@@ -32,17 +59,26 @@ export async function fetch(source: Source, length: number): Promise<ResultEntry
|
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
const preview =
|
|
35
|
-
firstItem.contentSnippet ??
|
|
62
|
+
firstItem.contentSnippet ??
|
|
63
|
+
firstItem.content ??
|
|
64
|
+
firstItem.description ??
|
|
65
|
+
firstItem["content:encoded"];
|
|
36
66
|
|
|
37
67
|
return {
|
|
38
68
|
title: firstItem.title,
|
|
39
69
|
url: firstItem.link,
|
|
40
70
|
date: new Date(firstItem.date),
|
|
41
71
|
source,
|
|
42
|
-
preview:
|
|
72
|
+
preview:
|
|
73
|
+
preview !== undefined && preview !== ""
|
|
74
|
+
? truncate(
|
|
75
|
+
sanitizeHtml(preview, { parseStyleAttributes: false }),
|
|
76
|
+
length,
|
|
77
|
+
)
|
|
78
|
+
: undefined,
|
|
43
79
|
};
|
|
44
|
-
} catch (
|
|
45
|
-
console.error(`Error fetching ${source.url}: ${
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`Error fetching ${source.url}: ${String(error)}`);
|
|
46
82
|
return undefined;
|
|
47
83
|
}
|
|
48
84
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { expect, test } from "
|
|
2
|
-
import type { Configuration } from "./types.
|
|
3
|
-
import { run } from "./index.
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import { mkdtemp } from "fs/promises";
|
|
6
|
-
import
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { Configuration } from "./types.ts";
|
|
3
|
+
import { run } from "./index.ts";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { mkdtemp } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
7
|
import express from "express";
|
|
8
8
|
|
|
9
9
|
const app = express();
|
|
10
10
|
app.use(express.static("src/testdata"));
|
|
11
11
|
|
|
12
|
-
const port = Math.floor(Math.random() *
|
|
12
|
+
const port = Math.floor(Math.random() * 10_000) + 3000;
|
|
13
13
|
app.listen(port, () => {
|
|
14
|
-
console.
|
|
14
|
+
console.warn(`Test server listening at http://localhost:${port.toString()}`);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
18
18
|
|
|
19
|
-
function createUrl(
|
|
20
|
-
return `http://localhost:${port.toString()}/${
|
|
19
|
+
function createUrl(urlPath: string): string {
|
|
20
|
+
return `http://localhost:${port.toString()}/${urlPath}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function createSources(count: number): Configuration["sources"] {
|
|
24
24
|
return Array.from({ length: count }, (_, i) => ({
|
|
25
|
-
title: `rss ${i.toString()}`,
|
|
26
|
-
url: createUrl(`rss-${i.toString()}.xml`),
|
|
25
|
+
title: `rss ${(i + 1).toString()}`,
|
|
26
|
+
url: createUrl(`rss-${(i + 1).toString()}.xml`),
|
|
27
27
|
}));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
test("it should fetch an RSS feed without caching",
|
|
30
|
+
test("it should fetch an RSS feed without caching", async () => {
|
|
31
31
|
const config: Configuration = {
|
|
32
32
|
sources: createSources(19),
|
|
33
33
|
number: 1,
|
|
@@ -41,7 +41,7 @@ test("it should fetch an RSS feed without caching", { timeout: 30000 }, async ()
|
|
|
41
41
|
expect(string).toMatchSnapshot();
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test("it should fetch several RSS feeds",
|
|
44
|
+
test("it should fetch several RSS feeds", async () => {
|
|
45
45
|
const config: Configuration = {
|
|
46
46
|
sources: createSources(19),
|
|
47
47
|
number: 3,
|
|
@@ -55,7 +55,7 @@ test("it should fetch several RSS feeds", { timeout: 30000 }, async () => {
|
|
|
55
55
|
expect(string).toMatchSnapshot();
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("it should fetch an RSS feed with caching",
|
|
58
|
+
test("it should fetch an RSS feed with caching", async () => {
|
|
59
59
|
const config: Configuration = {
|
|
60
60
|
sources: createSources(19),
|
|
61
61
|
number: 1,
|
|
@@ -76,6 +76,6 @@ test("it should fetch an RSS feed with caching", { timeout: 30000 }, async () =>
|
|
|
76
76
|
// https://sdorra.dev/posts/2024-02-12-vitest-tmpdir
|
|
77
77
|
async function createTempDir() {
|
|
78
78
|
const ostmpdir = tmpdir();
|
|
79
|
-
const dir = join(ostmpdir, "unit-test-");
|
|
79
|
+
const dir = path.join(ostmpdir, "unit-test-");
|
|
80
80
|
return await mkdtemp(dir);
|
|
81
81
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
import * as R from "remeda";
|
|
2
|
-
import { fetchAllCached
|
|
3
|
-
import { fetchAll as fetchAllUncached } from "./fetch.
|
|
4
|
-
import {
|
|
2
|
+
import { fetchAllCached } from "./cache.ts";
|
|
3
|
+
import { fetchAll as fetchAllUncached } from "./fetch.ts";
|
|
4
|
+
import {
|
|
5
|
+
type Configuration,
|
|
6
|
+
type Result,
|
|
7
|
+
CachedConfigurationSchema,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export type { Configuration, Result, ResultEntry } from "./types.ts";
|
|
5
11
|
|
|
6
12
|
export async function run(config: Configuration): Promise<Result> {
|
|
7
13
|
const { success, data } = CachedConfigurationSchema.safeParse(config);
|
|
8
14
|
|
|
9
|
-
let
|
|
15
|
+
let fetched: Result;
|
|
10
16
|
if (success) {
|
|
11
|
-
console.
|
|
12
|
-
|
|
17
|
+
console.warn(`Using cache at ${data.cache.cache_file}.`);
|
|
18
|
+
fetched = await fetchAllCached(data);
|
|
13
19
|
} else {
|
|
14
|
-
console.
|
|
15
|
-
|
|
20
|
+
console.warn("Cache disabled.");
|
|
21
|
+
fetched = await fetchAllUncached(config);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
let results = R.pipe(
|
|
19
|
-
|
|
20
|
-
R.sortBy((
|
|
25
|
+
fetched,
|
|
26
|
+
R.sortBy((entry) => entry.date.getTime()),
|
|
21
27
|
R.reverse(),
|
|
22
|
-
R.filter((
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
R.filter((entry) => {
|
|
29
|
+
const filterFn = entry.source.filter;
|
|
30
|
+
if (
|
|
31
|
+
filterFn === undefined ||
|
|
32
|
+
entry.preview === undefined ||
|
|
33
|
+
entry.preview === ""
|
|
34
|
+
) {
|
|
26
35
|
return true;
|
|
27
36
|
}
|
|
37
|
+
return filterFn(entry.preview);
|
|
28
38
|
}),
|
|
29
39
|
);
|
|
30
40
|
|
|
31
41
|
// shuffle if wanted
|
|
32
|
-
if (config.shuffle) {
|
|
42
|
+
if (config.shuffle === true) {
|
|
33
43
|
results = R.shuffle(results);
|
|
34
44
|
}
|
|
35
45
|
|
|
@@ -38,5 +48,3 @@ export async function run(config: Configuration): Promise<Result> {
|
|
|
38
48
|
|
|
39
49
|
return results;
|
|
40
50
|
}
|
|
41
|
-
|
|
42
|
-
export * from "./types.js";
|
package/src/types.ts
CHANGED
package/src/util.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import * as R from "remeda";
|
|
2
2
|
|
|
3
3
|
// run an async map operation, filtering out undefined results
|
|
4
|
-
export async function asyncMapFilterUndefined<T, U>(
|
|
4
|
+
export async function asyncMapFilterUndefined<T, U>(
|
|
5
|
+
input: T[],
|
|
6
|
+
fn: (x: T) => Promise<U | undefined>,
|
|
7
|
+
): Promise<U[]> {
|
|
5
8
|
const results = await asyncMap(input, fn);
|
|
6
9
|
return filterUndefined(results);
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
export async function asyncMap<T, U>(
|
|
12
|
+
export async function asyncMap<T, U>(
|
|
13
|
+
input: T[],
|
|
14
|
+
fn: (x: T) => Promise<U>,
|
|
15
|
+
): Promise<U[]> {
|
|
10
16
|
const promises = R.pipe(
|
|
11
17
|
input,
|
|
12
18
|
R.map((item) => fn(item)),
|
|
@@ -16,8 +22,11 @@ export async function asyncMap<T, U>(input: T[], fn: (x: T) => Promise<U>): Prom
|
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export function filterUndefined<T>(input: (T | undefined)[]): T[] {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const result: T[] = [];
|
|
26
|
+
for (const item of input) {
|
|
27
|
+
if (item !== undefined) {
|
|
28
|
+
result.push(item);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
23
32
|
}
|