diff --git a/AGENTS.md b/AGENTS.md index 34feb0168..0a2e9692d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,17 +61,21 @@ The repository is organized as a monorepo with the following packages: - *src/testing/*: Testing utilities - *src/vocab/*: ActivityPub vocabulary implementation - *src/webfinger/*: WebFinger protocol implementation - - *src/x/*: Framework integrations + - ~~src/x/: Framework integrations~~ **Don't use.** This directory will be removed in version 2.0.0. Use packages from the `@fedify` scope, which are located in the `packages/` directory (e.g., `@fedify/hono` is in `packages/hono/`). - *packages/cli/*: Fedify CLI implementation (@fedify/cli, built with Deno) - *packages/amqp/*: AMQP/RabbitMQ driver (@fedify/amqp) + - *packages/cfworkers/*: Cloudflare Workers integration (@fedify/cfworkers) + - *packages/denokv/*: Deno KV integration (@fedify/denokv) - *packages/elysia/*: Elysia integration (@fedify/elysia) - *packages/express/*: Express.js integration (@fedify/express) - *packages/h3/*: h3 framework integration (@fedify/h3) + - *packages/hono/*: Hono integration (@fedify/hono) - *packages/postgres/*: PostgreSQL drivers (@fedify/postgres) - *packages/redis/*: Redis drivers (@fedify/redis) - *packages/nestjs/*: NestJS integration (@fedify/nestjs) - *packages/next/*: Next.js integration (@fedify/next) - *packages/sqlite/*: SQLite driver (@fedify/sqlite) + - *packages/sveltekit/*: SvelteKit integration (@fedify/sveltekit) - *packages/testing/*: Testing utilities (@fedify/testing) - *docs/*: Documentation built with Node.js and VitePress - *examples/*: Example projects demonstrating Fedify usage @@ -143,10 +147,10 @@ Common Tasks ### Implementing Framework Integrations -1. Add new integrations in the *packages/fedify/src/x/* directory -2. Follow pattern from existing integrations (hono.ts, sveltekit.ts) +1. Create a new package in *packages/* directory for new integrations +2. Follow pattern from existing integration packages (*packages/hono/*, *packages/sveltekit/*) 3. Use standard request/response interfaces for compatibility -4. Consider creating a dedicated package for substantial integrations +4. Consider creating example applications in *examples/* that demonstrate usage ### Creating Database Adapters diff --git a/CHANGES.md b/CHANGES.md index 466806050..9d94047f0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -41,10 +41,21 @@ To be released. - Changed how `parseSoftware()` function handles non-Semantic Versioning number strings on `tryBestEffort` mode. [[#353], [#365] by Hyeonseo Kim]] + - Separated modules from `@fedify/fedify/x` into dedicated packages to + improve modularity and reduce bundle size. The existing integration + functions in `@fedify/fedify/x` are now deprecated and will be removed in + version 2.0.0. [[#375] by Chanhaeng Lee] + + - Deprecated `@fedify/fedify/x/cfworkers` in favor of `@fedify/cfworkers`. + - Deprecated `@fedify/fedify/x/denokv` in favor of `@fedify/denokv`. + - Deprecated `@fedify/fedify/x/hono` in favor of `@fedify/hono`. + - Deprecated `@fedify/fedify/x/sveltekit` in favor of `@fedify/sveltekit`. + [FEP-5711]: https://w3id.org/fep/5711 [#353]: https://github.com/fedify-dev/fedify/issues/353 [#365]: https://github.com/fedify-dev/fedify/pull/365 [#373]: https://github.com/fedify-dev/fedify/issues/373 +[#375]: https://github.com/fedify-dev/fedify/issues/375 [#381]: https://github.com/fedify-dev/fedify/pull/381 ### @fedify/cli @@ -67,6 +78,30 @@ To be released. [#353]: https://github.com/fedify-dev/fedify/issues/353 [#365]: https://github.com/fedify-dev/fedify/pull/365 +### @fedify/cfworkers + + - Created Cloudflare Workers integration as the *@fedify/cfworkers* package. + Separated from `@fedify/fedify/x/cfworkers` to improve modularity and + reduce bundle size. [[#375] by Chanhaeng Lee] + +[#375]: https://github.com/fedify-dev/fedify/issues/375 + +### @fedify/denokv + + - Created Deno KV integration as the *@fedify/denokv* package. + Separated from `@fedify/fedify/x/denokv` to improve modularity and + reduce bundle size. [[#375] by Chanhaeng Lee] + +[#375]: https://github.com/fedify-dev/fedify/issues/375 + +### @fedify/hono + + - Created Hono integration as the *@fedify/hono* package. + Separated from `@fedify/fedify/x/hono` to improve modularity and + reduce bundle size. [[#375] by Chanhaeng Lee] + +[#375]: https://github.com/fedify-dev/fedify/issues/375 + ### @fedify/next - Created [Next.js] integration as the *@fedify/next* package. @@ -82,6 +117,13 @@ To be released. [#368]: https://github.com/fedify-dev/fedify/pull/368 +### @fedify/sveltekit + + - Created SvelteKit integration as the *@fedify/sveltekit* package. + Separated from `@fedify/fedify/x/sveltekit` to improve modularity and + reduce bundle size. [[#375] by Chanhaeng Lee] + +[#375]: https://github.com/fedify-dev/fedify/issues/375 Version 1.8.8 ------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5e3b358c..d94dd4a72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -191,14 +191,18 @@ The repository is organized as a monorepo with the following packages: - *packages/cli/*: The Fedify CLI (@fedify/cli). The CLI is built with [Deno]. - *packages/amqp/*: AMQP/RabbitMQ driver (@fedify/amqp) for Fedify. + - *packages/cfworkers/*: Cloudflare Workers integration (@fedify/cfworkers) for Fedify. + - *packages/denokv/*: Deno KV integration (@fedify/denokv) for Fedify. - *packages/elysia/*: Elysia integration (@fedify/elysia) for Fedify. - *packages/express/*: Express integration (@fedify/express) for Fedify. - *packages/h3/*: h3 framework integration (@fedify/h3) for Fedify. + - *packages/hono/*: Hono integration (@fedify/hono) for Fedify. - *packages/postgres/*: PostgreSQL drivers (@fedify/postgres) for Fedify. - *packages/redis/*: Redis drivers (@fedify/redis) for Fedify. - *packages/nestjs/*: NestJS integration (@fedify/nestjs) for Fedify. - *packages/next/*: Next.js integration (@fedify/next) for Fedify. - *packages/sqlite/*: SQLite driver (@fedify/sqlite) for Fedify. + - *packages/sveltekit/*: SvelteKit integration (@fedify/sveltekit) for Fedify. - *packages/testing/*: Testing utilities (@fedify/testing) for Fedify. - *docs/*: The Fedify docs. The docs are built with [Node.js] and [VitePress]. diff --git a/cspell.json b/cspell.json index 707f84454..5f240a731 100644 --- a/cspell.json +++ b/cspell.json @@ -27,6 +27,7 @@ "docloader", "draft-cavage", "eddsa", + "elysia", "fanout", "federatable", "Federatable", @@ -100,6 +101,7 @@ "unfollows", "urlpattern", "uuidv7", + "Vinxi", "vitepress", "vtsls", "webfinger", diff --git a/deno.json b/deno.json index a73677663..776101a02 100644 --- a/deno.json +++ b/deno.json @@ -3,12 +3,16 @@ "./packages/fedify", "./packages/cli", "./packages/amqp", + "./packages/cfworkers", + "./packages/denokv", "./packages/elysia", "./packages/express", "./packages/h3", + "./packages/hono", "./packages/postgres", "./packages/redis", "./packages/sqlite", + "./packages/sveltekit", "./packages/testing", "./examples/blog", "./examples/cloudflare-workers", @@ -26,6 +30,7 @@ "@std/path": "jsr:@std/path@^1.0.6", "@std/yaml": "jsr:@std/yaml@^1.0.8", "amqplib": "npm:amqplib@^0.10.8", + "es-toolkit": "npm:es-toolkit@^1.30.0", "h3": "npm:h3@^1.15.0", "@nestjs/common": "npm:@nestjs/common@^11.0.1", "ioredis": "npm:ioredis@^5.6.1", diff --git a/docs/manual/deploy.md b/docs/manual/deploy.md index c23d7f5fe..bfe6d2d8a 100644 --- a/docs/manual/deploy.md +++ b/docs/manual/deploy.md @@ -231,7 +231,7 @@ type Env = { import { Person } from "@fedify/fedify"; // ---cut-before--- import { createFederationBuilder } from "@fedify/fedify"; -import { WorkersKvStore, WorkersMessageQueue } from "@fedify/fedify/x/cfworkers"; +import { WorkersKvStore, WorkersMessageQueue } from "@fedify/cfworkers"; const builder = createFederationBuilder(); @@ -266,7 +266,7 @@ must manually connect queue handlers: ~~~~ typescript twoslash // @noErrors: 2345 import { createFederationBuilder, Person, type Message } from "@fedify/fedify"; -import { WorkersKvStore, WorkersMessageQueue } from "@fedify/fedify/x/cfworkers"; +import { WorkersKvStore, WorkersMessageQueue } from "@fedify/cfworkers"; type Env = { KV_NAMESPACE: KVNamespace; @@ -323,7 +323,7 @@ as well: ~~~~ typescript import { createFederation } from "@fedify/fedify"; -import { DenoKvStore, DenoKvMessageQueue } from "@fedify/fedify/x/deno"; +import { DenoKvStore, DenoKvMessageQueue } from "@fedify/denokv"; // Open Deno KV (automatically available on Deno Deploy) const kv = await Deno.openKv(); diff --git a/docs/manual/federation.md b/docs/manual/federation.md index 8a0129b95..e14984482 100644 --- a/docs/manual/federation.md +++ b/docs/manual/federation.md @@ -51,16 +51,16 @@ testing and development purposes, and the `DenoKvStore` class is Deno KV-backed implementation for production use (as you can guess from the name, it is only available in Deno runtime). -As separate packages, [@fedify/redis] provides [`RedisKvStore`] class, which is -a Redis-backed implementation for production use, and [@fedify/postgres] +As separate packages, [`@fedify/redis`] provides [`RedisKvStore`] class, which +is a Redis-backed implementation for production use, and [`@fedify/postgres`] provides [`PostgresKvStore`] class, which is a PostgreSQL-backed implementation for production use. Further details are explained in the [*Key–value store* section](./kv.md). -[@fedify/redis]: https://github.com/fedify-dev/redis +[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis [`RedisKvStore`]: https://jsr.io/@fedify/redis/doc/kv/~/RedisKvStore -[@fedify/postgres]: https://github.com/fedify-dev/postgres +[`@fedify/postgres`]: https://github.com/fedify-dev/fedify/tree/main/packages/postgres [`PostgresKvStore`]: https://jsr.io/@fedify/postgres/doc/kv/~/PostgresKvStore ### `kvPrefixes` @@ -110,12 +110,12 @@ and the `DenoKvMessageQueue` class is a Deno KV-backed implementation for production use (as you can guess from the name, it is only available in Deno runtime). -As separate packages, [@fedify/redis] provides [`RedisMessageQueue`] class, +As separate packages, [`@fedify/redis`] provides [`RedisMessageQueue`] class, which is a Redis-backed implementation for production use, -and [@fedify/postgres] provides [`PostgresMessageQueue`] class, which is a -PostgreSQL-backed implementation for production use, and [@fedify/amqp] provides -[`AmqpMessageQueue`] class, which is an AMQP broker-backed implementation for -production use. +and [`@fedify/postgres`] provides [`PostgresMessageQueue`] class, which is a +PostgreSQL-backed implementation for production use, and [`@fedify/amqp`] +provides [`AmqpMessageQueue`] class, which is an AMQP broker-backed +implementation for production use. Further details are explained in the [*Message queue* section](./mq.md). @@ -184,7 +184,7 @@ Further details are explained in the [*Message queue* section](./mq.md). [`RedisMessageQueue`]: https://jsr.io/@fedify/redis/doc/mq/~/RedisMessageQueue [`PostgresMessageQueue`]: https://jsr.io/@fedify/postgres/doc/mq/~/PostgresMessageQueue -[@fedify/amqp]: https://github.com/fedify-dev/amqp +[`@fedify/amqp`]: https://github.com/fedify-dev/fedify/tree/main/packages/amqp [`AmqpMessageQueue`]: https://jsr.io/@fedify/amqp/doc/mq/~/AmqpMessageQueue ### `manuallyStartQueue` @@ -305,8 +305,8 @@ an object for options. > [!CAUTION] > > This settings do not affect the `User-Agent` header of the HTTP requests -> that `lookupWebFinger()`, `lookupObject()`, and `getNodeInfo()` functions make, -> since they do not depend on the `Federation` object. +> that `lookupWebFinger()`, `lookupObject()`, and `getNodeInfo()` functions +> make, since they do not depend on the `Federation` object. > > However, `Context.lookupObject()` method is affected by this settings. diff --git a/docs/manual/inbox.md b/docs/manual/inbox.md index 90702da23..a4705d73e 100644 --- a/docs/manual/inbox.md +++ b/docs/manual/inbox.md @@ -341,8 +341,8 @@ const federation = createFederation({ > [!NOTE] > The `InProcessMessageQueue` is a simple in-memory message queue that is > suitable for development and testing. For production use, you should -> consider using a more robust message queue, such as `DenoKvMessageQueue` -> from `@fedify/fedify/x/deno` module or [`RedisMessageQueue`] from +> consider using a more robust message queue, such as [`DenoKvMessageQueue`] +> from [`@fedify/denokv`] package or [`RedisMessageQueue`] from > [`@fedify/redis`] package. > > For more information, see the [*Message queue* section](./mq.md). @@ -378,8 +378,10 @@ duplicate retry mechanisms and leverages the backend's optimized retry features. > your application so that each node can process messages in parallel with > the shared message queue. +[`DenoKvMessageQueue`]: https://jsr.io/@fedify/denokv/doc/mq/~/DenoKvMessageQueue +[`@fedify/denokv`]: https://github.com/fedify-dev/fedify/tree/main/packages/denokv [`RedisMessageQueue`]: https://jsr.io/@fedify/redis/doc/mq/~/RedisMessageQueue -[`@fedify/redis`]: https://github.com/fedify-dev/redis +[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis Error handling diff --git a/docs/manual/integration.md b/docs/manual/integration.md index f330973b6..a2333a5a4 100644 --- a/docs/manual/integration.md +++ b/docs/manual/integration.md @@ -125,13 +125,41 @@ Hono *This API is available since Fedify 0.6.0.* +> [!WARNING] +> The `@fedify/fedify/x/hono` module is deprecated and will be removed in +> Fedify 2.0.0. Please use the `@fedify/hono` package instead. + [Hono] is a fast, lightweight, and Web standard-compliant server framework for -TypeScript. Fedify has the `@fedify/fedify/x/hono` module that provides -a middleware to integrate Fedify with Hono: +TypeScript. The *@fedify/hono* package provides a middleware to integrate +Fedify with Hono: + +::: code-group + +~~~~ sh [Deno] +deno add jsr:@fedify/hono +~~~~ + +~~~~ sh [npm] +npm add @fedify/hono +~~~~ + +~~~~ sh [pnpm] +pnpm add @fedify/hono +~~~~ + +~~~~ sh [Yarn] +yarn add @fedify/hono +~~~~ + +~~~~ sh [Bun] +bun add @fedify/hono +~~~~ + +::: ~~~~ typescript import { createFederation } from "@fedify/fedify"; -import { federation } from "@fedify/fedify/x/hono"; +import { federation } from "@fedify/hono"; import { Hono } from "hono"; const fedi = createFederation({ @@ -244,14 +272,41 @@ SvelteKit *This API is available since Fedify 1.3.0.* -[SvelteKit] is a framework for building web applications with [Svelte]. Fedify -has the `@fedify/fedify/x/sveltekit` module that provides a hook handler to -integrate Fedify with SvelteKit. Put the following code in your -*hooks.server.ts* file: +> [!WARNING] +> The `@fedify/fedify/x/sveltekit` module is deprecated and will be removed in +> Fedify 2.0.0. Please use the `@fedify/sveltekit` package instead. + +[SvelteKit] is a framework for building web applications with [Svelte]. The +*@fedify/sveltekit* package provides a middleware to integrate Fedify with +SvelteKit: + +::: code-group + +~~~~ sh [Deno] +deno add jsr:@fedify/sveltekit +~~~~ + +~~~~ sh [npm] +npm add @fedify/sveltekit +~~~~ + +~~~~ sh [pnpm] +pnpm add @fedify/sveltekit +~~~~ + +~~~~ sh [Yarn] +yarn add @fedify/sveltekit +~~~~ + +~~~~ sh [Bun] +bun add @fedify/sveltekit +~~~~ + +::: ~~~~ typescript import { createFederation } from "@fedify/fedify"; -import { fedifyHook } from "@fedify/fedify/x/sveltekit"; +import { fedifyHook } from "@fedify/sveltekit"; const federation = createFederation({ // Omitted for brevity; see the related section for details. @@ -492,18 +547,62 @@ Next.js > [!TIP] > You can see the example in the `examples/next-integration` directory in -> the [Fedify repository]. -> Or you can just create a Fedify–Next.js app with the following command: -> -> ~~~~ sh +> the [Fedify repository]. You can create a Fedify–Next.js app copying the +> example with the following command: +> +> ::: code-group +> +> ~~~~ sh [npm] > npx create-next-app -e https://github.com/fedify-dev/fedify \ > --example-path examples/next-integration > ~~~~ +> +> ~~~~ sh [pnpm] +> pnpm create next-app -e https://github.com/fedify-dev/fedify \ +> --example-path examples/next-integration +> ~~~~ +> +> ~~~~ sh [Yarn] +> yarn create next-app -e https://github.com/fedify-dev/fedify \ +> --example-path examples/next-integration +> ~~~~ +> +> ~~~~ sh [Bun] +> bun create next-app -e https://github.com/fedify-dev/fedify \ +> --example-path examples/next-integration +> ~~~~ +> +> ::: [Next.js] is a React framework that enables you to build server-rendered -and statically generated web applications. Fedify has the `@fedify/next` -module that provides a middleware to integrate Fedify with Next.js. Create -an app with the following command using the Fedify CLI: +and statically generated web applications. The *@fedify/next* package provides +a middleware to integrate Fedify with Next.js: + +::: code-group + +~~~~ sh [Deno] +deno add jsr:@fedify/next +~~~~ + +~~~~ sh [npm] +npm add @fedify/next +~~~~ + +~~~~ sh [pnpm] +pnpm add @fedify/next +~~~~ + +~~~~ sh [Yarn] +yarn add @fedify/next +~~~~ + +~~~~ sh [Bun] +bun add @fedify/next +~~~~ + +::: + +Or create an app with the following command using the Fedify CLI: ~~~~ sh fedify init my-next-app @@ -520,14 +619,15 @@ fedify init my-next-app ~~~~ Then you can see the Next.js boilerplate code in the `my-next-app` directory. -But if you created a Next.js app with `create-next-app` before, you'll see +If you have created a Next.js app with `create-next-app` before, you'll see some differences in the code. There is a `middleware.ts` file in the `my-next-app` directory, which is the entry point to the Fedify middleware -from the Next.js framework: +from the Next.js framework. Or, if you just install *@fedify/next* manually, +put the following code in your `middleware.ts` file: ~~~~ typescript import { fedifyWith } from "@fedify/next"; -import federation from "./federation"; +import federation from "./federation"; // Your `Federation` instance export default fedifyWith(federation)( /* diff --git a/docs/manual/kv.md b/docs/manual/kv.md index 019fa264b..8191de284 100644 --- a/docs/manual/kv.md +++ b/docs/manual/kv.md @@ -133,7 +133,22 @@ const federation = createFederation({ ### `DenoKvStore` (Deno only) -`DenoKvStore` is a key–value store implementation for [Deno] runtime that uses +> [!WARNING] +> The `@fedify/fedify/x/denokv` module is deprecated and will be removed in +> Fedify 2.0.0. Please use the [`@fedify/denokv`] package instead. + +To use the [`DenoKvStore`], you need to install the *@fedify/denokv* package +first: + +::: code-group + +~~~~ bash [Deno] +deno add jsr:@fedify/denokv +~~~~ + +::: + +[`DenoKvStore`] is a key–value store implementation for [Deno] runtime that uses Deno's built-in [`Deno.openKv()`] API. It provides persistent storage and good performance for Deno environments. It's suitable for production use in Deno applications. @@ -149,7 +164,7 @@ Cons ~~~~ typescript import { createFederation } from "@fedify/fedify"; -import { DenoKvStore } from "@fedify/fedify/x/deno"; +import { DenoKvStore } from "@fedify/denokv"; const kv = await Deno.openKv(); const federation = createFederation({ @@ -158,6 +173,8 @@ const federation = createFederation({ }); ~~~~ +[`@fedify/denokv`]: https://jsr.io/@fedify/denokv +[`DenoKvStore`]: https://jsr.io/@fedify/denokv/doc/mq/~/DenoKvStore [Deno]: https://deno.com/ [`Deno.openKv()`]: https://docs.deno.com/api/deno/~/Deno.openKv @@ -280,7 +297,38 @@ const federation = createFederation({ *This API is available since Fedify 1.6.0.* -`WorkersKvStore` is a key–value store implementation for [Cloudflare Workers] +> [!WARNING] +> The `@fedify/fedify/x/cfworkers` module is deprecated and will be removed in +> Fedify 2.0.0. Please use the [`@fedify/cfworkers`] package instead. + +To use the [`WorkersKvStore`], you need to install the *@fedify/cfworkers* +package first: + +::: code-group + +~~~~ bash [Deno] +deno add jsr:@fedify/cfworkers +~~~~ + +~~~~ bash [npm] +npm add @fedify/cfworkers +~~~~ + +~~~~ bash [pnpm] +pnpm add @fedify/cfworkers +~~~~ + +~~~~ bash [Yarn] +yarn add @fedify/cfworkers +~~~~ + +~~~~ bash [Bun] +bun add @fedify/cfworkers +~~~~ + +::: + +[`WorkersKvStore`] is a key–value store implementation for [Cloudflare Workers] that uses Cloudflare's built-in [Cloudflare Workers KV] API. It provides persistent storage and good performance for Cloudflare Workers environments. It's suitable for production use in Cloudflare Workers applications. @@ -300,7 +348,7 @@ import type { FederationBuilder } from "@fedify/fedify"; const builder = undefined as unknown as FederationBuilder; // ---cut-before--- import type { Federation } from "@fedify/fedify"; -import { WorkersKvStore } from "@fedify/fedify/x/cfworkers"; +import { WorkersKvStore } from "@fedify/cfworkers"; export default { async fetch(request, env, ctx) { @@ -323,6 +371,8 @@ export default { > object. See the [*Builder pattern for structuring* > section](./federation.md#builder-pattern-for-structuring) for details. +[`@fedify/cfworkers`]: https://github.com/fedify-dev/fedify/tree/main/packages/cfworkers +[`WorkersKvStore`]: https://jsr.io/@fedify/cfworkers/doc/kv/~/WorkersKvStore [Cloudflare Workers]: https://workers.cloudflare.com/ [Cloudflare Workers KV]: https://developers.cloudflare.com/kv/ diff --git a/docs/manual/mq.md b/docs/manual/mq.md index 6a286f7b9..bc5f2c498 100644 --- a/docs/manual/mq.md +++ b/docs/manual/mq.md @@ -60,7 +60,22 @@ const federation = createFederation({ ### `DenoKvMessageQueue` (Deno only) -`DenoKvMessageQueue` is a message queue implementation for [Deno] runtime that +> [!WARNING] +> The `@fedify/fedify/x/denokv` module is deprecated and will be removed in +> Fedify 2.0.0. Please use the [`@fedify/denokv`] package instead. + +To use the [`DenoKvMessageQueue`], you need to install the *@fedify/denokv* +package first: + +::: code-group + +~~~~ bash [Deno] +deno add jsr:@fedify/denokv +~~~~ + +::: + +[`DenoKvMessageQueue`] is a message queue implementation for [Deno] runtime that uses Deno's built-in [`Deno.openKv()`] API. It provides persistent storage and good performance for Deno environments. It's suitable for production use in Deno applications. @@ -76,7 +91,7 @@ Cons ~~~~ typescript import { createFederation } from "@fedify/fedify"; -import { DenoKvMessageQueue } from "@fedify/fedify/x/deno"; +import { DenoKvMessageQueue } from "@fedify/denokv"; const kv = await Deno.openKv(); const federation = createFederation({ @@ -85,6 +100,8 @@ const federation = createFederation({ }); ~~~~ +[`@fedify/denokv`]: https://jsr.io/@fedify/denokv +[`DenoKvMessageQueue`]: https://jsr.io/@fedify/denokv/doc/mq/~/DenoKvMessageQueue [`Deno.openKv()`]: https://docs.deno.com/api/deno/~/Deno.openKv ### [`RedisMessageQueue`] @@ -285,6 +302,37 @@ const federation = createFederation({ *This API is available since Fedify 1.6.0.* +> [!WARNING] +> The `@fedify/fedify/x/cfworkers` module is deprecated and will be removed in +> Fedify 2.0.0. Please use the [`@fedify/cfworkers`] package instead. + +To use the [`WorkersMessageQueue`], you need to install the *@fedify/cfworkers* +package first: + +::: code-group + +~~~~ bash [Deno] +deno add jsr:@fedify/cfworkers +~~~~ + +~~~~ bash [npm] +npm add @fedify/cfworkers +~~~~ + +~~~~ bash [pnpm] +pnpm add @fedify/cfworkers +~~~~ + +~~~~ bash [Yarn] +yarn add @fedify/cfworkers +~~~~ + +~~~~ bash [Bun] +bun add @fedify/cfworkers +~~~~ + +::: + `WorkersMessageQueue` is a message queue implementation for [Cloudflare Workers] that uses Cloudflare's built-in [Cloudflare Queues] API. It provides scalability and high performance, making it suitable for production use in @@ -307,7 +355,7 @@ import type { FederationBuilder, KvStore } from "@fedify/fedify"; const builder = undefined as unknown as FederationBuilder; // ---cut-before--- import type { Federation, Message } from "@fedify/fedify"; -import { WorkersMessageQueue } from "@fedify/fedify/x/cfworkers"; +import { WorkersMessageQueue } from "@fedify/cfworkers"; export default { async fetch(request, env, ctx) { @@ -361,6 +409,7 @@ export default { > process the messages. The `queue()` method is the only way to consume > messages from the queue in Cloudflare Workers. +[`@fedify/cfworkers`]: https://jsr.io/@fedify/cfworkers [Cloudflare Workers]: https://workers.cloudflare.com/ [Cloudflare Queues]: https://developers.cloudflare.com/queues/ diff --git a/docs/manual/send.md b/docs/manual/send.md index 67aea6ec2..3edbcf6c1 100644 --- a/docs/manual/send.md +++ b/docs/manual/send.md @@ -386,8 +386,8 @@ const federation = createFederation({ > [!NOTE] > The `InProcessMessageQueue` is a simple in-memory message queue that is > suitable for development and testing. For production use, you should -> consider using a more robust message queue, such as `DenoKvMessageQueue` -> from `@fedify/fedify/x/deno` module or [`RedisMessageQueue`] from +> consider using a more robust message queue, such as [`DenoKvMessageQueue`] +> from [`@fedify/denokv`] package or [`RedisMessageQueue`] from > [`@fedify/redis`] package. > > For further information, see the [*Message queue* section](./mq.md). @@ -407,8 +407,10 @@ If the `queue` is not set, the `~Context.sendActivity()` method immediately sends the activity to the recipient's inbox. If the delivery fails, it throws an error and does not retry the delivery. +[`DenoKvMessageQueue`]: https://jsr.io/@fedify/denokv/doc/mq/~/DenoKvMessageQueue +[`@fedify/denokv`]: https://github.com/fedify-dev/fedify/tree/main/packages/denokv [`RedisMessageQueue`]: https://jsr.io/@fedify/redis/doc/mq/~/RedisMessageQueue -[`@fedify/redis`]: https://github.com/fedify-dev/redis +[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis Optimizing activity delivery for large audiences diff --git a/docs/package.json b/docs/package.json index 40a872a72..1f7e5f57a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,14 +4,17 @@ "@cloudflare/workers-types": "catalog:", "@deno/kv": "^0.8.4", "@fedify/amqp": "workspace:", + "@fedify/cfworkers": "workspace:", "@fedify/express": "workspace:", "@fedify/fedify": "workspace:", "@fedify/h3": "workspace:", + "@fedify/hono": "workspace:", "@fedify/nestjs": "workspace:", "@fedify/next": "workspace:", "@fedify/postgres": "workspace:", "@fedify/redis": "workspace:", "@fedify/sqlite": "workspace:", + "@fedify/sveltekit": "workspace:", "@fedify/testing": "workspace:", "@hono/node-server": "^1.13.7", "@js-temporal/polyfill": "catalog:", diff --git a/docs/tutorial/basics.md b/docs/tutorial/basics.md index 6556d0b42..dd427219d 100644 --- a/docs/tutorial/basics.md +++ b/docs/tutorial/basics.md @@ -252,8 +252,8 @@ a key–value store. > [!IMPORTANT] > Since `MemoryKvStore` is for testing and development purposes, you should > use a persistent key–value store like `DenoKvStore` (in Deno) or -> [`RedisKvStore`] (from [@fedify/redis] package) or [`PostgresKvStore`] -> (from [@fedify/postgres] package) for production use. +> [`RedisKvStore`] (from [`@fedify/redis`] package) or [`PostgresKvStore`] +> (from [`@fedify/postgres`] package) for production use. > > For further details, see the [*Key–value store* section](../manual/kv.md). @@ -353,9 +353,9 @@ to the next step. > }); > ~~~~ -[@fedify/redis]: https://github.com/fedify-dev/redis +[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis [`RedisKvStore`]: https://jsr.io/@fedify/redis/doc/kv/~/RedisKvStore -[@fedify/postgres]: https://github.com/fedify-dev/postgres +[`@fedify/postgres`]: https://github.com/fedify-dev/fedify/tree/main/packages/postgres [`PostgresKvStore`]: https://jsr.io/@fedify/postgres/doc/kv/~/PostgresKvStore [LogTape]: https://logtape.org/ [`configure()`]: https://jsr.io/@logtape/logtape/doc/~/configure diff --git a/packages/cfworkers/README.md b/packages/cfworkers/README.md new file mode 100644 index 000000000..456c1acf3 --- /dev/null +++ b/packages/cfworkers/README.md @@ -0,0 +1,110 @@ + + +@fedify/cfworkers: Adapt Fedify with Cloudflare Workers +====================================================== + +[![JSR][JSR badge]][JSR] +[![npm][npm badge]][npm] +[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social] + +*This package is available since Fedify 1.9.0.* + +This package provides [Fedify]'s [`KvStore`] and [`MessageQueue`] +implementations for [Cloudflare Workers]: + + - [`WorkersKvStore`] + - [`WorkersMessageQueue`] + +~~~~ typescript +import type { Federation } from "@fedify/fedify"; +import { WorkersKvStore, WorkersMessageQueue } from "@fedify/cfworkers"; + +export default { + async fetch(request, env, ctx) { + const federation = createFederation({ + kv: new WorkersKvStore(env.KV_BINDING), + queue: new WorkersMessageQueue(env.QUEUE_BINDING), + // ... other options + }); + + return federation.handle(request, { contextData: env }); + }, + + async queue(batch, env, ctx) { + const federation = createFederation({ + kv: new WorkersKvStore(env.KV_BINDING), + queue: new WorkersMessageQueue(env.QUEUE_BINDING), + // ... other options + }); + + for (const message of batch.messages) { + await federation.processQueuedTask(message.body); + } + } +} satisfies ExportedHandler<{ + KV_BINDING: KVNamespace; + QUEUE_BINDING: Queue; +}>; +~~~~ + +`WorkersKvStore` +---------------- + +`WorkersKvStore` is a key–value store implementation for [Cloudflare Workers] +that uses Cloudflare's built-in [Cloudflare Workers KV] API. It provides +persistent storage and good performance for Cloudflare Workers environments. +It's suitable for production use in Cloudflare Workers applications. + +`WorkersMessageQueue` +--------------------- + +`WorkersMessageQueue` is a message queue implementation for [Cloudflare Workers] +that uses Cloudflare's built-in [Cloudflare Queues] API. It provides +scalability and high performance, making it suitable for production use in +Cloudflare Workers environments. It requires a Cloudflare Queues setup and +management. + +> [!NOTE] +> Since your `KVNamespace` and `Queue` are not bound to global variables, but rather +> passed as arguments to the `fetch()` and `queue()` methods, you need to instantiate +> your `Federation` object inside these methods, rather than at the top level. +> +> For better organization, you probably want to use a builder pattern to +> register your dispatchers and listeners before instantiating the `Federation` +> object. + +> [!NOTE] +> The [Cloudflare Queues] API does not provide a way to poll messages from +> the queue, so `WorkersMessageQueue.listen()` method always throws +> a `TypeError` when invoked. Instead, you should define a `queue()` method +> in your Cloudflare worker, which will be called by the Cloudflare Queues +> API when new messages are available in the queue. Inside the `queue()` +> method, you need to call `Federation.processQueuedTask()` method to manually +> process the messages. The `queue()` method is the only way to consume +> messages from the queue in Cloudflare Workers. + +Installation +------------ + +~~~~ sh +deno add jsr:@fedify/cfworkers # Deno +npm add @fedify/cfworkers # npm +pnpm add @fedify/cfworkers # pnpm +yarn add @fedify/cfworkers # Yarn +bun add @fedify/cfworkers # Bun +~~~~ + +[JSR]: https://jsr.io/@fedify/cfworkers +[JSR badge]: https://jsr.io/badges/@fedify/cfworkers +[npm]: https://www.npmjs.com/package/@fedify/cfworkers +[npm badge]: https://img.shields.io/npm/v/@fedify/cfworkers?logo=npm +[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg +[@fedify@hollo.social]: https://hollo.social/@fedify +[Fedify]: https://fedify.dev/ +[`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore +[`MessageQueue`]: https://jsr.io/@fedify/fedify/doc/federation/~/MessageQueue +[`WorkersKvStore`]: https://jsr.io/@fedify/cfworkers/doc/~/WorkersKvStore +[`WorkersMessageQueue`]: https://jsr.io/@fedify/cfworkers/doc/~/WorkersMessageQueue +[Cloudflare Workers]: https://workers.cloudflare.com/ +[Cloudflare Workers KV]: https://developers.cloudflare.com/kv/ +[Cloudflare Queues]: https://developers.cloudflare.com/queues/ diff --git a/packages/cfworkers/deno.json b/packages/cfworkers/deno.json new file mode 100644 index 000000000..1b6d70eb4 --- /dev/null +++ b/packages/cfworkers/deno.json @@ -0,0 +1,21 @@ +{ + "name": "@fedify/cfworkers", + "version": "1.9.0", + "license": "MIT", + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.13", + "es-toolkit": "npm:es-toolkit@^1.30.0", + "@cloudflare/workers-types/experimental": "npm:@cloudflare/workers-types@^4.20250529.0/experimental" + }, + "exports": { + ".": "./src/mod.ts" + }, + "exclude": [ + "dist", + "node_modules" + ], + "tasks": { + "check": "deno fmt --check && deno lint && deno check src/*.ts", + "test": "deno test --allow-net --allow-env" + } +} diff --git a/packages/cfworkers/package.json b/packages/cfworkers/package.json new file mode 100644 index 000000000..2b2c465b9 --- /dev/null +++ b/packages/cfworkers/package.json @@ -0,0 +1,67 @@ +{ + "name": "@fedify/cfworkers", + "version": "1.9.0", + "description": "Adapt Fedify with Cloudflare Workers", + "keywords": [ + "Fedify", + "ActivityPub", + "Fediverse", + "Cloudflare Workers" + ], + "author": { + "name": "Hong Minhee", + "email": "hong@minhee.org", + "url": "https://hongminhee.org/" + }, + "homepage": "https://fedify.dev/", + "repository": { + "type": "git", + "url": "git+https://github.com/fedify-dev/fedify.git", + "directory": "packages/cfworkers" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/fedify-dev/fedify/issues" + }, + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "type": "module", + "main": "./dist/mod.js", + "module": "./dist/mod.js", + "types": "./dist/mod.d.ts", + "exports": { + ".": { + "require": { + "types": "./dist/mod.d.ts", + "import": "./dist/mod.js", + "default": "./dist/mod.js" + }, + "import": { + "types": "./dist/mod.d.ts", + "import": "./dist/mod.js", + "default": "./dist/mod.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "package.json" + ], + "peerDependencies": { + "@fedify/fedify": "workspace:", + "@cloudflare/workers-types": "catalog:" + }, + "devDependencies": { + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "scripts": { + "build": "tsdown", + "prepack": "tsdown", + "prepublish": "tsdown", + "test": "deno task codegen && tsdown && cd dist/ && node --test" + } +} diff --git a/packages/cfworkers/src/mod.test.ts b/packages/cfworkers/src/mod.test.ts new file mode 100644 index 000000000..bfa105788 --- /dev/null +++ b/packages/cfworkers/src/mod.test.ts @@ -0,0 +1,83 @@ +import type { + KVNamespace, + Queue, +} from "@cloudflare/workers-types/experimental"; +import { delay } from "es-toolkit"; +import { deepStrictEqual, strictEqual } from "node:assert/strict"; +import { describe, test } from "node:test"; +import { WorkersKvStore, WorkersMessageQueue } from "./mod.ts"; + +describe("WorkersKvStore", { + skip: !("navigator" in globalThis && + navigator.userAgent === "Cloudflare-Workers"), +}, () => { + test("set() & get()", async (t) => { + const { env } = t as unknown as { + env: Record>; + }; + const store = new WorkersKvStore(env.KV1); + + await store.set(["foo", "bar"], { foo: 1, bar: 2 }); + deepStrictEqual(await store.get(["foo", "bar"]), { foo: 1, bar: 2 }); + strictEqual(await store.get(["foo"]), undefined); + + await store.set(["foo", "baz"], "baz", { + ttl: Temporal.Duration.from({ seconds: 0 }), + }); + strictEqual(await store.get(["foo", "baz"]), undefined); + }); + + test("delete()", async (t) => { + const { env } = t as unknown as { + env: Record>; + }; + const store = new WorkersKvStore(env.KV1); + + await store.delete(["foo", "bar"]); + strictEqual(await store.get(["foo", "bar"]), undefined); + }); +}); + +describe("WorkersMessageQueue", { + skip: !("navigator" in globalThis && + navigator.userAgent === "Cloudflare-Workers"), +}, () => { + test("message queue operations", async (t) => { + const { env, messageBatches } = t as unknown as { + env: Record; + messageBatches: MessageBatch[]; + }; + const queue = new WorkersMessageQueue(env.Q1); + await queue.enqueue({ foo: 1, bar: 2 }); + await waitFor(() => messageBatches.length > 0, 5000); + strictEqual(messageBatches.length, 1); + strictEqual(messageBatches[0].queue, "Q1"); + strictEqual(messageBatches[0].messages.length, 1); + deepStrictEqual(messageBatches[0].messages[0].body, { foo: 1, bar: 2 }); + + await queue.enqueue( + { baz: 3, qux: 4 }, + { delay: Temporal.Duration.from({ seconds: 3 }) }, + ); + await delay(2000); + strictEqual(messageBatches.length, 1); + await waitFor(() => messageBatches.length > 1, 6000); + strictEqual(messageBatches[1].queue, "Q1"); + strictEqual(messageBatches[1].messages.length, 1); + deepStrictEqual(messageBatches[1].messages[0].body, { baz: 3, qux: 4 }); + }); +}); + +async function waitFor( + predicate: () => boolean, + timeoutMs: number, +): Promise { + let delayed = 0; + while (!predicate()) { + await delay(500); + delayed += 500; + if (delayed > timeoutMs) { + throw new Error("Timeout"); + } + } +} diff --git a/packages/cfworkers/src/mod.ts b/packages/cfworkers/src/mod.ts new file mode 100644 index 000000000..b72cfb784 --- /dev/null +++ b/packages/cfworkers/src/mod.ts @@ -0,0 +1,146 @@ +/** + * `KvStore` & `MessageQueue` adapters for Cloudflare Workers + * ========================================================== + * + * This package provides `KvStore` and `MessageQueue` implementations that use + * Cloudflare Workers' KV and Queues bindings, respectively. + * + * @module + * @since 1.9.0 + */ +import type { + KVNamespace, + MessageSendRequest, + Queue, +} from "@cloudflare/workers-types/experimental"; +import type { + KvKey, + KvStore, + KvStoreSetOptions, + MessageQueue, + MessageQueueEnqueueOptions, + MessageQueueListenOptions, +} from "@fedify/fedify/federation"; + +interface KvMetadata { + expires?: number; +} + +/** + * Implementation of the {@link KvStore} interface for Cloudflare Workers KV + * binding. This class provides a wrapper around Cloudflare's KV namespace to + * store and retrieve JSON-serializable values using structured keys. + * + * Note that this implementation does not support the {@link KvStore.cas} + * operation, as Cloudflare Workers KV does not support atomic compare-and-swap + * operations. If you need this functionality, consider using a different + * key–value store that supports atomic operations. + * @since 1.9.0 + */ +export class WorkersKvStore implements KvStore { + #namespace: KVNamespace; + + constructor(namespace: KVNamespace) { + this.#namespace = namespace; + } + + #encodeKey(key: KvKey): string { + return JSON.stringify(key); + } + + async get(key: KvKey): Promise { + const encodedKey = this.#encodeKey(key); + const { value, metadata } = await this.#namespace.getWithMetadata( + encodedKey, + "json", + ); + return metadata == null || metadata.expires < Date.now() + ? undefined + : value as T; + } + + async set( + key: KvKey, + value: unknown, + options?: KvStoreSetOptions, + ): Promise { + const encodedKey = this.#encodeKey(key); + const metadata: KvMetadata = options?.ttl == null ? {} : { + expires: Date.now() + options.ttl.total("milliseconds"), + }; + await this.#namespace.put( + encodedKey, + JSON.stringify(value), + options?.ttl == null ? { metadata } : { + // According to Cloudflare Workers KV documentation, + // the minimum TTL is 60 seconds: + expirationTtl: Math.max(options.ttl.total("seconds"), 60), + metadata, + }, + ); + } + + delete(key: KvKey): Promise { + return this.#namespace.delete(this.#encodeKey(key)); + } +} + +/** + * Implementation of the {@link MessageQueue} interface for Cloudflare + * Workers Queues binding. This class provides a wrapper around Cloudflare's + * Queues to send messages to a queue. + * + * Note that this implementation does not support the `listen()` method, + * as Cloudflare Workers Queues do not support message consumption in the same + * way as other message queue systems. Instead, you should use + * the {@link Federation.processQueuedTask} method to process messages + * passed to the queue. + * @since 1.9.0 + */ +export class WorkersMessageQueue implements MessageQueue { + #queue: Queue; + + /** + * Cloudflare Queues provide automatic retry with exponential backoff + * and Dead Letter Queues. + * @since 1.7.0 + */ + readonly nativeRetrial = true; + + constructor(queue: Queue) { + this.#queue = queue; + } + + // deno-lint-ignore no-explicit-any + enqueue(message: any, options?: MessageQueueEnqueueOptions): Promise { + return this.#queue.send(message, { + contentType: "json", + delaySeconds: options?.delay?.total("seconds") ?? 0, + }); + } + + enqueueMany( + // deno-lint-ignore no-explicit-any + messages: any[], + options?: MessageQueueEnqueueOptions, + ): Promise { + const requests: MessageSendRequest[] = messages.map((msg) => ({ + body: msg, + contentType: "json", + })); + return this.#queue.sendBatch(requests, { + delaySeconds: options?.delay?.total("seconds") ?? 0, + }); + } + + listen( + // deno-lint-ignore no-explicit-any + _handler: (message: any) => Promise | void, + _options?: MessageQueueListenOptions, + ): Promise { + throw new TypeError( + "WorkersMessageQueue does not support listen(). " + + "Use Federation.processQueuedTask() method instead.", + ); + } +} diff --git a/packages/cfworkers/tsdown.config.ts b/packages/cfworkers/tsdown.config.ts new file mode 100644 index 000000000..88d29afa7 --- /dev/null +++ b/packages/cfworkers/tsdown.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/mod.ts"], + dts: true, + platform: "node", +}); diff --git a/packages/denokv/README.md b/packages/denokv/README.md new file mode 100644 index 000000000..fdebab376 --- /dev/null +++ b/packages/denokv/README.md @@ -0,0 +1,65 @@ + + +@fedify/denokv: Adapt Fedify with Deno KV +========================================= + +[![JSR][JSR badge]][JSR] +[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social] + +*This package is available since Fedify 1.9.0.* + +This package provides [Fedify]'s [`KvStore`] and [`MessageQueue`] +implementations for [Deno] runtime that uses Deno's built-in [`Deno.openKv()`] API: + + - [`DenoKvStore`] + - [`DenoKvMessageQueue`] + +~~~~ typescript +import { createFederation } from "@fedify/fedify"; +import { DenoKvStore, DenoKvMessageQueue } from "@fedify/denokv"; + +const kv = await Deno.openKv(); + +const federation = createFederation({ + kv: new DenoKvStore(kv), + queue: new DenoKvMessageQueue(kv), + // ... other options +}); +~~~~ + +`DenoKvStore` +------------- + +`DenoKvStore` is a key–value store implementation for [Deno] runtime that uses +Deno's built-in [`Deno.openKv()`] API. It provides persistent storage and good +performance for Deno environments. It's suitable for production use in Deno +applications. + +`DenoKvMessageQueue` +-------------------- + +`DenoKvMessageQueue` is a message queue implementation for [Deno] runtime that +uses Deno's built-in [`Deno.openKv()`] API. It provides persistent storage and +good performance for Deno environments. It's suitable for production use in +Deno applications. + + +Installation +------------ + +~~~~ sh +deno add jsr:@fedify/denokv # Deno +~~~~ + + +[JSR]: https://jsr.io/@fedify/denokv +[JSR badge]: https://jsr.io/badges/@fedify/denokv +[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg +[@fedify@hollo.social]: https://hollo.social/@fedify +[Fedify]: https://fedify.dev/ +[`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore +[`MessageQueue`]: https://jsr.io/@fedify/fedify/doc/federation/~/MessageQueue +[`DenoKvStore`]: https://jsr.io/@fedify/denokv/doc/~/DenoKvStore +[`DenoKvMessageQueue`]: https://jsr.io/@fedify/denokv/doc/~/DenoKvMessageQueue +[Deno]: https://deno.com/ +[`Deno.openKv()`]: https://docs.deno.com/api/deno/~/Deno.openKv diff --git a/packages/denokv/deno.json b/packages/denokv/deno.json new file mode 100644 index 000000000..1955bf238 --- /dev/null +++ b/packages/denokv/deno.json @@ -0,0 +1,20 @@ +{ + "name": "@fedify/denokv", + "version": "1.9.0", + "license": "MIT", + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.13", + "es-toolkit": "npm:es-toolkit@^1.30.0" + }, + "exports": { + ".": "./src/mod.ts" + }, + "exclude": [ + "dist", + "node_modules" + ], + "tasks": { + "check": "deno fmt --check && deno lint && deno check src/*.ts", + "test": "deno test --allow-net --allow-env" + } +} diff --git a/packages/denokv/src/mod.test.ts b/packages/denokv/src/mod.test.ts new file mode 100644 index 000000000..befbec047 --- /dev/null +++ b/packages/denokv/src/mod.test.ts @@ -0,0 +1,92 @@ +import { assertEquals, assertGreater } from "@std/assert"; +import { delay } from "es-toolkit"; +import { DenoKvMessageQueue, DenoKvStore } from "./mod.ts"; + +Deno.test("DenoKvStore", async (t) => { + const kv = await Deno.openKv(":memory:"); + const store = new DenoKvStore(kv); + + await t.step("get()", async () => { + await kv.set(["foo", "bar"], "foobar"); + assertEquals(await store.get(["foo", "bar"]), "foobar"); + }); + + await t.step("set()", async () => { + await store.set(["foo", "baz"], "baz"); + assertEquals((await kv.get(["foo", "baz"])).value, "baz"); + }); + + await t.step("delete()", async () => { + await store.delete(["foo", "baz"]); + assertEquals((await kv.get(["foo", "baz"])).value, null); + }); + + await t.step("cas()", async () => { + await store.set(["foo", "bar"], "foobar"); + assertEquals(await store.cas(["foo", "bar"], "bar", "baz"), false); + assertEquals(await store.get(["foo", "bar"]), "foobar"); + assertEquals(await store.cas(["foo", "bar"], "foobar", "baz"), true); + assertEquals(await store.get(["foo", "bar"]), "baz"); + await store.delete(["foo", "bar"]); + assertEquals(await store.cas(["foo", "bar"], "foobar", "baz"), false); + assertEquals(await store.get(["foo", "bar"]), undefined); + assertEquals(await store.cas(["foo", "bar"], undefined, "baz"), true); + assertEquals(await store.get(["foo", "bar"]), "baz"); + }); + + kv.close(); +}); + +Deno.test("DenoKvMessageQueue", async (t) => { + const kv = await Deno.openKv(":memory:"); + const mq = new DenoKvMessageQueue(kv); + + const messages: string[] = []; + const controller = new AbortController(); + const listening = mq.listen((message: string) => { + messages.push(message); + }, { signal: controller.signal }); + + await t.step("enqueue()", async () => { + await mq.enqueue("Hello, world!"); + }); + + await waitFor(() => messages.length > 0, 15_000); + + await t.step("listen()", () => { + assertEquals(messages, ["Hello, world!"]); + }); + + let started = 0; + await t.step("enqueue() with delay", async () => { + started = Date.now(); + await mq.enqueue( + "Delayed message", + { delay: Temporal.Duration.from({ seconds: 3 }) }, + ); + }); + + await waitFor(() => messages.length > 1, 15_000); + + await t.step("listen() with delay", () => { + assertEquals(messages, ["Hello, world!", "Delayed message"]); + assertGreater(Date.now() - started, 3_000); + }); + + controller.abort(); + await listening; + mq[Symbol.dispose](); +}); + +async function waitFor( + predicate: () => boolean, + timeoutMs: number, +): Promise { + const started = Date.now(); + while (!predicate()) { + await delay(500); + if (Date.now() - started > timeoutMs) { + throw new Error("Timeout"); + } + } +} diff --git a/packages/denokv/src/mod.ts b/packages/denokv/src/mod.ts new file mode 100644 index 000000000..a9e0c4c78 --- /dev/null +++ b/packages/denokv/src/mod.ts @@ -0,0 +1,156 @@ +/** + * `KvStore` & `MessageQueue` adapters for Deno's KV store + * ======================================================= + * + * This package provides `KvStore` and `MessageQueue` implementations that use + * Deno's KV store. The `DenoKvStore` class implements the `KvStore` interface + * using Deno's KV store, and the `DenoKvMessageQueue` class implements the + * `MessageQueue` interface using Deno's KV store. + * + * @module + * @since 1.9.0 + */ +import type { + KvKey, + KvStore, + KvStoreSetOptions, + MessageQueue, + MessageQueueEnqueueOptions, + MessageQueueListenOptions, +} from "@fedify/fedify/federation"; +import { isEqual } from "es-toolkit"; + +/** + * Represents a key–value store implementation using Deno's KV store. + * + * @since 1.9.0 + */ +export class DenoKvStore implements KvStore { + #kv: Deno.Kv; + + /** + * Constructs a new {@link DenoKvStore} adapter with the given Deno KV store. + * @param kv The Deno KV store to use. + */ + constructor(kv: Deno.Kv) { + this.#kv = kv; + } + + /** + * {@inheritDoc KvStore.set} + */ + async get(key: KvKey): Promise { + const entry = await this.#kv.get(key); + return entry == null || entry.value == null ? undefined : entry.value; + } + + /** + * {@inheritDoc KvStore.set} + */ + async set( + key: KvKey, + value: unknown, + options?: KvStoreSetOptions, + ): Promise { + await this.#kv.set( + key, + value, + options?.ttl == null ? undefined : { + expireIn: options.ttl.total("millisecond"), + }, + ); + } + + /** + * {@inheritDoc KvStore.delete} + */ + delete(key: KvKey): Promise { + return this.#kv.delete(key); + } + + /** + * {@inheritDoc KvStore.cas} + */ + async cas( + key: KvKey, + expectedValue: unknown, + newValue: unknown, + options?: KvStoreSetOptions, + ): Promise { + while (true) { + const entry = await this.#kv.get(key); + if (!isEqual(entry.value ?? undefined, expectedValue)) return false; + const result = await this.#kv.atomic() + .check(entry) + .set( + key, + newValue, + options?.ttl == null ? undefined : { + expireIn: options.ttl.total("millisecond"), + }, + ) + .commit(); + if (result.ok) return true; + } + } +} + +/** + * Represents a message queue adapter that uses Deno KV store. + * + * @since 1.9.0 + */ +export class DenoKvMessageQueue implements MessageQueue, Disposable { + #kv: Deno.Kv; + + /** + * Deno KV queues provide automatic retry with exponential backoff. + * @since 1.7.0 + */ + readonly nativeRetrial = true; + + /** + * Constructs a new {@link DenoKvMessageQueue} adapter with the given Deno KV + * store. + * @param kv The Deno KV store to use. + */ + constructor(kv: Deno.Kv) { + this.#kv = kv; + } + + async enqueue( + // deno-lint-ignore no-explicit-any + message: any, + options?: MessageQueueEnqueueOptions | undefined, + ): Promise { + await this.#kv.enqueue( + message, + options?.delay == null ? undefined : { + delay: Math.max(options.delay.total("millisecond"), 0), + }, + ); + } + + listen( + // deno-lint-ignore no-explicit-any + handler: (message: any) => void | Promise, + options: MessageQueueListenOptions = {}, + ): Promise { + options.signal?.addEventListener("abort", () => { + try { + this.#kv.close(); + } catch (e) { + if (!(e instanceof Deno.errors.BadResource)) throw e; + } + }, { once: true }); + return this.#kv.listenQueue(handler); + } + + [Symbol.dispose](): void { + try { + this.#kv.close(); + } catch (e) { + if (!(e instanceof Deno.errors.BadResource)) throw e; + } + } +} diff --git a/packages/fedify/README.md b/packages/fedify/README.md index aed769b8e..0302b9b1e 100644 --- a/packages/fedify/README.md +++ b/packages/fedify/README.md @@ -90,20 +90,24 @@ which provides the core functionality of the framework. Other packages provide integrations with various web frameworks, database drivers, and other features. Here is the list of packages: -| Package | JSR | npm | Description | -| --------------------------------------- | --------------------------- | --------------------------- | --------------------------------------- | -| [@fedify/fedify](/packages/fedify/) | [JSR] | [npm] | The core framework of Fedify | -| [@fedify/cli](/packages/cli/) | [JSR][jsr:@fedify/cli] | [npm][npm:@fedify/cli] | CLI toolchain for testing and debugging | -| [@fedify/amqp](/packages/amqp/) | [JSR][jsr:@fedify/amqp] | [npm][npm:@fedify/amqp] | AMQP/RabbitMQ driver | -| [@fedify/elysia](/packages/elysia/) | | [npm][npm:@fedify/elysia] | Elysia integration | -| [@fedify/express](/packages/express/) | [JSR][jsr:@fedify/express] | [npm][npm:@fedify/express] | Express integration | -| [@fedify/h3](/packages/h3/) | [JSR][jsr:@fedify/h3] | [npm][npm:@fedify/h3] | H3 integration | -| [@fedify/nestjs](/packages/nestjs/) | | [npm][npm:@fedify/nestjs] | NestJS integration | -| [@fedify/next](/packages/next/) | | [npm][npm:@fedify/next] | Next.js integration | -| [@fedify/postgres](/packages/postgres/) | [JSR][jsr:@fedify/postgres] | [npm][npm:@fedify/postgres] | PostgreSQL driver | -| [@fedify/redis](/packages/redis/) | [JSR][jsr:@fedify/redis] | [npm][npm:@fedify/redis] | Redis driver | -| [@fedify/sqlite](/packages/sqlite/) | [JSR][jsr:@fedify/sqlite] | [npm][npm:@fedify/sqlite] | SQLite driver | -| [@fedify/testing](/packages/testing/) | [JSR][jsr:@fedify/testing] | [npm][npm:@fedify/testing] | Testing utilities | +| Package | JSR | npm | Description | +| --------------------------------------- | --------------------------- | --------------------------- | --------------------------------------- | +| [@fedify/fedify](/packages/fedify/) | [JSR] | [npm] | The core framework of Fedify | +| [@fedify/cli](/packages/cli/) | [JSR][jsr:@fedify/cli] | [npm][npm:@fedify/cli] | CLI toolchain for testing and debugging | +| [@fedify/amqp](/packages/amqp/) | [JSR][jsr:@fedify/amqp] | [npm][npm:@fedify/amqp] | AMQP/RabbitMQ driver | +| [@fedify/cfworkers](/packages/cfworkers/) | | | Cloudflare Workers integration | +| [@fedify/denokv](/packages/denokv/) | | | Deno KV integration | +| [@fedify/elysia](/packages/elysia/) | | [npm][npm:@fedify/elysia] | Elysia integration | +| [@fedify/express](/packages/express/) | [JSR][jsr:@fedify/express] | [npm][npm:@fedify/express] | Express integration | +| [@fedify/h3](/packages/h3/) | [JSR][jsr:@fedify/h3] | [npm][npm:@fedify/h3] | H3 integration | +| [@fedify/hono](/packages/hono/) | | | Hono integration | +| [@fedify/nestjs](/packages/nestjs/) | | [npm][npm:@fedify/nestjs] | NestJS integration | +| [@fedify/next](/packages/next/) | | [npm][npm:@fedify/next] | Next.js integration | +| [@fedify/postgres](/packages/postgres/) | [JSR][jsr:@fedify/postgres] | [npm][npm:@fedify/postgres] | PostgreSQL driver | +| [@fedify/redis](/packages/redis/) | [JSR][jsr:@fedify/redis] | [npm][npm:@fedify/redis] | Redis driver | +| [@fedify/sqlite](/packages/sqlite/) | [JSR][jsr:@fedify/sqlite] | [npm][npm:@fedify/sqlite] | SQLite driver | +| [@fedify/sveltekit](/packages/sveltekit/) | | | SvelteKit integration | +| [@fedify/testing](/packages/testing/) | [JSR][jsr:@fedify/testing] | [npm][npm:@fedify/testing] | Testing utilities | [jsr:@fedify/cli]: https://jsr.io/@fedify/cli [npm:@fedify/cli]: https://www.npmjs.com/package/@fedify/cli diff --git a/packages/fedify/src/x/cfworkers.ts b/packages/fedify/src/x/cfworkers.ts index 90af04eff..9f029df27 100644 --- a/packages/fedify/src/x/cfworkers.ts +++ b/packages/fedify/src/x/cfworkers.ts @@ -5,6 +5,10 @@ * This module provides `KvStore` and `MessageQueue` implementations that use * Cloudflare Workers' KV and Queues bindings, respectively. * + * @deprecated This module has been moved to a separate package. + * Install and import from `@fedify/cfworkers` instead. + * This module will be removed in Fedify v2.0. + * * @module * @since 1.6.0 */ @@ -13,6 +17,7 @@ import type { MessageSendRequest, Queue, } from "@cloudflare/workers-types/experimental"; +import { getLogger } from "@logtape/logtape"; import type { KvKey, KvStore, KvStoreSetOptions } from "../federation/kv.ts"; import type { MessageQueue, @@ -33,12 +38,21 @@ interface KvMetadata { * operation, as Cloudflare Workers KV does not support atomic compare-and-swap * operations. If you need this functionality, consider using a different * key–value store that supports atomic operations. + * + * @deprecated This class has been moved to `@fedify/cfworkers` package. + * Import `WorkersKvStore` from `@fedify/cfworkers` instead. + * This class will be removed in Fedify v2.0. * @since 1.6.0 */ export class WorkersKvStore implements KvStore { #namespace: KVNamespace; constructor(namespace: KVNamespace) { + const logger = getLogger(["fedify", "cfworkers"]); + logger.warn( + "The `@fedify/fedify/x/cfworkers` module is deprecated; use " + + "`WorkersKvStore` from `@fedify/cfworkers` package instead.", + ); this.#namespace = namespace; } @@ -93,6 +107,10 @@ export class WorkersKvStore implements KvStore { * way as other message queue systems. Instead, you should use * the {@link Federation.processQueuedTask} method to process messages * passed to the queue. + * + * @deprecated This class has been moved to `@fedify/cfworkers` package. + * Import `WorkersMessageQueue` from `@fedify/cfworkers` instead. + * This class will be removed in Fedify v2.0. * @since 1.6.0 */ export class WorkersMessageQueue implements MessageQueue { @@ -106,6 +124,11 @@ export class WorkersMessageQueue implements MessageQueue { readonly nativeRetrial = true; constructor(queue: Queue) { + const logger = getLogger(["fedify", "cfworkers"]); + logger.warn( + "The `@fedify/fedify/x/cfworkers` module is deprecated; use " + + "`WorkersMessageQueue` from `@fedify/cfworkers` package instead.", + ); this.#queue = queue; } diff --git a/packages/fedify/src/x/denokv.ts b/packages/fedify/src/x/denokv.ts index b503bd160..095909460 100644 --- a/packages/fedify/src/x/denokv.ts +++ b/packages/fedify/src/x/denokv.ts @@ -7,9 +7,14 @@ * using Deno's KV store, and the `DenoKvMessageQueue` class implements the * `MessageQueue` interface using Deno's KV store. * + * @deprecated This module has been moved to a separate package. + * Install and import from `@fedify/denokv` instead. + * This module will be removed in Fedify v2.0. + * * @module * @since 0.5.0 */ +import { getLogger } from "@logtape/logtape"; import { isEqual } from "es-toolkit"; import type { KvKey, KvStore, KvStoreSetOptions } from "../federation/kv.ts"; import type { @@ -20,6 +25,10 @@ import type { /** * Represents a key–value store implementation using Deno's KV store. + * + * @deprecated This class has been moved to `@fedify/denokv` package. + * Import `DenoKvStore` from `@fedify/denokv` instead. + * This class will be removed in Fedify v2.0. */ export class DenoKvStore implements KvStore { #kv: Deno.Kv; @@ -29,6 +38,11 @@ export class DenoKvStore implements KvStore { * @param kv The Deno KV store to use. */ constructor(kv: Deno.Kv) { + const logger = getLogger(["fedify", "denokv"]); + logger.warn( + "The `@fedify/fedify/x/denokv` module is deprecated; use `DenoKvStore` " + + "from `@fedify/denokv` package instead.", + ); this.#kv = kv; } @@ -93,6 +107,10 @@ export class DenoKvStore implements KvStore { /** * Represents a message queue adapter that uses Deno KV store. + * + * @deprecated This class has been moved to `@fedify/denokv` package. + * Import `DenoKvMessageQueue` from `@fedify/denokv` instead. + * This class will be removed in Fedify v2.0. */ export class DenoKvMessageQueue implements MessageQueue, Disposable { #kv: Deno.Kv; @@ -109,6 +127,11 @@ export class DenoKvMessageQueue implements MessageQueue, Disposable { * @param kv The Deno KV store to use. */ constructor(kv: Deno.Kv) { + const logger = getLogger(["fedify", "denokv"]); + logger.warn( + "The `@fedify/fedify/x/denokv` module is deprecated; use " + + "`DenoKvMessageQueue` from `@fedify/denokv` package instead.", + ); this.#kv = kv; } diff --git a/packages/fedify/src/x/fresh.ts b/packages/fedify/src/x/fresh.ts index b004c97f2..b028d4504 100644 --- a/packages/fedify/src/x/fresh.ts +++ b/packages/fedify/src/x/fresh.ts @@ -7,6 +7,12 @@ * * [Fresh]: https://fresh.deno.dev/ * + * @deprecated This module is deprecated due to significant breaking changes + * in Fresh v2. A new integration for Fresh v2 will be implemented + * in a separate package in the future. Consider migrating to other + * frameworks supported by Fedify in the meantime. + * This module will be removed in Fedify v2.0. + * * @module * @since 0.4.0 */ @@ -23,6 +29,11 @@ interface FreshContext { * Create options for the {@link Federation.fetch} method to integrate with * Fresh. * + * @deprecated This function is deprecated due to significant breaking changes + * in Fresh v2. A new integration for Fresh v2 will be implemented + * in a separate package in the future. + * This function will be removed in Fedify v2.0. + * * @example _middleware.ts * ``` typescript * import { integrateFetchOptions } from "@fedify/fedify/x/fresh"; @@ -76,6 +87,11 @@ export function integrateFetchOptions( * Create a Fresh middleware handler to integrate with the {@link Federation} * object. * + * @deprecated This function is deprecated due to significant breaking changes + * in Fresh v2. A new integration for Fresh v2 will be implemented + * in a separate package in the future. + * This function will be removed in Fedify v2.0. + * * @example _middleware.ts * ``` typescript * import { integrateHandler } from "@fedify/fedify/x/fresh"; diff --git a/packages/fedify/src/x/hono.ts b/packages/fedify/src/x/hono.ts index d95eb9261..aea0400ec 100644 --- a/packages/fedify/src/x/hono.ts +++ b/packages/fedify/src/x/hono.ts @@ -6,9 +6,14 @@ * * [Hono]: https://hono.dev/ * + * @deprecated This module has been moved to a separate package. + * Install and import from `@fedify/hono` instead. + * This module will be removed in Fedify v2.0. + * * @module * @since 0.6.0 */ +import { getLogger } from "@logtape/logtape"; import type { Federation, FederationFetchOptions, @@ -32,6 +37,10 @@ type HonoMiddleware = ( * A factory function to create a context data for the {@link Federation} * object. * + * @deprecated This type has been moved to `@fedify/hono` package. + * Import `ContextDataFactory` from `@fedify/hono` instead. + * This type will be removed in Fedify v2.0. + * * @template TContextData A type of the context data for the {@link Federation} * object. * @template THonoContext A type of the Hono context. @@ -45,6 +54,10 @@ export type ContextDataFactory = ( /** * Create a Hono middleware to integrate with the {@link Federation} object. * + * @deprecated This function has been moved to `@fedify/hono` package. + * Import `federation` from `@fedify/hono` instead. + * This function will be removed in Fedify v2.0. + * * @template TContextData A type of the context data for the {@link Federation} * object. * @template THonoContext A type of the Hono context. @@ -57,6 +70,11 @@ export function federation( federation: Federation, contextDataFactory: ContextDataFactory, ): HonoMiddleware { + const logger = getLogger(["fedify", "federation", "hono"]); + logger.warn( + "The `@fedify/fedify/x/hono` module is deprecated; use `federation` from " + + "`@fedify/hono` package instead.", + ); return async (ctx, next) => { let contextData = contextDataFactory(ctx); if (contextData instanceof Promise) contextData = await contextData; diff --git a/packages/fedify/src/x/sveltekit.ts b/packages/fedify/src/x/sveltekit.ts index 6573191f1..dec585a06 100644 --- a/packages/fedify/src/x/sveltekit.ts +++ b/packages/fedify/src/x/sveltekit.ts @@ -6,10 +6,14 @@ * * [SvelteKit]: https://kit.svelte.dev/ * + * @deprecated This module has been moved to a separate package. + * Install and import from `@fedify/sveltekit` instead. + * This module will be removed in Fedify v2.0. + * * @module * @since 1.3.0 */ - +import { getLogger } from "@logtape/logtape"; import type { Federation, FederationFetchOptions, @@ -28,6 +32,10 @@ type HookParams = { * Create a SvelteKit hook handler to integrate with the {@link Federation} * object. * + * @deprecated This function has been moved to `@fedify/sveltekit` package. + * Import `fedifyHook` from `@fedify/sveltekit` instead. + * This function will be removed in Fedify v2.0. + * * @example hooks.server.ts * ``` typescript * import { federation } from "./federation"; // Import the `Federation` object @@ -49,6 +57,11 @@ export function fedifyHook( event: RequestEvent, ) => TContextData | Promise, ): (params: HookParams) => Promise { + const logger = getLogger(["fedify", "federation", "sveltekit"]); + logger.warn( + "The `@fedify/fedify/x/sveltekit` module is deprecated; use `fedifyHook` " + + "from `@fedify/sveltekit` package instead.", + ); return async ({ event, resolve }: HookParams) => { return await federation.fetch(event.request, { contextData: await createContextData(event), diff --git a/packages/hono/README.md b/packages/hono/README.md new file mode 100644 index 000000000..d7c2c0da4 --- /dev/null +++ b/packages/hono/README.md @@ -0,0 +1,65 @@ + + +@fedify/hono: Integrate Fedify with Hono +======================================== + +[![JSR][JSR badge]][JSR] +[![npm][npm badge]][npm] +[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social] + +*This package is available since Fedify 1.9.0.* + +This package provides a simple way to integrate [Fedify] with [Hono]. + +The integration code looks like this: + +~~~~ typescript +import { createFederation } from "@fedify/fedify"; +import { federation } from "@fedify/hono"; +import { Hono } from "hono"; + +const fedi = createFederation({ + // Omitted for brevity; see the related section for details. +}); + +const app = new Hono(); +app.use(federation(fedi, (ctx) => "context data")); +~~~~ + +How it works +------------ + +Fedify behaves as a middleware that wraps around the Hono request handler. +The middleware intercepts the incoming HTTP requests and dispatches them to +the appropriate handler based on the request path and the `Accept` header +(i.e., content negotiation). This architecture allows Fedify and your Hono +application to coexist in the same domain and port. + +For example, if you make a request to */.well-known/webfinger* Fedify will +handle the request by itself, but if you make a request to */users/alice* +(assuming your Hono app has a handler for `/users/:handle`) with `Accept: +text/html` header, Fedify will dispatch the request to the Hono app's +appropriate handler for `/users/:handle`. Or if you define an actor dispatcher +for `/users/{handle}` in Fedify, and the request is made with `Accept: +application/activity+json` header, Fedify will dispatch the request to the +appropriate actor dispatcher. + +Installation +------------ + +~~~~ sh +deno add jsr:@fedify/hono # Deno +npm add @fedify/hono # npm +pnpm add @fedify/hono # pnpm +yarn add @fedify/hono # Yarn +bun add @fedify/hono # Bun +~~~~ + +[JSR]: https://jsr.io/@fedify/hono +[JSR badge]: https://jsr.io/badges/@fedify/hono +[npm]: https://www.npmjs.com/package/@fedify/hono +[npm badge]: https://img.shields.io/npm/v/@fedify/hono?logo=npm +[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg +[@fedify@hollo.social]: https://hollo.social/@fedify +[Fedify]: https://fedify.dev/ +[Hono]: https://hono.dev/ diff --git a/packages/hono/deno.json b/packages/hono/deno.json new file mode 100644 index 000000000..4abca7aa5 --- /dev/null +++ b/packages/hono/deno.json @@ -0,0 +1,19 @@ +{ + "name": "@fedify/hono", + "version": "1.9.0", + "license": "MIT", + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.13" + }, + "exports": { + ".": "./src/mod.ts" + }, + "exclude": [ + "dist", + "node_modules" + ], + "tasks": { + "check": "deno fmt --check && deno lint && deno check src/*.ts", + "test": "deno test --allow-net --allow-env" + } +} diff --git a/packages/hono/package.json b/packages/hono/package.json new file mode 100644 index 000000000..d31ff9dc7 --- /dev/null +++ b/packages/hono/package.json @@ -0,0 +1,67 @@ +{ + "name": "@fedify/hono", + "version": "1.9.0", + "description": "Integrate Fedify with Hono", + "keywords": [ + "Fedify", + "ActivityPub", + "Fediverse", + "Hono" + ], + "author": { + "name": "Hong Minhee", + "email": "hong@minhee.org", + "url": "https://hongminhee.org/" + }, + "homepage": "https://fedify.dev/", + "repository": { + "type": "git", + "url": "git+https://github.com/fedify-dev/fedify.git", + "directory": "packages/hono" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/fedify-dev/fedify/issues" + }, + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "type": "module", + "main": "./dist/mod.js", + "module": "./dist/mod.js", + "types": "./dist/mod.d.ts", + "exports": { + ".": { + "require": { + "types": "./dist/mod.d.ts", + "import": "./dist/mod.js", + "default": "./dist/mod.js" + }, + "import": { + "types": "./dist/mod.d.ts", + "import": "./dist/mod.js", + "default": "./dist/mod.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "package.json" + ], + "peerDependencies": { + "@fedify/fedify": "workspace:", + "hono": "catalog:" + }, + "devDependencies": { + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "scripts": { + "build": "tsdown", + "prepack": "tsdown", + "prepublish": "tsdown", + "test": "deno task codegen && tsdown && cd dist/ && node --test" + } +} diff --git a/packages/hono/src/mod.test.ts b/packages/hono/src/mod.test.ts new file mode 100644 index 000000000..1db15697b --- /dev/null +++ b/packages/hono/src/mod.test.ts @@ -0,0 +1,96 @@ +import { strictEqual } from "node:assert/strict"; +import { describe, test } from "node:test"; +import { type ContextDataFactory, federation } from "./mod.ts"; + +interface MockHonoContext { + req: { + raw: Request; + }; + res: Response; +} + +interface MockFederation { + fetch(request: Request, options: unknown): Promise; +} + +describe("federation middleware", () => { + test("creates middleware function", () => { + const mockFederation: MockFederation = { + fetch: () => Promise.resolve(new Response("OK")), + }; + + const contextDataFactory: ContextDataFactory = + () => undefined; + + const middleware = federation(mockFederation as never, contextDataFactory); + strictEqual(typeof middleware, "function"); + }); + + test("calls federation.fetch with correct parameters", async () => { + let capturedRequest: Request | undefined; + let capturedOptions: unknown; + + const mockFederation: MockFederation = { + fetch: (request, options) => { + capturedRequest = request; + capturedOptions = options; + return Promise.resolve(new Response("Federation response")); + }, + }; + + const contextDataFactory: ContextDataFactory = + () => "test-context"; + + const middleware = federation(mockFederation as never, contextDataFactory); + + const mockRequest = new Request("https://example.com/test"); + const mockContext: MockHonoContext = { + req: { raw: mockRequest }, + res: new Response("Hono response"), + }; + + const result = await middleware(mockContext, () => Promise.resolve()); + + strictEqual(capturedRequest, mockRequest); + strictEqual( + (capturedOptions as { contextData: string }).contextData, + "test-context", + ); + strictEqual(result?.status, 200); + }); + + test("handles async context data factory", async () => { + const ASYNC_CONTEXT = "async-context" as const; + const capturedContextData: { contextData?: typeof ASYNC_CONTEXT } = {}; + + const mockFederation: MockFederation = { + fetch: (_request, options: { contextData: typeof ASYNC_CONTEXT }) => { + capturedContextData.contextData = options.contextData; + return Promise.resolve(new Response("OK")); + }, + }; + + const contextDataFactory: ContextDataFactory< + typeof ASYNC_CONTEXT, + MockHonoContext + > = async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return ASYNC_CONTEXT; + }; + + const middleware = federation(mockFederation as never, contextDataFactory); + + const mockRequest = new Request("https://example.com/test"); + const mockContext: MockHonoContext = { + req: { raw: mockRequest }, + res: new Response("Hono response"), + }; + + await middleware(mockContext, () => Promise.resolve()); + + strictEqual( + capturedContextData.contextData, + ASYNC_CONTEXT, + ); + }); +}); diff --git a/packages/hono/src/mod.ts b/packages/hono/src/mod.ts new file mode 100644 index 000000000..dda53c804 --- /dev/null +++ b/packages/hono/src/mod.ts @@ -0,0 +1,105 @@ +/** + * Fedify with Hono + * ================ + * + * This package provides a [Hono] middleware to integrate with the Fedify. + * + * [Hono]: https://hono.dev/ + * + * @module + * @since 1.9.0 + */ +import type { + Federation, + FederationFetchOptions, +} from "@fedify/fedify/federation"; + +interface HonoRequest { + raw: Request; +} + +interface HonoContext { + req: HonoRequest; + res: Response; +} + +type HonoMiddleware = ( + ctx: THonoContext, + next: () => Promise, +) => Promise; + +/** + * A factory function to create a context data for the {@link Federation} + * object. + * + * @template TContextData A type of the context data for the {@link Federation} + * object. + * @template THonoContext A type of the Hono context. + * @param context A Hono context object. + * @returns A context data for the {@link Federation} object. + * @since 1.9.0 + */ +export type ContextDataFactory = ( + context: THonoContext, +) => TContextData | Promise; + +/** + * Create a Hono middleware to integrate with the {@link Federation} object. + * + * @template TContextData A type of the context data for the {@link Federation} + * object. + * @template THonoContext A type of the Hono context. + * @param federation A {@link Federation} object to integrate with Hono. + * @param contextDataFactory A function to create a context data for the + * {@link Federation} object. + * @returns A Hono middleware. + * @since 1.9.0 + */ +export function federation( + federation: Federation, + contextDataFactory: ContextDataFactory, +): HonoMiddleware { + return async (ctx, next) => { + let contextData = contextDataFactory(ctx); + if (contextData instanceof Promise) contextData = await contextData; + return await federation.fetch(ctx.req.raw, { + contextData, + ...integrateFetchOptions(ctx, next), + }); + }; +} + +function integrateFetchOptions( + ctx: THonoContext, + next: () => Promise, +): Omit, "contextData"> { + return { + // If the `federation` object finds a request not responsible for it + // (i.e., not a federation-related request), it will call the `next` + // provided by the Hono framework to continue the request handling + // by the Hono: + async onNotFound(_req: Request): Promise { + await next(); + return ctx.res; + }, + + // Similar to `onNotFound`, but slightly more tricky one. + // When the `federation` object finds a request not acceptable type-wise + // (i.e., a user-agent doesn't want JSON-LD), it will call the `next` + // provided by the Hono framework so that it renders HTML if there's some + // page. Otherwise, it will simply return a 406 Not Acceptable response. + // This kind of trick enables the Fedify and Hono to share the same routes + // and they do content negotiation depending on `Accept` header: + async onNotAcceptable(_req: Request): Promise { + await next(); + if (ctx.res.status !== 404) return ctx.res; + return new Response("Not acceptable", { + status: 406, + headers: { + "Content-Type": "text/plain", + Vary: "Accept", + }, + }); + }, + }; +} diff --git a/packages/hono/tsdown.config.ts b/packages/hono/tsdown.config.ts new file mode 100644 index 000000000..88d29afa7 --- /dev/null +++ b/packages/hono/tsdown.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/mod.ts"], + dts: true, + platform: "node", +}); diff --git a/packages/redis/README.md b/packages/redis/README.md index ac58e51f6..847764769 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -5,7 +5,6 @@ [![JSR][JSR badge]][JSR] [![npm][npm badge]][npm] -[![GitHub Actions][GitHub Actions badge]][GitHub Actions] This package provides [Fedify]'s [`KvStore`] and [`MessageQueue`] implementations for Redis: @@ -43,8 +42,6 @@ const federation = createFederation({ [JSR badge]: https://jsr.io/badges/@fedify/redis [npm]: https://www.npmjs.com/package/@fedify/redis [npm badge]: https://img.shields.io/npm/v/@fedify/redis?logo=npm -[GitHub Actions]: https://github.com/fedify-dev/redis/actions/workflows/main.yaml -[GitHub Actions badge]: https://github.com/fedify-dev/redis/actions/workflows/main.yaml/badge.svg [Fedify]: https://fedify.dev/ [`KvStore`]: https://jsr.io/@fedify/fedify/doc/federation/~/KvStore [`MessageQueue`]: https://jsr.io/@fedify/fedify/doc/federation/~/MessageQueue diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md new file mode 100644 index 000000000..448cd75d6 --- /dev/null +++ b/packages/sveltekit/README.md @@ -0,0 +1,66 @@ + + +@fedify/sveltekit: Integrate Fedify with SvelteKit +================================================== + +[![JSR][JSR badge]][JSR] +[![npm][npm badge]][npm] +[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social] + +*This package is available since Fedify 1.9.0.* + +This package provides a simple way to integrate [Fedify] with [SvelteKit]. + +The integration code looks like this: + +~~~~ typescript +import { createFederation } from "@fedify/fedify"; +import { fedifyHook } from "@fedify/sveltekit"; + +const federation = createFederation({ + // Omitted for brevity; see the related section for details. +}); + +// This is the entry point to the Fedify hook from the SvelteKit framework: +export const handle = fedifyHook(federation, (req) => "context data"); +~~~~ + +Put the above code in your *hooks.server.ts* file. + +How it works +------------ + +Fedify behaves as a hook handler that wraps around the SvelteKit request handler. +The hook intercepts the incoming HTTP requests and dispatches them to +the appropriate handler based on the request path and the `Accept` header +(i.e., content negotiation). This architecture allows Fedify and your SvelteKit +application to coexist in the same domain and port. + +For example, if you make a request to */.well-known/webfinger* Fedify will +handle the request by itself, but if you make a request to */users/alice* +(assuming your SvelteKit app has a handler for `/users/[handle]`) with `Accept: +text/html` header, Fedify will dispatch the request to the SvelteKit app's +appropriate handler for `/users/[handle]`. Or if you define an actor dispatcher +for `/users/{handle}` in Fedify, and the request is made with `Accept: +application/activity+json` header, Fedify will dispatch the request to the +appropriate actor dispatcher. + +Installation +------------ + +~~~~ sh +deno add jsr:@fedify/sveltekit # Deno +npm add @fedify/sveltekit # npm +pnpm add @fedify/sveltekit # pnpm +yarn add @fedify/sveltekit # Yarn +bun add @fedify/sveltekit # Bun +~~~~ + +[JSR]: https://jsr.io/@fedify/sveltekit +[JSR badge]: https://jsr.io/badges/@fedify/sveltekit +[npm]: https://www.npmjs.com/package/@fedify/sveltekit +[npm badge]: https://img.shields.io/npm/v/@fedify/sveltekit?logo=npm +[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg +[@fedify@hollo.social]: https://hollo.social/@fedify +[Fedify]: https://fedify.dev/ +[SvelteKit]: https://kit.svelte.dev/ diff --git a/packages/sveltekit/deno.json b/packages/sveltekit/deno.json new file mode 100644 index 000000000..d5efedee6 --- /dev/null +++ b/packages/sveltekit/deno.json @@ -0,0 +1,19 @@ +{ + "name": "@fedify/sveltekit", + "version": "1.9.0", + "license": "MIT", + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.13" + }, + "exports": { + ".": "./src/mod.ts" + }, + "exclude": [ + "dist", + "node_modules" + ], + "tasks": { + "check": "deno fmt --check && deno lint && deno check src/*.ts", + "test": "deno test --allow-net --allow-env" + } +} diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json new file mode 100644 index 000000000..a3ea1e6c0 --- /dev/null +++ b/packages/sveltekit/package.json @@ -0,0 +1,68 @@ +{ + "name": "@fedify/sveltekit", + "version": "1.9.0", + "description": "Integrate Fedify with SvelteKit", + "keywords": [ + "Fedify", + "ActivityPub", + "Fediverse", + "SvelteKit", + "Svelte" + ], + "author": { + "name": "Hong Minhee", + "email": "hong@minhee.org", + "url": "https://hongminhee.org/" + }, + "homepage": "https://fedify.dev/", + "repository": { + "type": "git", + "url": "git+https://github.com/fedify-dev/fedify.git", + "directory": "packages/sveltekit" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/fedify-dev/fedify/issues" + }, + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "type": "module", + "main": "./dist/mod.js", + "module": "./dist/mod.js", + "types": "./dist/mod.d.ts", + "exports": { + ".": { + "require": { + "types": "./dist/mod.d.ts", + "import": "./dist/mod.js", + "default": "./dist/mod.js" + }, + "import": { + "types": "./dist/mod.d.ts", + "import": "./dist/mod.js", + "default": "./dist/mod.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "package.json" + ], + "peerDependencies": { + "@fedify/fedify": "workspace:", + "@sveltejs/kit": "catalog:" + }, + "devDependencies": { + "tsdown": "catalog:", + "typescript": "catalog:" + }, + "scripts": { + "build": "tsdown", + "prepack": "tsdown", + "prepublish": "tsdown", + "test": "deno task codegen && tsdown && cd dist/ && node --test" + } +} diff --git a/packages/sveltekit/src/mod.test.ts b/packages/sveltekit/src/mod.test.ts new file mode 100644 index 000000000..042237490 --- /dev/null +++ b/packages/sveltekit/src/mod.test.ts @@ -0,0 +1,93 @@ +import { strictEqual } from "node:assert/strict"; +import { describe, test } from "node:test"; +import { fedifyHook } from "./mod.ts"; + +interface MockRequestEvent { + request: Request; +} + +interface MockHookParams { + event: MockRequestEvent; + resolve: (event: MockRequestEvent) => Promise; +} + +interface MockFederation { + fetch(request: Request, options: unknown): Promise; +} + +describe("fedifyHook", () => { + test("creates hook handler function", () => { + const mockFederation: MockFederation = { + fetch: () => Promise.resolve(new Response("OK")), + }; + + const createContextData = () => undefined; + + const hookHandler = fedifyHook(mockFederation as never, createContextData); + strictEqual(typeof hookHandler, "function"); + }); + + test("calls federation.fetch with correct parameters", async () => { + let capturedRequest: Request | undefined; + let capturedOptions: unknown; + + const mockFederation: MockFederation = { + fetch: (request, options) => { + capturedRequest = request; + capturedOptions = options; + return Promise.resolve(new Response("Federation response")); + }, + }; + + const createContextData = () => "test-context"; + + const hookHandler = fedifyHook(mockFederation as never, createContextData); + + const mockRequest = new Request("https://example.com/test"); + const mockEvent: MockRequestEvent = { request: mockRequest }; + const mockResolve = () => + Promise.resolve(new Response("SvelteKit response")); + + const result = await hookHandler({ + event: mockEvent, + resolve: mockResolve, + }); + + strictEqual(capturedRequest, mockRequest); + strictEqual( + (capturedOptions as { contextData: string }).contextData, + "test-context", + ); + strictEqual(result.status, 200); + }); + + test("handles async context data creation", async () => { + let capturedContextData: unknown; + + const mockFederation: MockFederation = { + fetch: (_request, options) => { + capturedContextData = (options as { contextData: string }).contextData; + return Promise.resolve(new Response("OK")); + }, + }; + + const createContextData = async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return "async-context"; + }; + + const hookHandler = fedifyHook(mockFederation as never, createContextData); + + const mockRequest = new Request("https://example.com/test"); + const mockEvent: MockRequestEvent = { request: mockRequest }; + const mockResolve = () => + Promise.resolve(new Response("SvelteKit response")); + + await hookHandler({ event: mockEvent, resolve: mockResolve }); + + strictEqual( + capturedContextData, + "async-context", + ); + }); +}); diff --git a/packages/sveltekit/src/mod.ts b/packages/sveltekit/src/mod.ts new file mode 100644 index 000000000..05c6ea123 --- /dev/null +++ b/packages/sveltekit/src/mod.ts @@ -0,0 +1,87 @@ +/** + * Fedify with SvelteKit + * ===================== + * + * This package provides a [SvelteKit] hook to integrate with the Fedify. + * + * [SvelteKit]: https://kit.svelte.dev/ + * + * @module + * @since 1.9.0 + */ + +import type { + Federation, + FederationFetchOptions, +} from "@fedify/fedify/federation"; + +type RequestEvent = { + request: Request; +}; + +type HookParams = { + event: RequestEvent; + resolve: (event: RequestEvent) => Promise; +}; + +/** + * Create a SvelteKit hook handler to integrate with the {@link Federation} + * object. + * + * @example hooks.server.ts + * ``` typescript + * import { federation } from "./federation"; // Import the `Federation` object + * + * export const handle = fedifyHook(federation, () => undefined); + * ``` + * + * @template TContextData A type of the context data for the {@link Federation} + * object. + * @param federation A {@link Federation} object to integrate with SvelteKit. + * @param createContextData A function to create a context data for the + * {@link Federation} object. + * @returns A SvelteKit hook handler. + * @since 1.9.0 + */ +export function fedifyHook( + federation: Federation, + createContextData: ( + event: RequestEvent, + ) => TContextData | Promise, +): (params: HookParams) => Promise { + return async ({ event, resolve }: HookParams) => { + return await federation.fetch(event.request, { + contextData: await createContextData(event), + ...integrateFetchOptions({ event, resolve }), + }); + }; +} + +function integrateFetchOptions( + { event, resolve }: HookParams, +): Omit, "contextData"> { + return { + async onNotFound(): Promise { + return await resolve(event); + }, + + // Similar to `onNotFound`, but slightly more tricky one. + // When the `federation` object finds a request not acceptable type-wise + // (i.e., a user-agent doesn't want JSON-LD), it will call the `resolve` + // provided by the SvelteKit framework so that it renders HTML if there's some + // page. Otherwise, it will simply return a 406 Not Acceptable response. + // This kind of trick enables the Fedify and SvelteKit to share the same routes + // and they do content negotiation depending on `Accept` header: + async onNotAcceptable(): Promise { + const res = await resolve(event); + if (res.status !== 404) return res; + return new Response("Not acceptable", { + status: 406, + headers: { + "Content-Type": "text/plain", + Vary: "Accept", + }, + }); + }, + }; +} diff --git a/packages/sveltekit/tsdown.config.ts b/packages/sveltekit/tsdown.config.ts new file mode 100644 index 000000000..88d29afa7 --- /dev/null +++ b/packages/sveltekit/tsdown.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/mod.ts"], + dts: true, + platform: "node", +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02e94f996..9018279b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ catalogs: '@std/path': specifier: jsr:^1.0.6 version: 1.1.0 + '@sveltejs/kit': + specifier: ^2.0.0 + version: 2.36.2 '@types/amqplib': specifier: ^0.10.7 version: 0.10.7 @@ -51,6 +54,9 @@ catalogs: h3: specifier: ^1.15.0 version: 1.15.3 + hono: + specifier: ^4.0.0 + version: 4.8.3 ioredis: specifier: ^5.6.1 version: 5.6.1 @@ -90,6 +96,9 @@ importers: '@fedify/amqp': specifier: 'workspace:' version: link:../packages/amqp + '@fedify/cfworkers': + specifier: 'workspace:' + version: link:../packages/cfworkers '@fedify/express': specifier: 'workspace:' version: link:../packages/express @@ -99,6 +108,9 @@ importers: '@fedify/h3': specifier: 'workspace:' version: link:../packages/h3 + '@fedify/hono': + specifier: 'workspace:' + version: link:../packages/hono '@fedify/nestjs': specifier: 'workspace:' version: link:../packages/nestjs @@ -114,6 +126,9 @@ importers: '@fedify/sqlite': specifier: 'workspace:' version: link:../packages/sqlite + '@fedify/sveltekit': + specifier: 'workspace:' + version: link:../packages/sveltekit '@fedify/testing': specifier: 'workspace:' version: link:../packages/testing @@ -452,6 +467,22 @@ importers: specifier: 'catalog:' version: 5.9.2 + packages/cfworkers: + dependencies: + '@cloudflare/workers-types': + specifier: 'catalog:' + version: 4.20250529.0 + '@fedify/fedify': + specifier: 'workspace:' + version: link:../fedify + devDependencies: + tsdown: + specifier: 'catalog:' + version: 0.12.9(typescript@5.9.2) + typescript: + specifier: 'catalog:' + version: 5.9.2 + packages/cli: {} packages/elysia: @@ -611,6 +642,22 @@ importers: specifier: 'catalog:' version: 5.9.2 + packages/hono: + dependencies: + '@fedify/fedify': + specifier: 'workspace:' + version: link:../fedify + hono: + specifier: 'catalog:' + version: 4.8.3 + devDependencies: + tsdown: + specifier: 'catalog:' + version: 0.12.9(typescript@5.9.2) + typescript: + specifier: 'catalog:' + version: 5.9.2 + packages/nestjs: dependencies: '@fedify/fedify': @@ -730,6 +777,22 @@ importers: specifier: 'catalog:' version: 5.9.2 + packages/sveltekit: + dependencies: + '@fedify/fedify': + specifier: 'workspace:' + version: link:../fedify + '@sveltejs/kit': + specifier: 'catalog:' + version: 2.36.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)))(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)) + devDependencies: + tsdown: + specifier: 'catalog:' + version: 0.12.9(typescript@5.9.2) + typescript: + specifier: 'catalog:' + version: 5.9.2 + packages/testing: dependencies: '@fedify/fedify': @@ -1822,6 +1885,9 @@ packages: '@jridgewell/gen-mapping@0.3.11': resolution: {integrity: sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -2446,6 +2512,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@prisma/instrumentation@5.22.0': resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==} @@ -2730,6 +2799,42 @@ packages: '@sinclair/typebox@0.34.38': resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@sveltejs/acorn-typescript@1.0.5': + resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/kit@2.36.2': + resolution: {integrity: sha512-WlBGY060nHe4UE5QrDAJAbls5hOsG6mljtrDGkM8jJCDQ4JEcAEH04XrTVmQ0Ex1CU8nzoZto0EE75aiLA3G8Q==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1': + resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.1.3': + resolution: {integrity: sha512-3pppgIeIZs6nrQLazzKcdnTJ2IWiui/UucEPXKyFG35TKaHQrfkWBnv6hyJcLxFuR90t+LaoecrqTs8rJKWfSQ==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2856,6 +2961,9 @@ packages: '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -3689,6 +3797,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -3745,6 +3857,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -3997,6 +4113,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -4034,6 +4154,9 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -4312,6 +4435,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4329,6 +4455,9 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} + esrap@2.1.0: + resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -4800,6 +4929,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4926,6 +5058,10 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -5046,6 +5182,9 @@ packages: resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} engines: {node: '>=14'} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -5316,6 +5455,14 @@ packages: module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -5911,6 +6058,10 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -5959,6 +6110,9 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6025,6 +6179,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -6179,6 +6337,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte@5.38.3: + resolution: {integrity: sha512-ldbPzKdjUy7IALMBn15jzBM/TNxdXMxKeQZ538zzdABUjLg7e7/OIwnlaMQ+OR6s91W7DbDmJYjxHThHH7r9xA==} + engines: {node: '>=18'} + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -6230,6 +6392,10 @@ packages: tokenx@1.1.0: resolution: {integrity: sha512-KCjtiC2niPwTSuz4ktM82Ki5bjqBwYpssiHDsGr5BpejN/B3ksacRvrsdoxljdMIh2nCX78alnDkeemBmYUmTA==} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -6486,6 +6652,14 @@ packages: terser: optional: true + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + vitepress-plugin-group-icons@1.6.1: resolution: {integrity: sha512-eoFlFAhAy/yTZDbaIgA/nMbjVYXkf8pz8rr75MN2VCw7yH60I3cw6bW5EuwddAeafZtBqbo8OsEGU7TIWFiAjg==} peerDependencies: @@ -6656,6 +6830,9 @@ packages: youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} @@ -7491,6 +7668,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.3 '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.11 + '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.3': {} @@ -8177,6 +8359,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.29': {} + '@prisma/instrumentation@5.22.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -8481,6 +8665,55 @@ snapshots: '@sinclair/typebox@0.34.38': optional: true + '@standard-schema/spec@1.0.0': {} + + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/kit@2.36.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)))(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.1.3(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.1.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.17 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.1 + sirv: 3.0.1 + svelte: 5.38.3 + vite: 5.4.19(@types/node@24.3.0)(lightningcss@1.30.1) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)))(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.1.3(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)) + debug: 4.4.1 + svelte: 5.38.3 + vite: 5.4.19(@types/node@24.3.0)(lightningcss@1.30.1) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.1.3(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)))(svelte@5.38.3)(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)) + debug: 4.4.1 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.17 + svelte: 5.38.3 + vite: 5.4.19(@types/node@24.3.0)(lightningcss@1.30.1) + vitefu: 1.1.1(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)) + transitivePeerDependencies: + - supports-color + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -8602,6 +8835,8 @@ snapshots: dependencies: '@types/node': 24.3.0 + '@types/cookie@0.6.0': {} + '@types/d3-array@3.2.1': {} '@types/d3-axis@3.0.6': @@ -9538,6 +9773,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clsx@2.1.1: {} + cluster-key-slot@1.1.2: {} color-convert@2.0.1: @@ -9580,6 +9817,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie@0.6.0: {} + cookie@0.7.1: {} cookie@0.7.2: {} @@ -9842,6 +10081,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -9872,6 +10113,8 @@ snapshots: detect-libc@2.0.4: {} + devalue@5.1.1: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -10500,6 +10743,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + espree@10.4.0: dependencies: acorn: 8.15.0 @@ -10518,6 +10763,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@2.1.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.3 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -11053,6 +11302,10 @@ snapshots: is-plain-obj@4.1.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -11176,6 +11429,8 @@ snapshots: kind-of@6.0.3: {} + kleur@4.1.5: {} + kolorist@1.8.0: {} ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@3.3.3): @@ -11272,6 +11527,8 @@ snapshots: pkg-types: 2.2.0 quansync: 0.2.10 + locate-character@3.0.0: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -11706,6 +11963,10 @@ snapshots: module-details-from-path@1.0.4: {} + mri@1.2.0: {} + + mrmime@2.0.1: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -12361,6 +12622,10 @@ snapshots: dependencies: tslib: 2.8.1 + sade@1.8.1: + dependencies: + mri: 1.2.0 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -12426,6 +12691,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12572,6 +12839,12 @@ snapshots: dependencies: is-arrayish: 0.3.2 + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slash@3.0.0: {} source-map-js@1.2.1: {} @@ -12723,6 +12996,23 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte@5.38.3: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.3 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.2 + esrap: 2.1.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.17 + zimmerframe: 1.1.2 + tabbable@6.2.0: {} tailwindcss@3.4.17: @@ -12795,6 +13085,8 @@ snapshots: tokenx@1.1.0: {} + totalist@3.0.1: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -13083,6 +13375,20 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.30.1 + vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.44.1 + optionalDependencies: + '@types/node': 24.3.0 + fsevents: 2.3.3 + lightningcss: 1.30.1 + + vitefu@1.1.1(vite@5.4.19(@types/node@24.3.0)(lightningcss@1.30.1)): + optionalDependencies: + vite: 5.4.19(@types/node@24.3.0)(lightningcss@1.30.1) + vitepress-plugin-group-icons@1.6.1(markdown-it@14.1.0)(vite@5.4.19(@types/node@22.16.0)(lightningcss@1.30.1)): dependencies: '@iconify-json/logos': 1.2.4 @@ -13325,6 +13631,8 @@ snapshots: mustache: 4.2.0 stacktracey: 2.1.8 + zimmerframe@1.1.2: {} + zod@3.22.3: {} zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fcbc3d085..e404911d9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,14 +2,17 @@ packages: - packages/fedify - packages/cli - packages/amqp +- packages/cfworkers - packages/elysia - packages/express +- packages/hono - packages/nestjs - packages/next - packages/h3 - packages/postgres - packages/redis - packages/sqlite +- packages/sveltekit - packages/testing - docs - examples/cloudflare-workers @@ -27,14 +30,17 @@ catalog: "@std/assert": "jsr:^1.0.13" "@std/async": "jsr:^1.0.13" "@std/path": "jsr:^1.0.6" + "@sveltejs/kit": ^2.0.0 "@types/amqplib": ^0.10.7 "@types/express": ^4.0.0 "@types/node": ^22.16.0 "@nestjs/common": ^11.0.1 amqplib: ^0.10.8 elysia: ^1.3.8 + es-toolkit: ^1.30.0 express: ^4.0.0 h3: ^1.15.0 + hono: ^4.0.0 ioredis: ^5.6.1 next: ^15.4.6 postgres: ^3.4.7