From 9ff8b5f0e8df8822714fcd21b2fb6d0dc6d58323 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Mon, 13 Apr 2026 02:10:40 +0000 Subject: [PATCH 1/5] Create examples/nuxt with Claude Opus 4.6 --- examples/nuxt/.gitignore | 5 + examples/nuxt/README.md | 45 ++ examples/nuxt/app.vue | 13 + examples/nuxt/nuxt.config.ts | 9 + examples/nuxt/package.json | 27 + examples/nuxt/pages/index.vue | 287 ++++++++++ .../nuxt/pages/users/[identifier]/index.vue | 71 +++ .../pages/users/[identifier]/posts/[id].vue | 61 +++ examples/nuxt/public/demo-profile.png | Bin 0 -> 51021 bytes examples/nuxt/public/fedify-logo.svg | 180 +++++++ examples/nuxt/public/style.css | 504 ++++++++++++++++++ examples/nuxt/public/theme.js | 7 + examples/nuxt/server/api/events.get.ts | 36 ++ examples/nuxt/server/api/follow.post.ts | 42 ++ examples/nuxt/server/api/home.get.ts | 51 ++ examples/nuxt/server/api/post.post.ts | 44 ++ .../server/api/posts/[identifier]/[id].get.ts | 33 ++ .../server/api/profile/[identifier].get.ts | 29 + examples/nuxt/server/api/search.get.ts | 37 ++ examples/nuxt/server/api/unfollow.post.ts | 46 ++ examples/nuxt/server/federation.ts | 163 ++++++ examples/nuxt/server/plugins/logging.ts | 25 + examples/nuxt/server/sse.ts | 21 + examples/nuxt/server/store.ts | 50 ++ examples/nuxt/tsconfig.json | 3 + examples/test-examples/mod.ts | 13 + pnpm-lock.yaml | 204 +++---- pnpm-workspace.yaml | 1 + 28 files changed, 1907 insertions(+), 100 deletions(-) create mode 100644 examples/nuxt/.gitignore create mode 100644 examples/nuxt/README.md create mode 100644 examples/nuxt/app.vue create mode 100644 examples/nuxt/nuxt.config.ts create mode 100644 examples/nuxt/package.json create mode 100644 examples/nuxt/pages/index.vue create mode 100644 examples/nuxt/pages/users/[identifier]/index.vue create mode 100644 examples/nuxt/pages/users/[identifier]/posts/[id].vue create mode 100644 examples/nuxt/public/demo-profile.png create mode 100644 examples/nuxt/public/fedify-logo.svg create mode 100644 examples/nuxt/public/style.css create mode 100644 examples/nuxt/public/theme.js create mode 100644 examples/nuxt/server/api/events.get.ts create mode 100644 examples/nuxt/server/api/follow.post.ts create mode 100644 examples/nuxt/server/api/home.get.ts create mode 100644 examples/nuxt/server/api/post.post.ts create mode 100644 examples/nuxt/server/api/posts/[identifier]/[id].get.ts create mode 100644 examples/nuxt/server/api/profile/[identifier].get.ts create mode 100644 examples/nuxt/server/api/search.get.ts create mode 100644 examples/nuxt/server/api/unfollow.post.ts create mode 100644 examples/nuxt/server/federation.ts create mode 100644 examples/nuxt/server/plugins/logging.ts create mode 100644 examples/nuxt/server/sse.ts create mode 100644 examples/nuxt/server/store.ts create mode 100644 examples/nuxt/tsconfig.json diff --git a/examples/nuxt/.gitignore b/examples/nuxt/.gitignore new file mode 100644 index 000000000..d86952676 --- /dev/null +++ b/examples/nuxt/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.output +.nuxt +.data diff --git a/examples/nuxt/README.md b/examples/nuxt/README.md new file mode 100644 index 000000000..ae1c90a06 --- /dev/null +++ b/examples/nuxt/README.md @@ -0,0 +1,45 @@ + + +Fedify-Nuxt integration example application +=========================================== + +A comprehensive example of building a federated server application using +[Fedify] with [Nuxt]. This example demonstrates how to create an +ActivityPub-compatible federated social media server that can interact with +other federated platforms like [Mastodon], [Misskey], and other ActivityPub +implementations using the Fedify and [Nuxt]. + +[Fedify]: https://fedify.dev +[Nuxt]: https://nuxt.com/ +[Mastodon]: https://mastodon.social/ +[Misskey]: https://misskey.io/ + + +Running the example +------------------- + +~~~~ sh +pnpm dev +~~~~ + + +Communicate with other federated servers +---------------------------------------- + +1. Tunnel your local server to the internet using `fedify tunnel` + + ~~~~ sh + fedify tunnel 3000 + ~~~~ + +2. Open the tunneled URL in your browser and check that the server is running + properly. + +3. Search your handle and follow from other federated servers such as + [Mastodon] or [Misskey]. + + > [!NOTE] + > [ActivityPub Academy] is a great resource to learn how to interact + > with other federated servers using ActivityPub protocol. + +[ActivityPub Academy]: https://www.activitypub.academy/ diff --git a/examples/nuxt/app.vue b/examples/nuxt/app.vue new file mode 100644 index 000000000..420f15557 --- /dev/null +++ b/examples/nuxt/app.vue @@ -0,0 +1,13 @@ + + + diff --git a/examples/nuxt/nuxt.config.ts b/examples/nuxt/nuxt.config.ts new file mode 100644 index 000000000..d3f3a3fa0 --- /dev/null +++ b/examples/nuxt/nuxt.config.ts @@ -0,0 +1,9 @@ +export default defineNuxtConfig({ + modules: ["@fedify/nuxt"], + fedify: { + federationModule: "~/server/federation", + }, + ssr: true, + devServer: { host: "0.0.0.0" }, + vite: { server: { allowedHosts: true } }, +}); diff --git a/examples/nuxt/package.json b/examples/nuxt/package.json new file mode 100644 index 000000000..d7610eff2 --- /dev/null +++ b/examples/nuxt/package.json @@ -0,0 +1,27 @@ +{ + "name": "nuxt-example", + "version": "0.0.1", + "private": true, + "type": "module", + "description": "Fedify app with Nuxt integration", + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "preview": "nuxt preview", + "start": "node .output/server/index.mjs" + }, + "dependencies": { + "@fedify/fedify": "workspace:^", + "@fedify/nuxt": "workspace:^", + "@fedify/vocab": "workspace:^", + "@logtape/logtape": "catalog:", + "nuxt": "catalog:", + "h3": "catalog:", + "vue": "^3.5.13", + "x-forwarded-fetch": "^0.2.0" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + } +} diff --git a/examples/nuxt/pages/index.vue b/examples/nuxt/pages/index.vue new file mode 100644 index 000000000..51779f2cc --- /dev/null +++ b/examples/nuxt/pages/index.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/examples/nuxt/pages/users/[identifier]/index.vue b/examples/nuxt/pages/users/[identifier]/index.vue new file mode 100644 index 000000000..afd73d7bc --- /dev/null +++ b/examples/nuxt/pages/users/[identifier]/index.vue @@ -0,0 +1,71 @@ + + + diff --git a/examples/nuxt/pages/users/[identifier]/posts/[id].vue b/examples/nuxt/pages/users/[identifier]/posts/[id].vue new file mode 100644 index 000000000..e71e80d6a --- /dev/null +++ b/examples/nuxt/pages/users/[identifier]/posts/[id].vue @@ -0,0 +1,61 @@ + + + diff --git a/examples/nuxt/public/demo-profile.png b/examples/nuxt/public/demo-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..2c9883bcc5654c5c8e9396e183f51999e83eeda2 GIT binary patch literal 51021 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 +FedifyFedify diff --git a/examples/nuxt/public/style.css b/examples/nuxt/public/style.css new file mode 100644 index 000000000..3d8b3e6e5 --- /dev/null +++ b/examples/nuxt/public/style.css @@ -0,0 +1,504 @@ +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: var(--background); + color: var(--foreground); + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, sans-serif; +} + +a { + color: #3b82f6; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +/* Profile Header */ +.profile-header { + display: flex; + gap: 2rem; + padding: 2rem; + margin-bottom: 2rem; + border-radius: 1rem; + color: white; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +.avatar-section { + flex-shrink: 0; +} + +.avatar { + width: 7.5rem; + height: 7.5rem; + border-radius: 50%; + object-fit: cover; + border: 4px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.user-info { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; +} + +.user-name { + font-size: 2.25rem; + font-weight: bold; + margin: 0 0 0.5rem 0; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.user-handle { + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 1rem; + opacity: 0.9; +} + +.user-bio { + font-size: 1.125rem; + line-height: 1.625; + opacity: 0.95; + margin: 0; +} + +/* Profile Container & Content */ +.profile-container { + max-width: 56rem; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; + align-content: center; + padding: 2rem; + min-height: 100vh; +} + +.profile-content { + display: grid; + gap: 2rem; +} + +.info-card { + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.05); + background: var(--background); + color: var(--foreground); + padding: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.info-card h3 { + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 1.5rem 0; +} + +.info-grid { + display: grid; + gap: 1rem; +} + +.info-item { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.info-item:last-child { + border-bottom: none; +} + +.info-label { + font-size: 0.875rem; + font-weight: 600; + color: color-mix(in srgb, var(--foreground) 60%, transparent); +} + +.fedify-anchor { + display: inline-flex; + align-items: center; + gap: 0.25rem; + height: 1.5rem; + padding: 0.125rem 0.25rem; + border-radius: 0.375rem; + background: #7dd3fc; + color: white; + font-weight: 500; + text-decoration: none; +} + +.fedify-anchor::before { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + background-image: url("/fedify-logo.svg"); + background-size: 16px 16px; + vertical-align: middle; + margin-bottom: 0.125rem; +} + +/* Post Form */ +.post-form { + max-width: 56rem; + margin: 2rem auto; + padding: 1.5rem; + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.1); + background: var(--background); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); +} + +.form-group { + margin-bottom: 1rem; +} + +.form-label { + display: block; + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--foreground); +} + +.form-textarea { + width: 100%; + resize: vertical; + border-radius: 0.5rem; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0.75rem; + font-size: 1rem; + background: var(--background); + color: var(--foreground); + transition: border-color 0.2s, box-shadow 0.2s; + font-family: inherit; +} + +.form-textarea:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); +} + +.post-button { + padding: 0.5rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-size: 1rem; + font-weight: 600; + color: white; + cursor: pointer; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.post-button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Posts Container & Grid */ +.posts-container { + max-width: 56rem; + margin: 0 auto; + padding: 0 2rem; +} + +.posts-title { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1.5rem; + color: var(--foreground); +} + +.posts-grid { + display: grid; + gap: 1.5rem; +} + +.post-card { + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.1); + background: var(--background); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + transition: transform 0.2s, box-shadow 0.2s; +} + +.post-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); +} + +.post-link { + display: block; + padding: 1.5rem; + text-decoration: none; + color: inherit; +} +.post-link:hover { + text-decoration: none; +} + +.post-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.post-avatar { + width: 3rem; + height: 3rem; + border-radius: 50%; + object-fit: cover; + border: 2px solid #e5e7eb; +} + +.post-user-info { + flex: 1; +} + +.post-user-name { + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 0.25rem 0; + color: var(--foreground); +} + +.post-user-handle { + font-size: 0.875rem; + opacity: 0.7; + color: var(--foreground); + margin: 0; +} + +.post-content { + font-size: 1rem; + line-height: 1.625; + color: var(--foreground); +} + +.post-content p { + margin: 0; +} + +/* Post Detail */ +.post-detail-container { + max-width: 56rem; + margin: 0 auto; + padding: 2rem; +} + +.post-detail-card { + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.1); + background: var(--background); + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.post-detail-author { + display: flex; + align-items: flex-start; + gap: 1rem; + padding-bottom: 1.5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + text-decoration: none; + color: inherit; +} +.post-detail-author:hover { + text-decoration: none; +} + +.author-avatar { + width: 4rem; + height: 4rem; + border-radius: 50%; + object-fit: cover; + border: 2px solid #e5e7eb; +} + +.author-info { + flex: 1; +} + +.author-name { + font-size: 1.5rem; + font-weight: bold; + margin: 0 0 0.25rem 0; + color: var(--foreground); +} + +.author-handle { + font-size: 1rem; + font-weight: 500; + opacity: 0.7; + margin: 0 0 0.5rem 0; + color: var(--foreground); +} + +.post-timestamp { + font-size: 0.875rem; + opacity: 0.6; + color: var(--foreground); +} + +.post-detail-content { + padding: 1.5rem 0; + font-size: 1.125rem; + line-height: 1.625; + color: var(--foreground); +} + +.post-detail-content p { + margin: 0; +} + +.back-link { + display: inline-block; + margin-bottom: 1.5rem; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-weight: 500; + color: var(--foreground); + background: color-mix(in srgb, var(--foreground) 10%, transparent); + text-decoration: none; + transition: background 0.2s; +} + +.back-link:hover { + background: color-mix(in srgb, var(--foreground) 15%, transparent); + text-decoration: none; +} + +/* Home Page */ +.home-container { + max-width: 780px; + margin: 2rem auto; + display: grid; + gap: 1rem; + padding: 1rem; +} + +.home-logo { + display: block; + width: 8rem; + height: 8rem; + margin: 0 auto; +} + +.home-banner { + display: flex; + flex-wrap: wrap; + justify-content: center; + font-family: monospace; + line-height: 1.2; + white-space: pre; +} + +.home-handle { + font-family: monospace; + background: #f3f4f6; + color: #000; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + user-select: all; +} + +.follower-item { + font-family: monospace; + background: #f3f4f6; + color: #000; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; +} + +.follower-list { + display: flex; + flex-direction: column; + gap: 0.25rem; + width: max-content; + list-style: none; +} + +/* Responsive */ +@media (max-width: 768px) { + .profile-container { + padding: 1rem; + } + + .profile-header { + flex-direction: column; + align-items: center; + gap: 1rem; + text-align: center; + } + + .user-name { + font-size: 1.875rem; + } + + .info-item { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .posts-container { + padding: 0 1rem; + } + + .post-form { + padding: 1rem; + } + + .post-detail-container { + padding: 1rem; + } + + .post-detail-card { + padding: 1.5rem; + } + + .author-avatar { + width: 3.5rem; + height: 3.5rem; + } + + .author-name { + font-size: 1.25rem; + } + + .post-detail-content { + font-size: 1rem; + } +} diff --git a/examples/nuxt/public/theme.js b/examples/nuxt/public/theme.js new file mode 100644 index 000000000..8d521d625 --- /dev/null +++ b/examples/nuxt/public/theme.js @@ -0,0 +1,7 @@ +"use strict"; +var mq = window.matchMedia("(prefers-color-scheme: dark)"); +document.body.classList.add(mq.matches ? "dark" : "light"); +mq.addEventListener("change", function (e) { + document.body.classList.remove("light", "dark"); + document.body.classList.add(e.matches ? "dark" : "light"); +}); diff --git a/examples/nuxt/server/api/events.get.ts b/examples/nuxt/server/api/events.get.ts new file mode 100644 index 000000000..e2f710e0d --- /dev/null +++ b/examples/nuxt/server/api/events.get.ts @@ -0,0 +1,36 @@ +import { setResponseHeader } from "h3"; +import { addClient, removeClient } from "../sse"; + +export default defineEventHandler(async (event) => { + setResponseHeader(event, "Content-Type", "text/event-stream"); + setResponseHeader(event, "Cache-Control", "no-cache"); + setResponseHeader(event, "Connection", "keep-alive"); + + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + const client = { + send(data: string) { + controller.enqueue(encoder.encode(`data: ${data}\n\n`)); + }, + close() { + controller.close(); + }, + }; + + addClient(client); + + event.node.req.on("close", () => { + removeClient(client); + }); + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + }); +}); diff --git a/examples/nuxt/server/api/follow.post.ts b/examples/nuxt/server/api/follow.post.ts new file mode 100644 index 000000000..f71deff09 --- /dev/null +++ b/examples/nuxt/server/api/follow.post.ts @@ -0,0 +1,42 @@ +import { Follow, type Object as APObject } from "@fedify/vocab"; +import { readBody, sendRedirect, toWebRequest } from "h3"; +import federation from "../federation"; +import { broadcastEvent } from "../sse"; +import { followingStore } from "../store"; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const targetUri = body?.uri; + if (typeof targetUri !== "string" || !targetUri.trim()) { + return sendRedirect(event, "/", 303); + } + + const request = toWebRequest(event); + const ctx = federation.createContext(request, undefined); + const identifier = "demo"; + + const target = await ctx.lookupObject(targetUri) as APObject | null; + if (target?.id == null) { + return sendRedirect(event, "/", 303); + } + + await ctx.sendActivity( + { identifier }, + target, + new Follow({ + id: new URL( + `#follows/${target.id.href}`, + ctx.getActorUri(identifier), + ), + actor: ctx.getActorUri(identifier), + object: target.id, + }), + ); + + const { Person } = await import("@fedify/vocab"); + if (target instanceof Person) { + followingStore.set(target.id.href, target); + } + broadcastEvent(); + return sendRedirect(event, "/", 303); +}); diff --git a/examples/nuxt/server/api/home.get.ts b/examples/nuxt/server/api/home.get.ts new file mode 100644 index 000000000..53da585b1 --- /dev/null +++ b/examples/nuxt/server/api/home.get.ts @@ -0,0 +1,51 @@ +import { toWebRequest } from "h3"; +import federation from "../federation"; +import { followingStore, postStore, relationStore } from "../store"; + +export default defineEventHandler(async (event) => { + const request = toWebRequest(event); + const ctx = federation.createContext(request, undefined); + const identifier = "demo"; + const actorUri = ctx.getActorUri(identifier); + const host = new URL(actorUri).host; + + const followers = await Promise.all( + Array.from(relationStore.entries()).map(async ([uri, person]) => ({ + uri, + name: person.name?.toString() ?? null, + handle: person.preferredUsername + ? `@${person.preferredUsername}@${person.id?.hostname ?? ""}` + : uri, + icon: (await person.getIcon(ctx))?.url?.href ?? null, + })), + ); + + const following = await Promise.all( + Array.from(followingStore.entries()).map(async ([uri, person]) => ({ + uri, + name: person.name?.toString() ?? null, + handle: person.preferredUsername + ? `@${person.preferredUsername}@${person.id?.hostname ?? ""}` + : uri, + icon: (await person.getIcon(ctx))?.url?.href ?? null, + })), + ); + + const allPosts = postStore.getAll(); + const posts = allPosts.map((p) => ({ + url: p.url?.href ?? p.id?.href ?? "", + id: p.id?.href ?? "", + content: p.content?.toString() ?? "", + published: p.published?.toString() ?? null, + })); + + return { + identifier, + host, + name: "Fedify Demo", + summary: "This is a Fedify Demo account.", + followers, + following, + posts, + }; +}); diff --git a/examples/nuxt/server/api/post.post.ts b/examples/nuxt/server/api/post.post.ts new file mode 100644 index 000000000..03245ac3d --- /dev/null +++ b/examples/nuxt/server/api/post.post.ts @@ -0,0 +1,44 @@ +import { Create, Note } from "@fedify/vocab"; +import { readBody, sendRedirect, toWebRequest } from "h3"; +import federation from "../federation"; +import { postStore } from "../store"; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const content = body?.content; + if (typeof content !== "string" || !content.trim()) { + return sendRedirect(event, "/", 303); + } + + const request = toWebRequest(event); + const ctx = federation.createContext(request, undefined); + const identifier = "demo"; + const id = crypto.randomUUID(); + const attribution = ctx.getActorUri(identifier); + const url = new URL(`/users/${identifier}/posts/${id}`, attribution); + const post = new Note({ + id: url, + attribution, + content: content.trim(), + url, + }); + try { + postStore.append([post]); + const note = await ctx.getObject(Note, { identifier, id }); + await ctx.sendActivity( + { identifier }, + "followers", + new Create({ + id: new URL("#activity", attribution), + object: note, + actors: note?.attributionIds, + tos: note?.toIds, + ccs: note?.ccIds, + }), + ); + } catch { + postStore.delete(url); + } + + return sendRedirect(event, "/", 303); +}); diff --git a/examples/nuxt/server/api/posts/[identifier]/[id].get.ts b/examples/nuxt/server/api/posts/[identifier]/[id].get.ts new file mode 100644 index 000000000..67812a7a8 --- /dev/null +++ b/examples/nuxt/server/api/posts/[identifier]/[id].get.ts @@ -0,0 +1,33 @@ +import { Note } from "@fedify/vocab"; +import { toWebRequest } from "h3"; +import federation from "../../../federation"; + +export default defineEventHandler(async (event) => { + const identifier = event.context.params?.identifier as string; + const id = event.context.params?.id as string; + if (identifier !== "demo" || !id) { + return null; + } + + const request = toWebRequest(event); + const ctx = federation.createContext(request, undefined); + const actor = await ctx.getActor(identifier); + const noteObj = await ctx.getObject(Note, { identifier, id }); + if (!actor || !noteObj) return null; + + const icon = await actor.getIcon(ctx); + const actorUri = ctx.getActorUri(identifier); + const host = new URL(actorUri).host; + + return { + identifier, + host, + author: { + name: actor.name?.toString() ?? "Fedify Demo", + icon: icon?.url?.href ?? null, + }, + content: noteObj.content?.toString() ?? "", + published: noteObj.published?.toString() ?? null, + url: noteObj.url?.href ?? noteObj.id?.href ?? "", + }; +}); diff --git a/examples/nuxt/server/api/profile/[identifier].get.ts b/examples/nuxt/server/api/profile/[identifier].get.ts new file mode 100644 index 000000000..2804da243 --- /dev/null +++ b/examples/nuxt/server/api/profile/[identifier].get.ts @@ -0,0 +1,29 @@ +import { toWebRequest } from "h3"; +import federation from "../../federation"; +import { followingStore, relationStore } from "../../store"; + +export default defineEventHandler(async (event) => { + const identifier = event.context.params?.identifier as string; + if (identifier !== "demo") { + return null; + } + + const request = toWebRequest(event); + const ctx = federation.createContext(request, undefined); + const actor = await ctx.getActor(identifier); + if (!actor) return null; + + const icon = await actor.getIcon(ctx); + const actorUri = ctx.getActorUri(identifier); + const host = new URL(actorUri).host; + + return { + identifier, + host, + name: actor.name?.toString() ?? "Fedify Demo", + summary: actor.summary?.toString() ?? null, + icon: icon?.url?.href ?? null, + followingCount: followingStore.size, + followersCount: relationStore.size, + }; +}); diff --git a/examples/nuxt/server/api/search.get.ts b/examples/nuxt/server/api/search.get.ts new file mode 100644 index 000000000..aee9c7d39 --- /dev/null +++ b/examples/nuxt/server/api/search.get.ts @@ -0,0 +1,37 @@ +import { Person } from "@fedify/vocab"; +import { getQuery, toWebRequest } from "h3"; +import federation from "../federation"; +import { followingStore } from "../store"; + +export default defineEventHandler(async (event) => { + const query = getQuery(event); + const q = query.q as string | undefined; + if (!q || !q.trim()) { + return { result: null }; + } + + const request = toWebRequest(event); + const ctx = federation.createContext(request, undefined); + + try { + const target = await ctx.lookupObject(q.trim()); + if (target instanceof Person && target.id) { + const iconUrl = await target.getIcon(ctx); + return { + result: { + uri: target.id.href, + name: target.name?.toString() ?? null, + handle: target.preferredUsername + ? `@${target.preferredUsername}@${target.id.hostname}` + : target.id.href, + icon: iconUrl?.url?.href ?? null, + isFollowing: followingStore.has(target.id.href), + }, + }; + } + } catch { + // lookup failed + } + + return { result: null }; +}); diff --git a/examples/nuxt/server/api/unfollow.post.ts b/examples/nuxt/server/api/unfollow.post.ts new file mode 100644 index 000000000..516005eb9 --- /dev/null +++ b/examples/nuxt/server/api/unfollow.post.ts @@ -0,0 +1,46 @@ +import { Follow, type Object as APObject, Undo } from "@fedify/vocab"; +import { readBody, sendRedirect, toWebRequest } from "h3"; +import federation from "../federation"; +import { broadcastEvent } from "../sse"; +import { followingStore } from "../store"; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const targetUri = body?.uri; + if (typeof targetUri !== "string" || !targetUri.trim()) { + return sendRedirect(event, "/", 303); + } + + const request = toWebRequest(event); + const ctx = federation.createContext(request, undefined); + const identifier = "demo"; + + const target = await ctx.lookupObject(targetUri) as APObject | null; + if (target?.id == null) { + return sendRedirect(event, "/", 303); + } + + await ctx.sendActivity( + { identifier }, + target, + new Undo({ + id: new URL( + `#undo-follows/${target.id.href}`, + ctx.getActorUri(identifier), + ), + actor: ctx.getActorUri(identifier), + object: new Follow({ + id: new URL( + `#follows/${target.id.href}`, + ctx.getActorUri(identifier), + ), + actor: ctx.getActorUri(identifier), + object: target.id, + }), + }), + ); + + followingStore.delete(target.id.href); + broadcastEvent(); + return sendRedirect(event, "/", 303); +}); diff --git a/examples/nuxt/server/federation.ts b/examples/nuxt/server/federation.ts new file mode 100644 index 000000000..de471c749 --- /dev/null +++ b/examples/nuxt/server/federation.ts @@ -0,0 +1,163 @@ +import { + createFederation, + generateCryptoKeyPair, + InProcessMessageQueue, + MemoryKvStore, +} from "@fedify/fedify"; +import { + Accept, + Endpoints, + Follow, + Image, + Note, + Person, + PUBLIC_COLLECTION, + type Recipient, + Undo, +} from "@fedify/vocab"; +import { broadcastEvent } from "./sse"; +import { keyPairsStore, postStore, relationStore } from "./store"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); + +const IDENTIFIER = "demo"; + +federation + .setActorDispatcher( + "/users/{identifier}", + async (context, identifier) => { + if (identifier != IDENTIFIER) { + return null; + } + const keyPairs = await context.getActorKeyPairs(identifier); + return new Person({ + id: context.getActorUri(identifier), + name: "Fedify Demo", + summary: "This is a Fedify Demo account.", + preferredUsername: identifier, + icon: new Image({ url: new URL("/demo-profile.png", context.url) }), + url: new URL("/", context.url), + inbox: context.getInboxUri(identifier), + followers: context.getFollowersUri(identifier), + endpoints: new Endpoints({ sharedInbox: context.getInboxUri() }), + publicKey: keyPairs[0].cryptographicKey, + assertionMethods: keyPairs.map((keyPair) => keyPair.multikey), + }); + }, + ) + .setKeyPairsDispatcher(async (_, identifier) => { + if (identifier != IDENTIFIER) { + return []; + } + const keyPairs = keyPairsStore.get(identifier); + if (keyPairs) { + return keyPairs; + } + const { privateKey, publicKey } = await generateCryptoKeyPair(); + keyPairsStore.set(identifier, [{ privateKey, publicKey }]); + return [{ privateKey, publicKey }]; + }); + +federation + .setInboxListeners("/users/{identifier}/inbox", "/inbox") + .on(Follow, async (context, follow) => { + if ( + follow.id == null || + follow.actorId == null || + follow.objectId == null + ) { + return; + } + const result = context.parseUri(follow.objectId); + if (result?.type !== "actor" || result.identifier !== IDENTIFIER) { + return; + } + const follower = await follow.getActor(context) as Person; + if (!follower?.id) { + throw new Error("follower is null"); + } + await context.sendActivity( + { identifier: result.identifier }, + follower, + new Accept({ + id: new URL( + `#accepts/${follower.id.href}`, + context.getActorUri(IDENTIFIER), + ), + actor: follow.objectId, + object: follow, + }), + ); + relationStore.set(follower.id.href, follower); + broadcastEvent(); + }) + .on(Undo, async (context, undo) => { + const activity = await undo.getObject(context); + if (activity instanceof Follow) { + if (activity.id == null) { + return; + } + if (undo.actorId == null) { + return; + } + relationStore.delete(undo.actorId.href); + broadcastEvent(); + } else { + console.debug(undo); + } + }); + +federation.setObjectDispatcher( + Note, + "/users/{identifier}/posts/{id}", + (ctx, values) => { + const id = ctx.getObjectUri(Note, values); + const post = postStore.get(id); + if (post == null) return null; + return new Note({ + id, + attribution: ctx.getActorUri(values.identifier), + to: PUBLIC_COLLECTION, + cc: ctx.getFollowersUri(values.identifier), + content: post.content, + mediaType: "text/html", + published: post.published, + url: id, + }); + }, +); + +federation + .setFollowersDispatcher( + "/users/{identifier}/followers", + () => { + const followers = Array.from(relationStore.values()); + const items: Recipient[] = followers.map((f) => ({ + id: f.id, + inboxId: f.inboxId, + endpoints: f.endpoints, + })); + return { items }; + }, + ); + +federation.setNodeInfoDispatcher("/nodeinfo/2.1", (ctx) => { + return { + software: { + name: "fedify-nuxt", + version: "0.0.1", + homepage: new URL(ctx.canonicalOrigin), + }, + protocols: ["activitypub"], + usage: { + users: { total: 1, activeHalfyear: 1, activeMonth: 1 }, + localPosts: postStore.getAll().length, + localComments: 0, + }, + }; +}); + +export default federation; diff --git a/examples/nuxt/server/plugins/logging.ts b/examples/nuxt/server/plugins/logging.ts new file mode 100644 index 000000000..97a87a814 --- /dev/null +++ b/examples/nuxt/server/plugins/logging.ts @@ -0,0 +1,25 @@ +import { configure, getConsoleSink } from "@logtape/logtape"; +import { AsyncLocalStorage } from "node:async_hooks"; + +export default defineNitroPlugin(async () => { + await configure({ + contextLocalStorage: new AsyncLocalStorage(), + sinks: { + console: getConsoleSink(), + }, + filters: {}, + loggers: [ + { + category: ["default", "example"], + lowestLevel: "debug", + sinks: ["console"], + }, + { category: "fedify", lowestLevel: "info", sinks: ["console"] }, + { + category: ["logtape", "meta"], + lowestLevel: "warning", + sinks: ["console"], + }, + ], + }); +}); diff --git a/examples/nuxt/server/sse.ts b/examples/nuxt/server/sse.ts new file mode 100644 index 000000000..c1af7c9b4 --- /dev/null +++ b/examples/nuxt/server/sse.ts @@ -0,0 +1,21 @@ +type EventClient = { + send: (data: string) => void; + close: () => void; +}; + +const clients = new Set(); + +export function addClient(client: EventClient): void { + clients.add(client); +} + +export function removeClient(client: EventClient): void { + clients.delete(client); +} + +export function broadcastEvent(): void { + const data = JSON.stringify({ type: "update" }); + for (const client of clients) { + client.send(data); + } +} diff --git a/examples/nuxt/server/store.ts b/examples/nuxt/server/store.ts new file mode 100644 index 000000000..8c987f07a --- /dev/null +++ b/examples/nuxt/server/store.ts @@ -0,0 +1,50 @@ +import { Note, Person } from "@fedify/vocab"; + +declare global { + var keyPairsStore: Map>; + var relationStore: Map; + var postStore: PostStore; + var followingStore: Map; +} + +class PostStore { + #map: Map = new Map(); + #timeline: URL[] = []; + constructor() {} + append(posts: Note[]) { + posts.filter((p) => p.id && !this.#map.has(p.id.toString())) + .forEach((p) => { + this.#map.set(p.id!.toString(), p); + this.#timeline.push(p.id!); + }); + } + get(id: URL) { + return this.#map.get(id.toString()); + } + getAll() { + return this.#timeline.toReversed() + .map((id) => id.toString()) + .map((id) => this.#map.get(id)!) + .filter((p) => p); + } + delete(id: URL) { + const existed = this.#map.delete(id.toString()); + if (existed) { + this.#timeline = this.#timeline.filter((i) => i.href !== id.href); + } + } +} + +const keyPairsStore = globalThis.keyPairsStore ?? new Map(); +const relationStore = globalThis.relationStore ?? new Map(); +const postStore = globalThis.postStore ?? new PostStore(); +const followingStore = globalThis.followingStore ?? new Map(); + +// this is just a hack for the demo +// never do this in production, use safe and secure storage +globalThis.keyPairsStore = keyPairsStore; +globalThis.relationStore = relationStore; +globalThis.postStore = postStore; +globalThis.followingStore = followingStore; + +export { followingStore, keyPairsStore, postStore, relationStore }; diff --git a/examples/nuxt/tsconfig.json b/examples/nuxt/tsconfig.json new file mode 100644 index 000000000..4b34df157 --- /dev/null +++ b/examples/nuxt/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/examples/test-examples/mod.ts b/examples/test-examples/mod.ts index d83dac360..33f0dbc56 100644 --- a/examples/test-examples/mod.ts +++ b/examples/test-examples/mod.ts @@ -269,6 +269,19 @@ const SERVER_EXAMPLES: ServerExample[] = [ actor: "demo", readyUrl: "http://localhost:4321/", }, + { + // Nuxt sample using @fedify/nuxt; actor path is /users/{identifier}. + // Built with nuxt build; served with node .output/server/index.mjs + // on port 3000. + name: "nuxt", + dir: "nuxt", + buildCmd: ["pnpm", "build"], + startCmd: ["pnpm", "start"], + port: 3000, + actor: "demo", + readyUrl: "http://localhost:3000/", + readyTimeout: 30_000, + }, ]; const SCRIPT_EXAMPLES: ScriptExample[] = [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1981f5154..d9bead79d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -652,6 +652,40 @@ importers: specifier: 'catalog:' version: 5.9.3 + examples/nuxt: + dependencies: + '@fedify/fedify': + specifier: workspace:^ + version: link:../../packages/fedify + '@fedify/nuxt': + specifier: workspace:^ + version: link:../../packages/nuxt + '@fedify/vocab': + specifier: workspace:^ + version: link:../../packages/vocab + '@logtape/logtape': + specifier: 'catalog:' + version: 2.0.5 + h3: + specifier: 'catalog:' + version: 1.15.3 + nuxt: + specifier: 'catalog:' + version: 4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@emnapi/core@1.4.3)(@emnapi/runtime@1.4.5)(@parcel/watcher@2.5.6)(@types/node@22.19.1)(@vue/compiler-sfc@3.5.32)(cac@6.7.14)(db0@0.3.4(mysql2@3.18.2(@types/node@22.19.1)))(ioredis@5.10.0)(lightningcss@1.30.1)(magicast@0.5.2)(mysql2@3.18.2(@types/node@22.19.1))(optionator@0.9.4)(rolldown@1.0.0-rc.12(@emnapi/core@1.4.3)(@emnapi/runtime@1.4.5))(rollup-plugin-visualizer@6.0.11(rolldown@1.0.0-rc.12(@emnapi/core@1.4.3)(@emnapi/runtime@1.4.5))(rollup@4.59.0))(rollup@4.59.0)(terser@5.46.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.3))(yaml@2.8.3) + vue: + specifier: ^3.5.13 + version: 3.5.32(typescript@5.9.3) + x-forwarded-fetch: + specifier: ^0.2.0 + version: 0.2.0 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 22.19.1 + typescript: + specifier: 'catalog:' + version: 5.9.3 + examples/solidstart: dependencies: '@fedify/fedify': @@ -6834,9 +6868,6 @@ packages: birpc@0.2.14: resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} - birpc@2.4.0: - resolution: {integrity: sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg==} - birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} @@ -7168,9 +7199,6 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} @@ -7189,9 +7217,6 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@1.2.2: - resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} - cookie-es@1.2.3: resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} @@ -8422,7 +8447,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} @@ -8480,9 +8505,6 @@ packages: h3@1.15.3: resolution: {integrity: sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ==} - h3@1.15.5: - resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} - hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -12772,7 +12794,7 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/trace-mapping': 0.3.31 '@antfu/install-pkg@1.1.0': dependencies: @@ -12861,7 +12883,7 @@ snapshots: '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 @@ -12876,10 +12898,10 @@ snapshots: '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@8.0.0-rc.3': @@ -12887,7 +12909,7 @@ snapshots: '@babel/parser': 8.0.0-rc.3 '@babel/types': 8.0.0-rc.3 '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/trace-mapping': 0.3.31 '@types/jsesc': 2.5.1 jsesc: 3.1.0 @@ -13018,7 +13040,7 @@ snapshots: '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@babel/traverse@7.29.0': @@ -13026,7 +13048,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 debug: 4.4.3 @@ -14064,7 +14086,7 @@ snapshots: '@isaacs/fs-minipass@4.0.1': dependencies: - minipass: 7.1.2 + minipass: 7.1.3 '@jimp/bmp@0.22.12(@jimp/custom@0.22.12)': dependencies: @@ -14292,19 +14314,19 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/source-map@0.3.11': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.28 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.3': {} @@ -14541,7 +14563,7 @@ snapshots: confbox: 0.2.4 consola: 3.4.2 debug: 4.4.3 - defu: 6.1.4 + defu: 6.1.7 exsolve: 1.0.8 fuse.js: 7.3.0 fzf: 0.5.2 @@ -14665,7 +14687,7 @@ snapshots: '@unhead/vue': 2.1.13(vue@3.5.32(typescript@5.9.3)) '@vue/shared': 3.5.32 consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 devalue: 5.6.3 errx: 0.1.0 @@ -14729,7 +14751,7 @@ snapshots: '@nuxt/schema@4.4.2': dependencies: '@vue/shared': 3.5.32 - defu: 6.1.4 + defu: 6.1.7 pathe: 2.0.3 pkg-types: 2.3.0 std-env: 4.0.0 @@ -14752,7 +14774,7 @@ snapshots: autoprefixer: 10.4.27(postcss@8.5.9) consola: 3.4.2 cssnano: 7.1.4(postcss@8.5.9) - defu: 6.1.4 + defu: 6.1.7 escape-string-regexp: 5.0.0 exsolve: 1.0.8 get-port-please: 3.2.0 @@ -16433,7 +16455,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 ansis: 4.2.0 babel-dead-code-elimination: 1.0.12 @@ -16491,7 +16513,7 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 @@ -16503,7 +16525,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': @@ -17380,9 +17402,9 @@ snapshots: citty: 0.1.6 clipboardy: 4.0.0 consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.7 get-port-please: 3.2.0 - h3: 1.15.5 + h3: 1.15.11 http-shutdown: 1.2.2 jiti: 1.21.7 mlly: 1.8.1 @@ -17395,7 +17417,7 @@ snapshots: '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@types/node@24.3.0)(db0@0.3.4(mysql2@3.18.2(@types/node@24.3.0)))(ioredis@5.10.0)(jiti@2.6.1)(lightningcss@1.30.1)(mysql2@3.18.2(@types/node@24.3.0))(rolldown@1.0.0-rc.12(@emnapi/core@1.4.3)(@emnapi/runtime@1.4.5))(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.3))': dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) acorn-loose: 8.5.2 @@ -17522,7 +17544,7 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@vue/compiler-sfc': 3.5.32 transitivePeerDependencies: - supports-color @@ -17609,7 +17631,7 @@ snapshots: '@vue/devtools-kit@7.7.7': dependencies: '@vue/devtools-shared': 7.7.7 - birpc: 2.4.0 + birpc: 2.9.0 hookable: 5.5.3 mitt: 3.0.1 perfect-debounce: 1.0.0 @@ -17632,9 +17654,9 @@ snapshots: '@vue/language-core@2.1.10(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.15 - '@vue/compiler-dom': 3.5.17 + '@vue/compiler-dom': 3.5.32 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.17 + '@vue/shared': 3.5.32 alien-signals: 0.2.2 minimatch: 9.0.5 muggle-string: 0.4.1 @@ -17695,7 +17717,7 @@ snapshots: '@types/web-bluetooth': 0.0.21 '@vueuse/metadata': 12.8.2 '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.17(typescript@5.9.3) + vue: 3.5.32(typescript@5.9.3) transitivePeerDependencies: - typescript @@ -17703,7 +17725,7 @@ snapshots: dependencies: '@vueuse/core': 12.8.2(typescript@5.9.3) '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.17(typescript@5.9.3) + vue: 3.5.32(typescript@5.9.3) optionalDependencies: focus-trap: 7.6.5 fuse.js: 7.3.0 @@ -17714,7 +17736,7 @@ snapshots: '@vueuse/shared@12.8.2(typescript@5.9.3)': dependencies: - vue: 3.5.17(typescript@5.9.3) + vue: 3.5.32(typescript@5.9.3) transitivePeerDependencies: - typescript @@ -17950,7 +17972,7 @@ snapshots: ast-kit@2.2.0: dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 pathe: 2.0.3 ast-kit@3.0.0-beta.1: @@ -17967,7 +17989,7 @@ snapshots: ast-walker-scope@0.8.3: dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 ast-kit: 2.2.0 astring@1.9.0: {} @@ -18113,7 +18135,7 @@ snapshots: babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 transitivePeerDependencies: @@ -18188,8 +18210,6 @@ snapshots: birpc@0.2.14: {} - birpc@2.4.0: {} - birpc@2.9.0: {} birpc@4.0.0: {} @@ -18303,8 +18323,8 @@ snapshots: c12@3.3.3(magicast@0.5.2): dependencies: chokidar: 5.0.0 - confbox: 0.2.2 - defu: 6.1.4 + confbox: 0.2.4 + defu: 6.1.7 dotenv: 17.3.1 exsolve: 1.0.8 giget: 2.0.0 @@ -18532,8 +18552,6 @@ snapshots: confbox@0.1.8: {} - confbox@0.2.2: {} - confbox@0.2.4: {} consola@3.4.2: {} @@ -18546,8 +18564,6 @@ snapshots: convert-source-map@2.0.0: {} - cookie-es@1.2.2: {} - cookie-es@1.2.3: {} cookie-es@2.0.0: {} @@ -20209,7 +20225,7 @@ snapshots: dependencies: citty: 0.1.6 consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.7 node-fetch-native: 1.6.7 nypm: 0.6.5 pathe: 2.0.3 @@ -20233,7 +20249,7 @@ snapshots: foreground-child: 3.3.1 jackspeak: 3.4.3 minimatch: 9.0.5 - minipass: 7.1.2 + minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -20320,21 +20336,9 @@ snapshots: h3@1.15.3: dependencies: - cookie-es: 1.2.2 - crossws: 0.3.5 - defu: 6.1.4 - destr: 2.0.5 - iron-webcrypto: 1.2.1 - node-mock-http: 1.0.4 - radix3: 1.1.2 - ufo: 1.6.3 - uncrypto: 0.1.3 - - h3@1.15.5: - dependencies: - cookie-es: 1.2.2 + cookie-es: 1.2.3 crossws: 0.3.5 - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 iron-webcrypto: 1.2.1 node-mock-http: 1.0.4 @@ -21150,9 +21154,9 @@ snapshots: clipboardy: 4.0.0 consola: 3.4.2 crossws: 0.3.5 - defu: 6.1.4 + defu: 6.1.7 get-port-please: 3.2.0 - h3: 1.15.5 + h3: 1.15.11 http-shutdown: 1.2.2 jiti: 2.6.1 mlly: 1.8.1 @@ -21252,13 +21256,13 @@ snapshots: magicast@0.2.11: dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 recast: 0.23.11 magicast@0.5.2: dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 source-map-js: 1.2.1 @@ -21769,7 +21773,7 @@ snapshots: minizlib@3.0.2: dependencies: - minipass: 7.1.2 + minipass: 7.1.3 mitt@3.0.1: {} @@ -21941,13 +21945,13 @@ snapshots: chokidar: 5.0.0 citty: 0.1.6 compatx: 0.2.0 - confbox: 0.2.2 + confbox: 0.2.4 consola: 3.4.2 cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 db0: 0.3.4(mysql2@3.18.2(@types/node@22.19.1)) - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 dot-prop: 10.1.0 esbuild: 0.27.3 @@ -21956,7 +21960,7 @@ snapshots: exsolve: 1.0.8 globby: 16.1.1 gzip-size: 7.0.0 - h3: 1.15.5 + h3: 1.15.11 hookable: 5.5.3 httpxy: 0.1.7 ioredis: 5.10.0 @@ -22044,13 +22048,13 @@ snapshots: chokidar: 5.0.0 citty: 0.1.6 compatx: 0.2.0 - confbox: 0.2.2 + confbox: 0.2.4 consola: 3.4.2 cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 db0: 0.3.4(mysql2@3.18.2(@types/node@24.3.0)) - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 dot-prop: 10.1.0 esbuild: 0.27.3 @@ -22059,7 +22063,7 @@ snapshots: exsolve: 1.0.8 globby: 16.1.1 gzip-size: 7.0.0 - h3: 1.15.5 + h3: 1.15.11 hookable: 5.5.3 httpxy: 0.1.7 ioredis: 5.10.0 @@ -22190,7 +22194,7 @@ snapshots: compatx: 0.2.0 consola: 3.4.2 cookie-es: 2.0.0 - defu: 6.1.4 + defu: 6.1.7 devalue: 5.6.3 errx: 0.1.0 escape-string-regexp: 5.0.0 @@ -22617,7 +22621,7 @@ snapshots: path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 - minipass: 7.1.2 + minipass: 7.1.3 path-scurry@2.0.2: dependencies: @@ -22708,7 +22712,7 @@ snapshots: pkg-types@2.3.0: dependencies: - confbox: 0.2.2 + confbox: 0.2.4 exsolve: 1.0.8 pathe: 2.0.3 @@ -22739,7 +22743,7 @@ snapshots: postcss-calc@10.1.1(postcss@8.5.9): dependencies: postcss: 8.5.9 - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 postcss-colormin@7.0.7(postcss@8.5.9): @@ -22795,7 +22799,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.5.6): dependencies: lilconfig: 3.1.3 - yaml: 2.8.1 + yaml: 2.8.3 optionalDependencies: postcss: 8.5.6 @@ -22909,9 +22913,9 @@ snapshots: dependencies: postcss: 8.5.6 - postcss-scss@4.0.9(postcss@8.5.6): + postcss-scss@4.0.9(postcss@8.5.9): dependencies: - postcss: 8.5.6 + postcss: 8.5.9 postcss-selector-parser@6.1.2: dependencies: @@ -23093,7 +23097,7 @@ snapshots: rc9@2.1.2: dependencies: - defu: 6.1.4 + defu: 6.1.7 destr: 2.0.5 rc9@3.0.1: @@ -23627,7 +23631,7 @@ snapshots: serve-placeholder@2.0.2: dependencies: - defu: 6.1.4 + defu: 6.1.7 serve-static@1.16.2: dependencies: @@ -24115,8 +24119,8 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - postcss: 8.5.6 - postcss-scss: 4.0.9(postcss@8.5.6) + postcss: 8.5.9 + postcss-scss: 4.0.9(postcss@8.5.9) postcss-selector-parser: 7.1.0 optionalDependencies: svelte: 5.38.3 @@ -24545,14 +24549,14 @@ snapshots: unenv@1.10.0: dependencies: consola: 3.4.2 - defu: 6.1.4 + defu: 6.1.7 mime: 3.0.0 node-fetch-native: 1.6.7 pathe: 1.1.2 unenv@2.0.0-rc.17: dependencies: - defu: 6.1.4 + defu: 6.1.7 exsolve: 1.0.8 ohash: 2.0.11 pathe: 2.0.3 @@ -24560,7 +24564,7 @@ snapshots: unenv@2.0.0-rc.21: dependencies: - defu: 6.1.4 + defu: 6.1.7 exsolve: 1.0.8 ohash: 2.0.11 pathe: 2.0.3 @@ -24737,7 +24741,7 @@ snapshots: anymatch: 3.1.3 chokidar: 5.0.0 destr: 2.0.5 - h3: 1.15.5 + h3: 1.15.11 lru-cache: 11.2.6 node-fetch-native: 1.6.7 ofetch: 1.5.1 @@ -24751,7 +24755,7 @@ snapshots: anymatch: 3.1.3 chokidar: 5.0.0 destr: 2.0.5 - h3: 1.15.5 + h3: 1.15.11 lru-cache: 11.2.6 node-fetch-native: 1.6.7 ofetch: 1.5.1 @@ -24769,7 +24773,7 @@ snapshots: untyped@2.0.0: dependencies: citty: 0.1.6 - defu: 6.1.4 + defu: 6.1.7 jiti: 2.6.1 knitwork: 1.3.0 scule: 1.3.0 @@ -24921,7 +24925,7 @@ snapshots: vite-dev-rpc@1.1.0(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.3)): dependencies: - birpc: 2.4.0 + birpc: 2.9.0 vite: 7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.3) vite-hot-client: 2.1.0(vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.46.0)(tsx@4.20.3)(yaml@2.8.3)) @@ -25028,7 +25032,7 @@ snapshots: vite@5.4.19(@types/node@22.19.1)(lightningcss@1.30.1)(terser@5.46.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.6 + postcss: 8.5.9 rollup: 4.44.1 optionalDependencies: '@types/node': 22.19.1 @@ -25041,7 +25045,7 @@ snapshots: esbuild: 0.25.5 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.6 + postcss: 8.5.9 rollup: 4.44.1 tinyglobby: 0.2.15 optionalDependencies: @@ -25486,7 +25490,7 @@ snapshots: xml2js@0.5.0: dependencies: - sax: 1.4.3 + sax: 1.6.0 xmlbuilder: 11.0.1 xmlbuilder@11.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 87c64dae4..88531aa43 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -37,6 +37,7 @@ packages: - examples/express - examples/koa - examples/next-integration +- examples/nuxt - examples/fastify - examples/next14-app-router - examples/next15-app-router From afaa1f374bf79b07768922ac9b796a7775e26b59 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Wed, 15 Apr 2026 08:00:50 +0000 Subject: [PATCH 2/5] Replace `~~/server` to `#server` --- examples/nuxt/nuxt.config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/nuxt/nuxt.config.ts b/examples/nuxt/nuxt.config.ts index d3f3a3fa0..b6642ac66 100644 --- a/examples/nuxt/nuxt.config.ts +++ b/examples/nuxt/nuxt.config.ts @@ -1,8 +1,6 @@ export default defineNuxtConfig({ modules: ["@fedify/nuxt"], - fedify: { - federationModule: "~/server/federation", - }, + fedify: { federationModule: "#server/federation" }, ssr: true, devServer: { host: "0.0.0.0" }, vite: { server: { allowedHosts: true } }, From 7497859db249cbfb399841b125778be25330322d Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sat, 18 Apr 2026 22:13:05 +0000 Subject: [PATCH 3/5] Format files --- .../example/public/style.css | 5 +- .../example/public/theme.js | 2 +- examples/nuxt/public/fedify-logo.svg | 394 ++++++++++-------- examples/nuxt/public/style.css | 5 +- examples/nuxt/public/theme.js | 2 +- 5 files changed, 224 insertions(+), 184 deletions(-) diff --git a/.agents/skills/create-example-app-with-integration/example/public/style.css b/.agents/skills/create-example-app-with-integration/example/public/style.css index 3d8b3e6e5..6cd70d7b7 100644 --- a/.agents/skills/create-example-app-with-integration/example/public/style.css +++ b/.agents/skills/create-example-app-with-integration/example/public/style.css @@ -20,8 +20,9 @@ body { background: var(--background); color: var(--foreground); font-size: 16px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - Oxygen, Ubuntu, Cantarell, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + Cantarell, sans-serif; } a { diff --git a/.agents/skills/create-example-app-with-integration/example/public/theme.js b/.agents/skills/create-example-app-with-integration/example/public/theme.js index 8d521d625..3da2aaacf 100644 --- a/.agents/skills/create-example-app-with-integration/example/public/theme.js +++ b/.agents/skills/create-example-app-with-integration/example/public/theme.js @@ -1,5 +1,5 @@ "use strict"; -var mq = window.matchMedia("(prefers-color-scheme: dark)"); +const mq = globalThis.window.matchMedia("(prefers-color-scheme: dark)"); document.body.classList.add(mq.matches ? "dark" : "light"); mq.addEventListener("change", function (e) { document.body.classList.remove("light", "dark"); diff --git a/examples/nuxt/public/fedify-logo.svg b/examples/nuxt/public/fedify-logo.svg index 812c62a32..e9d2e54ea 100644 --- a/examples/nuxt/public/fedify-logo.svg +++ b/examples/nuxt/public/fedify-logo.svg @@ -1,180 +1,218 @@ FedifyFedify + width="48" + height="48" + viewBox="0 0 112 112" + version="1.1" + id="svg5" + sodipodi:docname="fedify-logo.svg" + xml:space="preserve" + inkscape:version="1.4.3 (0d15f75, 2025-12-25)" + inkscape:export-filename="demo-profile.png" + inkscape:export-xdpi="1024" + inkscape:export-ydpi="1024" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/" +> + FedifyFedify + diff --git a/examples/nuxt/public/style.css b/examples/nuxt/public/style.css index 3d8b3e6e5..6cd70d7b7 100644 --- a/examples/nuxt/public/style.css +++ b/examples/nuxt/public/style.css @@ -20,8 +20,9 @@ body { background: var(--background); color: var(--foreground); font-size: 16px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - Oxygen, Ubuntu, Cantarell, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + Cantarell, sans-serif; } a { diff --git a/examples/nuxt/public/theme.js b/examples/nuxt/public/theme.js index 8d521d625..3da2aaacf 100644 --- a/examples/nuxt/public/theme.js +++ b/examples/nuxt/public/theme.js @@ -1,5 +1,5 @@ "use strict"; -var mq = window.matchMedia("(prefers-color-scheme: dark)"); +const mq = globalThis.window.matchMedia("(prefers-color-scheme: dark)"); document.body.classList.add(mq.matches ? "dark" : "light"); mq.addEventListener("change", function (e) { document.body.classList.remove("light", "dark"); From 0c6a7ad3ac5962bc14a54653149235a822ac5778 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sat, 18 Apr 2026 22:13:29 +0000 Subject: [PATCH 4/5] Apply necessary reviews --- examples/nuxt/server/api/events.get.ts | 2 +- examples/nuxt/server/api/post.post.ts | 13 +++++++++---- examples/nuxt/server/federation.ts | 21 +++++++++------------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/examples/nuxt/server/api/events.get.ts b/examples/nuxt/server/api/events.get.ts index e2f710e0d..8fad50104 100644 --- a/examples/nuxt/server/api/events.get.ts +++ b/examples/nuxt/server/api/events.get.ts @@ -1,7 +1,7 @@ import { setResponseHeader } from "h3"; import { addClient, removeClient } from "../sse"; -export default defineEventHandler(async (event) => { +export default defineEventHandler((event) => { setResponseHeader(event, "Content-Type", "text/event-stream"); setResponseHeader(event, "Cache-Control", "no-cache"); setResponseHeader(event, "Connection", "keep-alive"); diff --git a/examples/nuxt/server/api/post.post.ts b/examples/nuxt/server/api/post.post.ts index 03245ac3d..6a13a9df8 100644 --- a/examples/nuxt/server/api/post.post.ts +++ b/examples/nuxt/server/api/post.post.ts @@ -1,7 +1,12 @@ import { Create, Note } from "@fedify/vocab"; -import { readBody, sendRedirect, toWebRequest } from "h3"; -import federation from "../federation"; -import { postStore } from "../store"; +import { + defineEventHandler, + readBody, + sendRedirect, + toWebRequest, +} from "@nuxt/nitro-server/h3"; +import federation from "../federation.ts"; +import { postStore } from "../store.ts"; export default defineEventHandler(async (event) => { const body = await readBody(event); @@ -29,7 +34,7 @@ export default defineEventHandler(async (event) => { { identifier }, "followers", new Create({ - id: new URL("#activity", attribution), + id: new URL(`#create/${id}`, attribution), object: note, actors: note?.attributionIds, tos: note?.toIds, diff --git a/examples/nuxt/server/federation.ts b/examples/nuxt/server/federation.ts index de471c749..64c671019 100644 --- a/examples/nuxt/server/federation.ts +++ b/examples/nuxt/server/federation.ts @@ -15,8 +15,8 @@ import { type Recipient, Undo, } from "@fedify/vocab"; -import { broadcastEvent } from "./sse"; -import { keyPairsStore, postStore, relationStore } from "./store"; +import { broadcastEvent } from "./sse.ts"; +import { keyPairsStore, postStore, relationStore } from "./store.ts"; const federation = createFederation({ kv: new MemoryKvStore(), @@ -96,18 +96,15 @@ federation }) .on(Undo, async (context, undo) => { const activity = await undo.getObject(context); - if (activity instanceof Follow) { - if (activity.id == null) { - return; - } - if (undo.actorId == null) { - return; - } - relationStore.delete(undo.actorId.href); - broadcastEvent(); - } else { + if (!(activity instanceof Follow)) { console.debug(undo); + return; } + if (activity.id == null || undo.actorId == null) return; + const demoActorUri = context.getActorUri(IDENTIFIER); + if (activity.objectId?.href !== demoActorUri.href) return; + relationStore.delete(undo.actorId.href); + broadcastEvent(); }); federation.setObjectDispatcher( From 4876c0bf7f3c7cfa2737a543cae5273ed8b873b6 Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <2chanhaeng@gmail.com> Date: Sun, 19 Apr 2026 06:52:47 +0000 Subject: [PATCH 5/5] Move `@nuxt/kit` to peerDependencies for efficiency in `@fedify/nuxt` This makes mod.d.ts from 50k+ lines to only 32 lines --- deno.lock | 8 +------- packages/nuxt/package.json | 4 ++-- pnpm-lock.yaml | 8 ++++---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/deno.lock b/deno.lock index a5f4bd148..93e5ff6c0 100644 --- a/deno.lock +++ b/deno.lock @@ -80,7 +80,6 @@ "npm:@nestjs/common@^11.0.1": "11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2", "npm:@nuxt/kit@4": "4.4.2", "npm:@nuxt/schema@4": "4.4.2", - "npm:@nuxt/schema@^4.4.0": "4.4.2", "npm:@opentelemetry/api@^1.9.0": "1.9.1", "npm:@opentelemetry/context-async-hooks@^2.5.0": "2.6.1_@opentelemetry+api@1.9.1", "npm:@opentelemetry/core@^2.5.0": "2.6.1_@opentelemetry+api@1.9.1", @@ -9485,12 +9484,7 @@ "npm:@nuxt/kit@4", "npm:@nuxt/schema@4", "npm:h3@^1.15.0" - ], - "packageJson": { - "dependencies": [ - "npm:@nuxt/schema@^4.4.0" - ] - } + ] }, "packages/relay": { "dependencies": [ diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index a5918cd60..2f34ebdd8 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -51,14 +51,14 @@ ], "peerDependencies": { "@fedify/fedify": "workspace:^", - "@nuxt/kit": "^4.4.0", + "@nuxt/kit": "^4.4.2", + "@nuxt/schema": "^4.4.2", "h3": "catalog:", "nuxt": "catalog:" }, "devDependencies": { "@fedify/fixture": "workspace:^", "@types/node": "catalog:", - "@nuxt/schema": "^4.4.0", "tsdown": "catalog:", "typescript": "catalog:" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9bead79d..0a14490f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1430,8 +1430,11 @@ importers: specifier: workspace:^ version: link:../fedify '@nuxt/kit': - specifier: ^4.4.0 + specifier: ^4.4.2 version: 4.4.2(magicast@0.5.2) + '@nuxt/schema': + specifier: ^4.4.2 + version: 4.4.2 h3: specifier: 'catalog:' version: 1.15.3 @@ -1442,9 +1445,6 @@ importers: '@fedify/fixture': specifier: workspace:^ version: link:../fixture - '@nuxt/schema': - specifier: ^4.4.0 - version: 4.4.2 '@types/node': specifier: 'catalog:' version: 22.19.1