diff --git a/README.md b/README.md index 6f362df..bfc266b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,35 @@ # FujoCoded Plugins -A repository of plugins for true webdev fujin. Includes plugins for: +
-- Remark -- Rehype -- Zod -- Astro + -You can learn about each plugin by looking at the `README.md` file within their directories. +A repository of plugins for true webdev fujin. + + + + Fandom Coders badge + +
+ +## What is `fujocoded-plugins`? + +This repo includes a collection of plugins for [remark](https://remark.js.org/), [rehype](https://github.com/rehypejs/rehype), [Zod](https://zod.dev/), and [Astro](https://astro.build/). + +You can learn more about each plugin by looking at the `README.md` file in its directory. + +- [`astro-alt-text-toolkit`](/astro-alt-text-toolkit/README.md) - (WIP) Makes it easier to manage alt text on your Astro site, and beyond. +- [`astro-ao3-loader`](/astro-ao3-loader/README.md) - Uses [AO3.js](https://github.com/fujowebdev/ao3.js) to load data from AO3 to your Astro site. +- [`astro-authproto`](/astro-authproto/README.md) - Adds ATproto authentication to your Astro site. +- [`astro-dev-only`](/astro-dev-only/README.md) - Renders pages to your Astro site during development (`npm run dev`), and removes them from the final build (`npm run build`). +- [`astro-rehype-html-headings`](/astro-rehype-html-headings/README.md) - Returns Astro headings from an `.md` or `.mdx` including their rendered HTML. +- [`astro-remark-collect-components`](/astro-remark-collect-components/README.md) - Adds a list of attributes from Astro components to the `remarkFrontmatter` property for later use. +- [`astro-smooth-actions`](/astro-smooth-actions/README.md) - Makes Astro Action smoother by persisting form action results in the session. +- [`expressive-code-caption`](/expressive-code-caption/README.md) - Allows adding captions to [`expressive-code`](https://github.com/expressive-code/expressive-code) code blocks. +- [`expressive-code-output`](/expressive-code-output/README.md) - Allows separating code in [`expressive-code`](https://github.com/expressive-code/expressive-code) code blocks from the output. +- [`rehype-code-params`](/rehype-code-params/README.md) - Styles param values in inline code. +- [`remark-alt-text-files`](/remark-alt-text-files/README.md) - Allows loading alt text from a file. +- [`remark-capitalize-titles`](/remark-capitalize-titles/README.md) - Transforms all Markdown titles using [title.sh](https://github.com/zeit/title). +- [`remark-excalidraw`](/remark-excalidraw/README.md) - Allows loading [Excalidraw](https://github.com/excalidraw/excalidraw) files in Markdown. +- [`remark-jsx-auto-slug`](/remark-jsx-auto-slug/README.md) - Automatically adds slugs to Astro components. +- [`zod-transform-socials`](/zod-transform-socials/README.md) - Applies a Zod transformation to a list of social contacts to surface the appropriate website, username, and icon name. diff --git a/astro-alt-text-toolkit/README.md b/astro-alt-text-toolkit/README.md index 551670d..cf4f9e5 100644 --- a/astro-alt-text-toolkit/README.md +++ b/astro-alt-text-toolkit/README.md @@ -1,4 +1,6 @@ -# @fujocoded/astro-alt-text-toolkit +# `@fujocoded/astro-alt-text-toolkit` -A WIP bundle of ALT text utilities to make the ALT text of your -Astro site (and beyond) easier to manage +> [!WARNING] +> This plugin is a work in progress. + +A bundle of utilities that make the alt text of your Astro site (and beyond) easier to manage. diff --git a/astro-ao3-loader/README.md b/astro-ao3-loader/README.md index 8807d09..a43e0a5 100644 --- a/astro-ao3-loader/README.md +++ b/astro-ao3-loader/README.md @@ -1,70 +1,91 @@ -# Astro AO3 loader +# `@fujocoded/astro-ao3-loader` + +
+ +Load data from Archive of Our Own to your Astro site. + + + + NPM license + + + Fandom Coders badge + + + Np PM version badge + + + Open in GitHub Codespaces + +
+ +## What is `@fujocoded/astro-ao3-loader`? > [!WARNING] -> This is an experimental library with only basic functionality. If you -> want to help us expand it, contact us via GitHub, Fandom Coders, or at -> contacts@fujocoded.com. +> This is an experimental library with only basic functionality. If you want to help us expand it, you can reach out via GitHub, Fandom Coders, our socials, or contacts@fujocoded.com. > -> If you've never joined an open source project before, this is an excellent -> first place to contribute! +> If you've never joined an open source project before, this is an excellent first place to contribute! -A AO3 data loader for Astro using the experimental content layer and -[AO3.js](https://github.com/fujowebdev/ao3.js) to load data from AO3 to your -Astro website. +This library makes it easy to use the [Content Loader API](https://docs.astro.build/en/reference/content-loader-reference/) and [AO3.js](https://github.com/fujowebdev/ao3.js) to load data from [Archive of Our Own](https://archiveofourown.org/) to your [Astro](https://astro.build/) website. -You can see an example of its usage [in our sample -repository](https://github.com/FujoWebDev/ao3-content-layer-example). +## What can `@fujocoded/astro-ao3-loader` do? -## Installation +You can use this library to easily grab data about content hosted on [Archive of Our Own](https://archiveofourown.org/) to use in your [Astro](https://astro.build/) siteβ€”however you wish to. ✨ -```sh -npm install @fujocoded/astro-ao3-loader -``` +The library includes the following loaders: -## Usage +| **Loader** | **Description** | **Data file** | +| -------------- | -------------------------------------------------------------------------------------------- | ----------------------------- | +| `worksLoader` | Loads data from a set of works hosted on [Archive of Our Own](https://archiveofourown.org/). | `src/content/ao3/works.yaml` | +| `seriesLoader` | Loads data from series hosted on [Archive of Our Own](https://archiveofourown.org/). | `src/content/ao3/series.yaml` | -This package requires Astro 4.14.0 or later. You must enable the experimental -content layer in Astro unless you are using version 5.0.0-beta or later. You can -do this by adding the following to your `astro.config.mjs`: +## How to use `@fujocoded/astro-ao3-loader` -```javascript -export default defineConfig({ - // ... - experimental: { - contentLayer: true, - }, -}); -``` +> [!TIP] +> Want to see some examples? Take a look around the [examples folder](/astro-ao3-loader/__examples__/). πŸ‘€ -Currently, this package contains only a single loader called `worksLoader`, -which loads the details of a series of works whose id is listed in the -`src/content/ao3/works.yaml` file. +### Prerequisites -First add the configuration in `src/content/config.ts`: +This library requires [Astro](https://astro.build/) 5.0.0 or later, and Astro's built-in [Content Loader API](https://docs.astro.build/en/reference/content-loader-reference/). Astro 4 isn't currently supported, but if you'd like to use the library on an older version, [open an issue](https://github.com/FujoWebDev/fujocoded-plugins/issues/new?labels=ao3-content-loader) to let us know! -```typescript -// src/content/config.ts -import { defineCollection } from "astro:content"; -import { feedLoader } from "@fujocoded/astro-ao3-loader"; +### Installation -export const collections = { - fanfictions: defineCollection({ loader: worksLoader }), -}; +```bash +npm install @fujocoded/astro-ao3-loader ``` -Then create your `src/content/ao3/works.yaml` file: +### Configuration -```yaml -- 38226814 -- 49238326 -- 59988091 -- 41160522 -- 11728554 -- 12928950 -- 58869805 -``` +The configuration steps are the same for each [loader](#what-can-fujocodedastro-ao3-loader-do). In this example, we'll use the `worksLoader` to get information from a list of works specified in `src/content/ao3/works.yaml`. + +1. Set up [a content collection](https://docs.astro.build/en/guides/content-collections/#defining-collections) in `src/content/config.ts` that uses your chosen loader. + + ```ts + import { defineCollection } from "astro:content"; + import { worksLoader } from "@fujocoded/astro-ao3-loader"; + + export const collections = { + fanfictions: defineCollection({ loader: worksLoader }), + }; + ``` -You can then use this like any other collection in Astro: +2. Create `src/content/ao3/works.yaml` and add a list of work IDs to the file. + + ```yaml + - 38226814 + - 49238326 + - 59988091 + - 41160522 + - 11728554 + - 12928950 + - 58869805 + ``` + +> [!TIP] +> This file uses the YAML data format to list work IDs. If you're running into issues, check your syntax using one of the many YAML validators out there. + +Once configured, you can use the collection created with `astro-ao3-loader` like you +would any other [Astro content collection](https://docs.astro.build/en/guides/content-collections/#using-content-in-astro-templates). ```astro --- @@ -76,14 +97,18 @@ const fanfictions = await getCollection("fanfictions");

Hello fujin

``` + +For a more complete example, you can take a look at [our example page](/astro-ao3-loader/__examples__/works-list/src/pages/index.astro). diff --git a/astro-ao3-loader/__examples__/astro-5/README.md b/astro-ao3-loader/__examples__/astro-5/README.md deleted file mode 100644 index 87b813a..0000000 --- a/astro-ao3-loader/__examples__/astro-5/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Astro Starter Kit: Minimal - -```sh -npm create astro@latest -- --template minimal -``` - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”œβ”€β”€ src/ -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/astro-ao3-loader/__examples__/astro-5/src/content/config.ts b/astro-ao3-loader/__examples__/astro-5/src/content/config.ts deleted file mode 100644 index 46638e3..0000000 --- a/astro-ao3-loader/__examples__/astro-5/src/content/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineCollection } from "astro:content"; -import { worksLoader, seriesLoader } from "@fujocoded/astro-ao3-loader"; - -export const collections = { - fanfictions: defineCollection({ loader: worksLoader }), - series: defineCollection({ loader: seriesLoader }), -}; \ No newline at end of file diff --git a/astro-ao3-loader/__examples__/astro-5/src/pages/index.astro b/astro-ao3-loader/__examples__/astro-5/src/pages/index.astro deleted file mode 100644 index 0183a5f..0000000 --- a/astro-ao3-loader/__examples__/astro-5/src/pages/index.astro +++ /dev/null @@ -1,63 +0,0 @@ ---- -import { getCollection } from "astro:content"; - -const fanfictions = await getCollection("fanfictions"); -const series = await getCollection("series"); - -// Uncomment the next line to see what's inside the `fanfictions` -// or `series`variables and what data you can access -// console.dir(fanfictions, { depth: null }); -// console.dir(series, {depth: null}) ---- - - - - - - - - Astro - - -

Hello fujin

- - - - - diff --git a/astro-ao3-loader/__examples__/astro-5/.gitignore b/astro-ao3-loader/__examples__/works-list/.gitignore similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/.gitignore rename to astro-ao3-loader/__examples__/works-list/.gitignore diff --git a/astro-ao3-loader/__examples__/works-list/README.md b/astro-ao3-loader/__examples__/works-list/README.md new file mode 100644 index 0000000..6aa3da0 --- /dev/null +++ b/astro-ao3-loader/__examples__/works-list/README.md @@ -0,0 +1,38 @@ +# `@fujocoded/astro-ao3-loader` Example + +
+ A list of works from AO3 as shown in this example. Features title, author, ratings and relationships +
+ +This example shows the `@fujocoded/astro-ao3-loader` library in action. Inside, you'll see (among others) the following folders and files: + +```text +/ +β”œβ”€ ... +β”œβ”€ src/ +β”‚ └─ content/ +β”‚ └── ao3/ +β”‚ └── series.yaml +β”‚ └── works.yaml +β”‚ └── config.ts +β”‚ └─ pages/ +β”‚ └── index.astro +└─ package.json +``` + +Explore at your leisure, and feel free to open an Issue if you have any questions. + +## Commands + +The following commands are Astro's built-in functionality. You can run these from a terminal pointing to your project's root. + +| **Command** | **Action** | +| ----------------- | --------------------------------------------- | +| `npm install` | Install dependencies. | +| `npm run dev` | Start a local dev server at `localhost:4321`. | +| `npm run build` | Build your production site to `./dist/`. | +| `npm run preview` | Preview your build locally before deploying. | diff --git a/astro-ao3-loader/__examples__/astro-5/astro.config.mjs b/astro-ao3-loader/__examples__/works-list/astro.config.mjs similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/astro.config.mjs rename to astro-ao3-loader/__examples__/works-list/astro.config.mjs diff --git a/astro-ao3-loader/__examples__/astro-5/package-lock.json b/astro-ao3-loader/__examples__/works-list/package-lock.json similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/package-lock.json rename to astro-ao3-loader/__examples__/works-list/package-lock.json diff --git a/astro-ao3-loader/__examples__/astro-5/package.json b/astro-ao3-loader/__examples__/works-list/package.json similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/package.json rename to astro-ao3-loader/__examples__/works-list/package.json diff --git a/astro-ao3-loader/__examples__/astro-5/public/favicon.svg b/astro-ao3-loader/__examples__/works-list/public/favicon.svg similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/public/favicon.svg rename to astro-ao3-loader/__examples__/works-list/public/favicon.svg diff --git a/astro-ao3-loader/__examples__/astro-5/src/content/ao3/series.yaml b/astro-ao3-loader/__examples__/works-list/src/content/ao3/series.yaml similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/src/content/ao3/series.yaml rename to astro-ao3-loader/__examples__/works-list/src/content/ao3/series.yaml diff --git a/astro-ao3-loader/__examples__/astro-5/src/content/ao3/works.yaml b/astro-ao3-loader/__examples__/works-list/src/content/ao3/works.yaml similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/src/content/ao3/works.yaml rename to astro-ao3-loader/__examples__/works-list/src/content/ao3/works.yaml diff --git a/astro-ao3-loader/__examples__/works-list/src/content/config.ts b/astro-ao3-loader/__examples__/works-list/src/content/config.ts new file mode 100644 index 0000000..769e137 --- /dev/null +++ b/astro-ao3-loader/__examples__/works-list/src/content/config.ts @@ -0,0 +1,17 @@ +import { defineCollection } from "astro:content"; +import { worksLoader, seriesLoader } from "@fujocoded/astro-ao3-loader"; + +/** + * You can configure content collections in Astro by exporting + * "collections" from `src/content/config`, like you see here. + * + * This loader exports two collections: + * - fanfictions, which uses our worksLoader + * - series, which uses our seriesLoader + * + * Learn more: https://docs.astro.build/en/guides/content-collections/#defining-collections + */ +export const collections = { + fanfictions: defineCollection({ loader: worksLoader }), + series: defineCollection({ loader: seriesLoader }), +}; diff --git a/astro-ao3-loader/__examples__/works-list/src/pages/index.astro b/astro-ao3-loader/__examples__/works-list/src/pages/index.astro new file mode 100644 index 0000000..e141cf7 --- /dev/null +++ b/astro-ao3-loader/__examples__/works-list/src/pages/index.astro @@ -0,0 +1,81 @@ +--- +import { getCollection } from "astro:content"; + +/** + * We use getCollection with the name of our collection to + * load the data we want to use in our page. + * + * See: https://docs.astro.build/en/guides/content-collections/#using-content-in-astro-templates + */ +const fanfictions = await getCollection("fanfictions"); +const series = await getCollection("series"); + +// Uncomment the next line to see what's inside the `fanfictions` +// or `series`variables and what data you can access. The data will +// appear in your terminal when you refresh the page. +// console.dir(fanfictions, { depth: null }); +// console.dir(series, {depth: null}) +--- + + + + + + + + @fujocoded/astro-ao3-loader example + + +

Hello, fujin!

+ + + + + diff --git a/astro-ao3-loader/__examples__/astro-5/tsconfig.json b/astro-ao3-loader/__examples__/works-list/tsconfig.json similarity index 100% rename from astro-ao3-loader/__examples__/astro-5/tsconfig.json rename to astro-ao3-loader/__examples__/works-list/tsconfig.json diff --git a/astro-ao3-loader/__examples__/works-list/works-list.png b/astro-ao3-loader/__examples__/works-list/works-list.png new file mode 100644 index 0000000..0af211a Binary files /dev/null and b/astro-ao3-loader/__examples__/works-list/works-list.png differ diff --git a/astro-ao3-loader/src/utils.ts b/astro-ao3-loader/src/fetcher.ts similarity index 71% rename from astro-ao3-loader/src/utils.ts rename to astro-ao3-loader/src/fetcher.ts index 83678e4..5f7a661 100644 --- a/astro-ao3-loader/src/utils.ts +++ b/astro-ao3-loader/src/fetcher.ts @@ -6,7 +6,6 @@ const CONCURRENCY = 5; const CACHE_TTL_MS = 1000 * 60 * 5; // 5 minutes const REQUEST_TIMEOUT_MS = 120_000; // 2 minutes total (includes all retries) const MAX_RETRIES = 5; -const PROGRESS_INTERVAL_MS = 15 * 1000; // 15 seconds const RESPONSE_CACHE = new Map(); const getRetryReason = (error: Error): string => { @@ -86,7 +85,9 @@ const createFetcher = (logger: LoaderContext["logger"]) => { ], beforeRetry: [ async ({ request, error, retryCount }) => { - logger.warn(`${getRetryReason(error)}, retrying (${retryCount}/${MAX_RETRIES}): ${request.url}`); + logger.warn( + `${getRetryReason(error)}, retrying (${retryCount}/${MAX_RETRIES}): ${request.url}`, + ); }, ], }, @@ -94,7 +95,7 @@ const createFetcher = (logger: LoaderContext["logger"]) => { return async ( input: RequestInfo | URL, - init?: RequestInit + init?: RequestInit, ): Promise => { // We skip the queue if the response is already cached if (RESPONSE_CACHE.has(input.toString())) { @@ -104,7 +105,6 @@ const createFetcher = (logger: LoaderContext["logger"]) => { }; }; - let AO3_FETCHER: typeof fetch | null = null; export const getFetcher = (logger: LoaderContext["logger"]) => { if (!AO3_FETCHER) { @@ -112,54 +112,3 @@ export const getFetcher = (logger: LoaderContext["logger"]) => { } return AO3_FETCHER; }; - -export const getProgressTracker = ({ - logger, - prefix, - total, - itemsType -}: { - logger: LoaderContext["logger"], - prefix: string, - total: number, - itemsType: string -}) => { - let successCount = 0; - // TODO: add details of failed items - let failCount = 0; - let timeout: NodeJS.Timeout | undefined; - - const log = () => { - const completed = successCount + failCount; - logger.info( - `${prefix} ${completed}/${total} ${itemsType} loaded ${failCount > 0 ? `(${failCount} failed)` : "" - }` - ); - - clearTimeout(timeout); - if (completed < total) { - timeout = setTimeout(log, PROGRESS_INTERVAL_MS); - } - }; - - return { - start: () => { - logger.info(`${prefix} Loading ${total} ${itemsType}...`); - timeout = setTimeout(log, PROGRESS_INTERVAL_MS); - }, - incrementSuccess: () => { - successCount++; - log(); - }, - incrementFail: () => { - failCount++; - log(); - }, - finish: () => { - clearTimeout(timeout); - logger.info( - `${prefix} Loaded ${successCount} of ${total} ${itemsType} (${failCount} failed)` - ); - }, - }; -}; diff --git a/astro-ao3-loader/src/index.ts b/astro-ao3-loader/src/index.ts index 661d357..8f597ac 100644 --- a/astro-ao3-loader/src/index.ts +++ b/astro-ao3-loader/src/index.ts @@ -4,27 +4,20 @@ import { workSummarySchema, seriesSchema } from "@fujocoded/ao3.js/zod-schemas"; import { parse } from "yaml"; import type { Loader, LoaderContext } from "astro/loaders"; import { z } from "zod"; -import { getFetcher, getProgressTracker } from "./utils.ts"; - -const PREFIX_ANSI_COLORS = { - works: "\x1b[36m", // Cyan - series: "\x1b[35m", // Magenta - reset: "\x1b[0m", -}; - -const getPrefix = (type: "works" | "series") => { - const color = PREFIX_ANSI_COLORS[type]; - return `${color}[${type}]${PREFIX_ANSI_COLORS.reset}`; -}; - +import { getFetcher } from "./fetcher.ts"; +import { getProgressTracker} from "./logger.ts"; +/** + * Loads items of the given "type" from the specified file ("yamlPath"). This uses the "itemFetcher" + * function to fetch items from Archive of Our Own. +**/ const loadItems = async ( { store, logger }: LoaderContext, config: { type: "works" | "series"; yamlPath: string; - fetchFn: (id: string) => Promise>; - } + itemFetcher: (id: string) => Promise>; + }, ) => { setFetcher(getFetcher(logger)); @@ -33,31 +26,32 @@ const loadItems = async ( const tracker = getProgressTracker({ logger, - prefix: getPrefix(config.type), total: ids.length, - itemsType: config.type + itemsType: config.type, }); + // Start tracking progress for this "itemsType". tracker.start(); try { await Promise.all( ids.map(async (id) => { try { - const item = await config.fetchFn(id); + // Fetch the item with the specified ID from the archive. + const item = await config.itemFetcher(id); + // If that works, add it to the store and mark success. store.set({ id: item.id.toString(), data: { ...item } }); tracker.incrementSuccess(); } catch (error) { - tracker.incrementFail(); - logger.error( - `${getPrefix(config.type)} Failed to fetch ${config.type.slice(0, -1)} ${id}: ${error}` - ); + // If there's an error, increment the fail count. + tracker.incrementFail(id, error); } - }) + }), ); } finally { + // Once everything is done, close off progress tracking. tracker.finish(); } -} +}; export const worksLoader: Loader = { name: "ao3-loader", @@ -65,7 +59,7 @@ export const worksLoader: Loader = { loadItems(context, { type: "works", yamlPath: "./src/content/ao3/works.yaml", - fetchFn: (workId) => getWork({ workId }), + itemFetcher: (workId) => getWork({ workId }), }), schema: workSummarySchema, }; @@ -76,7 +70,7 @@ export const seriesLoader: Loader = { loadItems(context, { type: "series", yamlPath: "./src/content/ao3/series.yaml", - fetchFn: (seriesId) => getSeries({ seriesId }), + itemFetcher: (seriesId) => getSeries({ seriesId }), }), schema: seriesSchema, }; diff --git a/astro-ao3-loader/src/logger.ts b/astro-ao3-loader/src/logger.ts new file mode 100644 index 0000000..6806734 --- /dev/null +++ b/astro-ao3-loader/src/logger.ts @@ -0,0 +1,77 @@ + +import type { LoaderContext } from "astro/loaders"; + +const PROGRESS_INTERVAL_MS = 15 * 1000; // 15 seconds + +const PREFIX_ANSI_COLORS = { + works: "\x1b[36m", // Cyan + series: "\x1b[35m", // Magenta + reset: "\x1b[0m", // Resets the terminal color to the original +}; + +/** + * Get the name (with colors!) of the type of content we're loading so that it's + * easier to distinguish the load state of our content. + */ +const getPrefix = (type: "works" | "series") => { + const color = PREFIX_ANSI_COLORS[type]; + return `${color}[${type}]${PREFIX_ANSI_COLORS.reset}`; +}; + +/** + * Logs download progress for "itemsType" to the command line, using + * the appropriate prefix. + */ +export const getProgressTracker = ({ + logger, + total, + itemsType, +}: { + logger: LoaderContext["logger"]; + total: number; + itemsType: "works" | "series"; +}) => { + let successCount = 0; + // TODO: add details of failed items + let failCount = 0; + let timeout: NodeJS.Timeout | undefined; + const prefix = getPrefix(itemsType); + + const log = () => { + const completed = successCount + failCount; + logger.info( + `${prefix} ${completed}/${total} ${itemsType} loaded ${ + failCount > 0 ? `(${failCount} failed)` : "" + }`, + ); + + clearTimeout(timeout); + if (completed < total) { + timeout = setTimeout(log, PROGRESS_INTERVAL_MS); + } + }; + + return { + start: () => { + logger.info(`${prefix} Loading ${total} ${itemsType}...`); + timeout = setTimeout(log, PROGRESS_INTERVAL_MS); + }, + incrementSuccess: () => { + successCount++; + log(); + }, + incrementFail: (id: string, error: unknown) => { + failCount++; + log(); + logger.error( + `${prefix} Failed to fetch ${itemsType.slice(0, -1)} ${id}: ${error}`, + ); + }, + finish: () => { + clearTimeout(timeout); + logger.info( + `${prefix} Loaded ${successCount} of ${total} ${itemsType} (${failCount} failed)`, + ); + }, + }; +};