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/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 { type Source, type ResultEntry, FeedEntrySchema, type Configuration } from "./types.js";
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.js";
12
+ import { asyncMapFilterUndefined } from "./util.ts";
7
13
 
8
14
  // Handle ESM/CommonJS interop - truncate-html exports differently in different environments
9
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
10
- const truncate: (html: string, length?: number) => string = (truncateHtml as any).default ?? truncateHtml;
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) => fetch(source, config.truncate));
35
+ return await asyncMapFilterUndefined(config.sources, (source) =>
36
+ fetch(source, config.truncate),
37
+ );
14
38
  }
15
39
 
16
- export async function fetch(source: Source, length: number): Promise<ResultEntry | undefined> {
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 ?? firstItem.content ?? firstItem.description ?? firstItem["content:encoded"];
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: preview ? truncate(sanitizeHtml(preview, { parseStyleAttributes: false }), length) : undefined,
72
+ preview:
73
+ preview !== undefined && preview !== ""
74
+ ? truncate(
75
+ sanitizeHtml(preview, { parseStyleAttributes: false }),
76
+ length,
77
+ )
78
+ : undefined,
43
79
  };
44
- } catch (e) {
45
- console.error(`Error fetching ${source.url}: ${e as string}`);
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 "vitest";
2
- import type { Configuration } from "./types.js";
3
- import { run } from "./index.js";
4
- import { tmpdir } from "os";
5
- import { mkdtemp } from "fs/promises";
6
- import { join } from "path";
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() * 10000) + 3000;
12
+ const port = Math.floor(Math.random() * 10_000) + 3000;
13
13
  app.listen(port, () => {
14
- console.log(`Test server listening at http://localhost:${port.toString()}`);
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(path: string): string {
20
- return `http://localhost:${port.toString()}/${path}`;
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", { timeout: 30000 }, async () => {
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", { timeout: 30000 }, async () => {
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", { timeout: 30000 }, async () => {
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 as fetchAllCached } from "./cache.js";
3
- import { fetchAll as fetchAllUncached } from "./fetch.js";
4
- import { type Configuration, type Result, CachedConfigurationSchema } from "./types.js";
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 result: Result;
15
+ let fetched: Result;
10
16
  if (success) {
11
- console.log(`Using cache at ${data.cache.cache_file}.`);
12
- result = await fetchAllCached(data);
17
+ console.warn(`Using cache at ${data.cache.cache_file}.`);
18
+ fetched = await fetchAllCached(data);
13
19
  } else {
14
- console.log("Cache disabled.");
15
- result = await fetchAllUncached(config);
20
+ console.warn("Cache disabled.");
21
+ fetched = await fetchAllUncached(config);
16
22
  }
17
23
 
18
24
  let results = R.pipe(
19
- result,
20
- R.sortBy((result) => result.date.getTime()),
25
+ fetched,
26
+ R.sortBy((entry) => entry.date.getTime()),
21
27
  R.reverse(),
22
- R.filter((result) => {
23
- if (result.source.filter && result.preview) {
24
- return result.source.filter(result.preview);
25
- } else {
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
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import { z } from "zod/v3";
2
2
 
3
3
  export type Source = z.infer<typeof SourceSchema>;
4
4
  /** An RSS source */
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>(input: T[], fn: (x: T) => Promise<U | undefined>): Promise<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>(input: T[], fn: (x: T) => Promise<U>): Promise<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
- return R.pipe(
20
- input,
21
- R.filter((result) => result !== undefined),
22
- ) as T[];
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
  }