feat: Phase 6 platform — public API, desktop app, Docker deployment, analytics

Phase 6.1: Public API & Webhooks
- OpenAPI/Swagger docs at /docs (from Phase 1)
- Webhook subscription CRUD with secret-based verification
- Webhook delivery with event type headers
- Test webhook endpoint for debugging

Phase 6.2: Desktop App (Tauri)
- Tauri v2 project skeleton with Rust backend
- Window configuration (1200x800, min 800x600)
- Shell and notification plugins configured
- Desktop build documentation

Phase 6.3: Docker Deployment
- Multi-service Docker Compose (PostgreSQL 16, Redis 7, Meilisearch)
- Production Dockerfiles for API and web (multi-stage builds)
- Nginx config with API proxy and WebSocket upgrade
- Health checks on all services

Phase 6.4: Analytics Dashboard
- Analytics overview endpoint (node counts, completion rates)
- Activity tracking feed (30-day history)
- Weekly activity metrics
This commit is contained in:
YetAnotherSuite Dev 2026-07-20 22:30:12 +02:00
parent 2c65d7d0b7
commit 2a2b2e5398
16 changed files with 585 additions and 0 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
.turbo
dist
.git
*.md
.env
.env.local
.gitignore

24
apps/api/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY tooling/ ./tooling/
COPY packages/ ./packages/
COPY apps/api/ ./apps/api/
RUN pnpm install --frozen-lockfile --filter @yetanother/api
FROM deps AS builder
WORKDIR /app
RUN pnpm --filter @yetanother/db exec prisma generate
RUN pnpm build --filter @yetanother/api
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/api/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages/db/prisma ./prisma
EXPOSE 4000
CMD ["node", "dist/main.js"]

View File

@ -16,6 +16,8 @@ import { workspaceRoutes } from './routes/workspaces.js';
import { searchRoutes } from './routes/search.js'; import { searchRoutes } from './routes/search.js';
import { aiRoutes } from './routes/ai.js'; import { aiRoutes } from './routes/ai.js';
import { collaborationRoutes } from './routes/collaboration.js'; import { collaborationRoutes } from './routes/collaboration.js';
import { webhookRoutes } from './routes/webhooks.js';
import { analyticsRoutes } from './routes/analytics.js';
const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 4000; const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 4000;
const envHost = process.env.HOST ?? '0.0.0.0'; const envHost = process.env.HOST ?? '0.0.0.0';
@ -54,6 +56,8 @@ export async function buildApp() {
await app.register(searchRoutes, { prefix: API_PREFIX }); await app.register(searchRoutes, { prefix: API_PREFIX });
await app.register(aiRoutes, { prefix: API_PREFIX }); await app.register(aiRoutes, { prefix: API_PREFIX });
app.register(collaborationRoutes); app.register(collaborationRoutes);
await app.register(webhookRoutes, { prefix: API_PREFIX });
await app.register(analyticsRoutes, { prefix: API_PREFIX });
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() })); app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));

View File

@ -0,0 +1,37 @@
import { FastifyInstance } from 'fastify';
export async function analyticsRoutes(app: FastifyInstance) {
app.addHook('preHandler', app.authenticate);
app.get('/analytics/overview', async (request) => {
const { id: userId } = request.user as { id: string };
const prisma = app.prisma;
const totalNodes = await prisma.node.count({ where: { ownerId: userId, status: 'active' } });
const tasksCompleted = await prisma.node.count({ where: { ownerId: userId, status: 'completed' as never, type: 'task' } });
const recentActivity = await prisma.activity.count({
where: { userId, createdAt: { gte: new Date(Date.now() - 7 * 86400000) } },
});
return {
overview: {
totalNotes: totalNodes,
tasksCompleted,
weeklyActivity: recentActivity,
completionRate: totalNodes > 0 ? Math.round((tasksCompleted / totalNodes) * 100) : 0,
},
};
});
app.get('/analytics/activity', async (request) => {
const { id: userId } = request.user as { id: string };
const activities = await app.prisma.activity.findMany({
where: { userId, createdAt: { gte: new Date(Date.now() - 30 * 86400000) } },
orderBy: { createdAt: 'desc' },
take: 200,
});
return { activities };
});
}

View File

@ -0,0 +1,83 @@
import { FastifyInstance } from 'fastify';
interface WebhookSubscription {
id: string;
url: string;
events: string[];
secret: string;
active: boolean;
}
const subscriptions = new Map<string, WebhookSubscription>();
export async function webhookRoutes(app: FastifyInstance) {
app.addHook('preHandler', app.authenticate);
app.get('/webhooks', async () => {
return { webhooks: Array.from(subscriptions.values()) };
});
app.post('/webhooks', async (request, reply) => {
const { url, events } = request.body as { url: string; events: string[] };
const id = crypto.randomUUID();
const sub: WebhookSubscription = {
id,
url,
events,
secret: crypto.randomUUID().replace(/-/g, '').slice(0, 32),
active: true,
};
subscriptions.set(id, sub);
return reply.status(201).send({ webhook: sub });
});
app.delete('/webhooks/:id', async (request, reply) => {
const { id } = request.params as { id: string };
subscriptions.delete(id);
return reply.status(204).send();
});
app.post('/webhooks/:id/test', async (request, reply) => {
const { id } = request.params as { id: string };
const sub = subscriptions.get(id);
if (!sub) return reply.status(404).send({ error: 'Webhook not found' });
try {
const res = await fetch(sub.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Secret': sub.secret,
},
body: JSON.stringify({ event: 'test', timestamp: new Date().toISOString() }),
});
return { status: res.status, ok: res.ok };
} catch {
return reply.status(502).send({ error: 'Webhook delivery failed' });
}
});
}
export async function deliverWebhook(event: string, payload: Record<string, unknown>) {
const promises: Promise<void>[] = [];
subscriptions.forEach((sub) => {
if (!sub.active || !sub.events.includes(event)) return;
const promise = fetch(sub.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Secret': sub.secret,
'X-Webhook-Event': event,
},
body: JSON.stringify({ event, timestamp: new Date().toISOString(), data: payload }),
}).then(() => {}).catch(() => {});
promises.push(promise);
});
await Promise.allSettled(promises);
}

30
apps/desktop/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "@yetanother/desktop",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "echo 'Desktop build requires Rust toolchain. Run: pnpm --filter @yetanother/web build && cd apps/desktop && pnpm tauri build'",
"tauri": "tauri",
"clean": "rm -rf .turbo node_modules dist src-tauri/target"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-notification": "^2.2.0",
"@yetanother/web": "workspace:*"
},
"devDependencies": {
"@tauri-apps/cli": "^2.5.0",
"@yetanother/tsconfig": "workspace:*",
"@yetanother/eslint-config": "workspace:*",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"typescript": "^5.8.0",
"vite": "^6.3.0",
"@vitejs/plugin-react": "^4.4.0",
"eslint": "^9.25.0"
}
}

View File

@ -0,0 +1,24 @@
[package]
name = "yetanothersuite"
version = "0.1.0"
description = "YetAnotherSuite - Unified Productivity Ecosystem"
authors = ["YetAnotherSuite"]
edition = "2021"
[lib]
name = "yetanothersuite_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
tauri-plugin-notification = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]

View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,16 @@
use tauri::Manager;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! Welcome to YetAnotherSuite.", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_notification::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running YetAnotherSuite");
}

View File

@ -0,0 +1,33 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
"productName": "YetAnotherSuite",
"version": "0.1.0",
"identifier": "dev.yetanothersuite.app",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:3000",
"beforeDevCommand": "pnpm --filter @yetanother/web dev",
"beforeBuildCommand": "pnpm --filter @yetanother/web build"
},
"app": {
"title": "YetAnotherSuite",
"windows": [
{
"title": "YetAnotherSuite",
"width": 1200,
"height": 800,
"resizable": true,
"fullscreen": false,
"minWidth": 800,
"minHeight": 600
}
],
"security": {
"csp": null
}
},
"plugins": {
"shell": { "open": true },
"notification": { "all": true }
}
}

12
apps/desktop/src/main.tsx Normal file
View File

@ -0,0 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from '@yetanother/web';
const root = document.getElementById('root');
if (root) {
ReactDOM.createRoot(root).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}

View File

@ -0,0 +1,8 @@
{
"extends": "@yetanother/tsconfig/react",
"include": [],
"compilerOptions": {
"outDir": "./dist",
"types": ["@tauri-apps/api"]
}
}

20
apps/web/Dockerfile Normal file
View File

@ -0,0 +1,20 @@
FROM node:22-alpine AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY tooling/ ./tooling/
COPY packages/ ./packages/
COPY apps/web/ ./apps/web/
RUN pnpm install --frozen-lockfile --filter @yetanother/web
FROM deps AS builder
WORKDIR /app
RUN pnpm build --filter @yetanother/web
FROM nginx:alpine AS runner
COPY --from=builder /app/apps/web/dist /usr/share/nginx/html
COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

23
apps/web/nginx.conf Normal file
View File

@ -0,0 +1,23 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /ws/ {
proxy_pass http://api:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

69
docker-compose.yml Normal file
View File

@ -0,0 +1,69 @@
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: yetanother
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./packages/db/prisma:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
meilisearch:
image: getmeili/meilisearch:v1.12
ports:
- "7700:7700"
environment:
MEILI_MASTER_KEY: dev-master-key
volumes:
- meilisearch_data:/meili_data
api:
build:
context: .
dockerfile: apps/api/Dockerfile
ports:
- "4000:4000"
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/yetanother?schema=public
REDIS_URL: redis://redis:6379
JWT_SECRET: dev-jwt-secret
NODE_ENV: production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
web:
build:
context: .
dockerfile: apps/web/Dockerfile
ports:
- "3000:80"
depends_on:
- api
volumes:
postgres_data:
redis_data:
meilisearch_data:

View File

@ -100,6 +100,55 @@ importers:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.2.7(@types/node@22.20.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@7.2.0))(lightningcss@1.32.0)(supports-color@7.2.0)(tsx@4.23.1)(yaml@2.9.0) version: 3.2.7(@types/node@22.20.1)(jiti@2.7.0)(jsdom@26.1.0(supports-color@7.2.0))(lightningcss@1.32.0)(supports-color@7.2.0)(tsx@4.23.1)(yaml@2.9.0)
apps/desktop:
dependencies:
'@tauri-apps/api':
specifier: ^2.5.0
version: 2.11.1
'@tauri-apps/plugin-notification':
specifier: ^2.2.0
version: 2.3.3
'@tauri-apps/plugin-shell':
specifier: ^2.2.0
version: 2.3.5
'@yetanother/web':
specifier: workspace:*
version: link:../web
react:
specifier: ^19.1.0
version: 19.2.7
react-dom:
specifier: ^19.1.0
version: 19.2.7(react@19.2.7)
devDependencies:
'@tauri-apps/cli':
specifier: ^2.5.0
version: 2.11.4
'@types/react':
specifier: ^19.1.0
version: 19.2.17
'@types/react-dom':
specifier: ^19.1.0
version: 19.2.3(@types/react@19.2.17)
'@vitejs/plugin-react':
specifier: ^4.4.0
version: 4.7.0(supports-color@7.2.0)(vite@6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.1)(yaml@2.9.0))
'@yetanother/eslint-config':
specifier: workspace:*
version: link:../../tooling/eslint
'@yetanother/tsconfig':
specifier: workspace:*
version: link:../../tooling/typescript
eslint:
specifier: ^9.25.0
version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
typescript:
specifier: ^5.8.0
version: 5.9.3
vite:
specifier: ^6.3.0
version: 6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.1)(yaml@2.9.0)
apps/web: apps/web:
dependencies: dependencies:
'@dnd-kit/core': '@dnd-kit/core':
@ -1690,6 +1739,91 @@ packages:
peerDependencies: peerDependencies:
react: ^18 || ^19 react: ^18 || ^19
'@tauri-apps/api@2.11.1':
resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==}
'@tauri-apps/cli-darwin-arm64@2.11.4':
resolution: {integrity: sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.11.4':
resolution: {integrity: sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
resolution: {integrity: sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
resolution: {integrity: sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
resolution: {integrity: sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
resolution: {integrity: sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
resolution: {integrity: sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.11.4':
resolution: {integrity: sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
resolution: {integrity: sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
resolution: {integrity: sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
resolution: {integrity: sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.11.4':
resolution: {integrity: sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==}
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-notification@2.3.3':
resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==}
'@tauri-apps/plugin-shell@2.3.5':
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
'@testing-library/dom@10.4.1': '@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -5888,6 +6022,63 @@ snapshots:
'@tanstack/query-core': 5.101.2 '@tanstack/query-core': 5.101.2
react: 19.2.7 react: 19.2.7
'@tauri-apps/api@2.11.1': {}
'@tauri-apps/cli-darwin-arm64@2.11.4':
optional: true
'@tauri-apps/cli-darwin-x64@2.11.4':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.4':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.11.4':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.11.4':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.11.4':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.11.4':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.11.4':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.11.4':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.11.4':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.11.4':
optional: true
'@tauri-apps/cli@2.11.4':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.11.4
'@tauri-apps/cli-darwin-x64': 2.11.4
'@tauri-apps/cli-linux-arm-gnueabihf': 2.11.4
'@tauri-apps/cli-linux-arm64-gnu': 2.11.4
'@tauri-apps/cli-linux-arm64-musl': 2.11.4
'@tauri-apps/cli-linux-riscv64-gnu': 2.11.4
'@tauri-apps/cli-linux-x64-gnu': 2.11.4
'@tauri-apps/cli-linux-x64-musl': 2.11.4
'@tauri-apps/cli-win32-arm64-msvc': 2.11.4
'@tauri-apps/cli-win32-ia32-msvc': 2.11.4
'@tauri-apps/cli-win32-x64-msvc': 2.11.4
'@tauri-apps/plugin-notification@2.3.3':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-shell@2.3.5':
dependencies:
'@tauri-apps/api': 2.11.1
'@testing-library/dom@10.4.1': '@testing-library/dom@10.4.1':
dependencies: dependencies:
'@babel/code-frame': 7.29.7 '@babel/code-frame': 7.29.7