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/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/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..b6642ac66 --- /dev/null +++ b/examples/nuxt/nuxt.config.ts @@ -0,0 +1,7 @@ +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 000000000..2c9883bcc Binary files /dev/null and b/examples/nuxt/public/demo-profile.png differ diff --git a/examples/nuxt/public/fedify-logo.svg b/examples/nuxt/public/fedify-logo.svg new file mode 100644 index 000000000..e9d2e54ea --- /dev/null +++ b/examples/nuxt/public/fedify-logo.svg @@ -0,0 +1,218 @@ + + + FedifyFedify + diff --git a/examples/nuxt/public/style.css b/examples/nuxt/public/style.css new file mode 100644 index 000000000..6cd70d7b7 --- /dev/null +++ b/examples/nuxt/public/style.css @@ -0,0 +1,505 @@ +: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..3da2aaacf --- /dev/null +++ b/examples/nuxt/public/theme.js @@ -0,0 +1,7 @@ +"use strict"; +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"); + 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..8fad50104 --- /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((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..6a13a9df8 --- /dev/null +++ b/examples/nuxt/server/api/post.post.ts @@ -0,0 +1,49 @@ +import { Create, Note } from "@fedify/vocab"; +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); + 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(`#create/${id}`, 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..64c671019 --- /dev/null +++ b/examples/nuxt/server/federation.ts @@ -0,0 +1,160 @@ +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.ts"; +import { keyPairsStore, postStore, relationStore } from "./store.ts"; + +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)) { + 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( + 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/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 1981f5154..0a14490f4 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': @@ -1396,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 @@ -1408,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 @@ -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