diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b75593e..f915fd28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,6 +228,46 @@ jobs: run: for i in $(seq 1 30); do curl -s -o /dev/null localhost:9000 && exit 0; sleep 1; done; exit 1 - run: pnpm --filter @easy-cms/storage-s3 test + multi-instance: + needs: scope + if: needs.scope.outputs.full == 'true' + # Two `easy-cms serve` instances on one Postgres and one S3 bucket, behind a round-robin + # proxy (ADR-0051): REST, sessions and CSRF, login rate limits, uploads and jobs. + runs-on: ubuntu-24.04 + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: easycms + POSTGRES_PASSWORD: easycms + POSTGRES_DB: easycms + ports: ['5432:5432'] + options: >- + --health-cmd "pg_isready -U easycms" --health-interval 5s --health-timeout 5s --health-retries 10 + s3: + image: rustfs/rustfs:1.0.0 + env: + RUSTFS_ACCESS_KEY: ecmsaccess + RUSTFS_SECRET_KEY: ecmssecret123 + ports: ['9000:9000'] + env: + POSTGRES_URL: postgres://easycms:easycms@localhost:5432/easycms + S3_TEST_ENDPOINT: http://localhost:9000 + S3_TEST_ACCESS_KEY: ecmsaccess + S3_TEST_SECRET_KEY: ecmssecret123 + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build + - name: Wait for the S3 server + run: for i in $(seq 1 30); do curl -s -o /dev/null localhost:9000 && exit 0; sleep 1; done; exit 1 + - run: pnpm --filter easy-cms-integration-tests test:multi-instance + e2e: needs: scope if: needs.scope.outputs.full == 'true' diff --git a/packages/integration/multi-instance/easy-cms.config.ts b/packages/integration/multi-instance/easy-cms.config.ts new file mode 100644 index 00000000..02bf9b22 --- /dev/null +++ b/packages/integration/multi-instance/easy-cms.config.ts @@ -0,0 +1,46 @@ +// Run by `easy-cms serve` as two instances against one Postgres and one S3 bucket (CI's +// multi-instance job), as the reference architecture describes (ADR-0051). +import { defineConfig } from '@easy-cms/core' +import { postgres } from '@easy-cms/db-postgres' +import { s3Storage } from '@easy-cms/storage-s3' + +const env = (name: string) => { + const value = process.env[name] + if (!value) throw new Error(`${name} is not set`) + return value +} + +export default defineConfig({ + secret: env('EASY_CMS_SECRET'), + cronSecret: env('CRON_SECRET'), + db: postgres({ url: env('POSTGRES_URL'), max: 5 }), + upload: { + storage: s3Storage({ + bucket: env('S3_TEST_BUCKET'), + endpoint: env('S3_TEST_ENDPOINT'), + region: 'us-east-1', + accessKeyId: env('S3_TEST_ACCESS_KEY'), + secretAccessKey: env('S3_TEST_SECRET_KEY'), + }), + }, + collections: [ + { + slug: 'posts', + drafts: true, + schedule: true, + access: { read: () => true }, + fields: [{ name: 'title', type: 'text' }], + }, + // One document per run of the job below. + { slug: 'runs', fields: [{ name: 'at', type: 'text' }] }, + ], + jobs: [ + { + name: 'test:count-runs', + every: 3600, + run: async ({ cms, now }) => { + await cms.create('runs', { at: now.toISOString() }) + }, + }, + ], +}) diff --git a/packages/integration/multi-instance/multi-instance.test.ts b/packages/integration/multi-instance/multi-instance.test.ts new file mode 100644 index 00000000..0f5c49e1 --- /dev/null +++ b/packages/integration/multi-instance/multi-instance.test.ts @@ -0,0 +1,261 @@ +// Two `easy-cms serve` instances on one Postgres and one S3 bucket, behind a round-robin proxy: +// the reference architecture (ADR-0051). Assertions that need a request to reach a given +// instance call it directly; the rest go through the proxy, alternating between them. +import { type ChildProcess, execFile, spawn } from 'node:child_process' +import { mkdtempSync, rmSync } from 'node:fs' +import { createServer, request as httpRequest, type Server } from 'node:http' +import type { AddressInfo } from 'node:net' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' +import { createEasyCMS, type EasyCMS, silentLogger } from '@easy-cms/core' +import { AwsClient } from 'aws4fetch' +import sharp from 'sharp' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import config from './easy-cms.config.js' + +const configFile = fileURLToPath(new URL('./easy-cms.config.ts', import.meta.url)) +const bin = fileURLToPath(new URL('../bin/easy-cms.js', import.meta.resolve('easy-cms'))) +const PASSWORD = 'password123' +const cwd = mkdtempSync(join(tmpdir(), 'easy-cms-multi-')) +// The instances run as in production; the secrets and bucket come from vitest.config.ts. +const env = { ...process.env, NODE_ENV: 'production' } + +const instances: ChildProcess[] = [] +let a: string +let b: string +let proxy: Server +let lb: string +/** The Local API on the same database, to set up and check what the instances did. */ +let cms: EasyCMS + +async function freePort() { + const server = createServer().listen(0) + await new Promise((resolve) => server.once('listening', resolve)) + const { port } = server.address() as AddressInfo + await new Promise((resolve) => server.close(resolve)) + return port +} + +async function cli(...args: string[]) { + await promisify(execFile)( + process.execPath, + [bin, ...args, '--config', configFile, '--cwd', cwd], + { + env, + }, + ) +} + +async function serve() { + const port = await freePort() + const child = spawn( + process.execPath, + [bin, 'serve', '--port', String(port), '--trust-proxy', '--config', configFile, '--cwd', cwd], + { env, stdio: ['ignore', 'inherit', 'inherit'] }, + ) + instances.push(child) + const url = `http://127.0.0.1:${port}` + for (let i = 0; i < 120; i++) { + if (child.exitCode !== null) throw new Error(`serve exited with ${child.exitCode}`) + if ( + await fetch(`${url}/healthz`).then( + (r) => r.ok, + () => false, + ) + ) + return url + await new Promise((resolve) => setTimeout(resolve, 250)) + } + throw new Error('serve did not start') +} + +/** Round robin, as a load balancer without sticky sessions; passes the client IP on. */ +function roundRobin(targets: string[]) { + let next = 0 + return createServer((req, res) => { + const target = new URL(targets[next++ % targets.length] as string) + const upstream = httpRequest( + { + host: target.hostname, + port: target.port, + method: req.method, + path: req.url, + headers: { + ...req.headers, + 'x-forwarded-for': (req.headers['x-test-ip'] as string) ?? '203.0.113.1', + 'x-forwarded-proto': 'http', + }, + }, + (response) => { + res.writeHead(response.statusCode ?? 502, response.headers) + response.pipe(res) + }, + ) + upstream.on('error', () => res.writeHead(502).end()) + req.pipe(upstream) + }).listen(0) +} + +// Same origin as the instance called: in production both answer on one hostname. +const api = (base: string, path: string, init: RequestInit = {}) => + fetch(`${base}/api/cms${path}`, { + ...init, + headers: { origin: base, ...(init.headers as Record) }, + }) +const json = (body: unknown, headers: Record = {}) => ({ + method: 'POST', + headers: { 'content-type': 'application/json', ...headers }, + body: JSON.stringify(body), +}) + +/** Logs in on one instance and returns headers that authenticate like a browser. */ +async function browser(base: string, email: string) { + const login = await api(base, '/auth/login', json({ email, password: PASSWORD })) + expect(login.status).toBe(200) + const cookie = login.headers + .getSetCookie() + // `__Host-` in production + .find((c) => /^(__Host-)?ecms-session=/.test(c)) + ?.split(';')[0] as string + const { csrfToken } = (await login.json()) as { csrfToken: string } + return { cookie, 'x-csrf-token': csrfToken } +} + +beforeAll(async () => { + const s3 = new AwsClient({ + accessKeyId: process.env.S3_TEST_ACCESS_KEY as string, + secretAccessKey: process.env.S3_TEST_SECRET_KEY as string, + service: 's3', + region: 'us-east-1', + }) + expect( + ( + await s3.fetch(`${process.env.S3_TEST_ENDPOINT}/${process.env.S3_TEST_BUCKET}`, { + method: 'PUT', + }) + ).ok, + ).toBe(true) + // As in production: migrate once, then start the instances (which refuse pending migrations). + await cli('migrate:create', 'init') + await cli('migrate') + ;[a, b] = await Promise.all([serve(), serve()]) + proxy = roundRobin([a as string, b as string]) + await new Promise((resolve) => proxy.once('listening', resolve)) + lb = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}` + cms = await createEasyCMS(config, { + cwd, + schema: 'verify', + scheduler: false, + logger: silentLogger, + }) + await cms.create('users', { email: 'admin@x.co', password: PASSWORD, role: 'admin' }) +}, 120_000) + +afterAll(async () => { + for (const child of instances) child.kill('SIGTERM') + proxy?.close() + await cms?.destroy() + rmSync(cwd, { recursive: true, force: true }) +}) + +describe('two instances behind a load balancer', () => { + it('serves REST writes and reads from either instance', async () => { + const admin = await browser(a, 'admin@x.co') + const created = await api(b, '/posts', json({ title: 'From B', status: 'published' }, admin)) + expect(created.status).toBe(201) + const { id } = (await created.json()) as { id: number } + expect(await (await api(a, `/posts/${id}`)).json()).toMatchObject({ title: 'From B' }) + + const updated = await api(a, `/posts/${id}`, { + ...json({ title: 'Edited on A', status: 'published' }, admin), + method: 'PATCH', + }) + expect(updated.status).toBe(200) + expect(await (await api(b, `/posts/${id}`)).json()).toMatchObject({ title: 'Edited on A' }) + + // Through the balancer: every request lands on the other instance than the one before. + for (let i = 0; i < 6; i++) { + expect(await (await api(lb, `/posts/${id}`)).json()).toMatchObject({ title: 'Edited on A' }) + } + expect((await api(b, `/posts/${id}`, { method: 'DELETE', headers: admin })).status).toBe(200) + expect((await api(a, `/posts/${id}`)).status).toBe(404) + }) + + it('keeps a session made on one instance on the other, and ends it everywhere', async () => { + const onA = await browser(a, 'admin@x.co') + const me = (await (await api(b, '/auth/me', { headers: { cookie: onA.cookie } })).json()) as { + user: { email: string } | null + csrfToken: string + } + expect(me.user?.email).toBe('admin@x.co') + // The CSRF token from A is accepted by B. + expect(me.csrfToken).toBe(onA['x-csrf-token']) + expect((await api(b, '/posts', json({ title: 'csrf' }, onA))).status).toBe(201) + expect( + (await api(b, '/posts', json({ title: 'csrf' }, { ...onA, 'x-csrf-token': 'wrong' }))).status, + ).toBe(403) + + expect((await api(b, '/auth/logout', { method: 'POST', headers: onA })).status).toBe(200) + const after = await (await api(a, '/auth/me', { headers: { cookie: onA.cookie } })).json() + expect(after).toMatchObject({ user: null }) + }) + + it('counts failed logins from one client across instances', async () => { + const attempt = (password: string) => + api( + lb, + '/auth/login', + json({ email: 'admin@x.co', password }, { 'x-test-ip': '198.51.100.9' }), + ) + for (let i = 0; i < 5; i++) expect((await attempt('wrong-password')).status).toBe(401) + // Locked out on both instances, even with the right password. + expect((await attempt(PASSWORD)).status).toBe(429) + expect((await attempt(PASSWORD)).status).toBe(429) + }) + + it('serves a file uploaded through one instance from the other', async () => { + const admin = await browser(a, 'admin@x.co') + const form = new FormData() + const png = await sharp({ + create: { width: 4, height: 4, channels: 3, background: '#c00' }, + }) + .png() + .toBuffer() + form.set('file', new Blob([new Uint8Array(png)], { type: 'image/png' }), 'red.png') + const uploaded = await api(a, '/media', { method: 'POST', headers: admin, body: form }) + expect(uploaded.status).toBe(201) + const media = (await uploaded.json()) as { id: number; url: string } + const file = await fetch(`${b}${media.url}`) + expect(file.status).toBe(200) + expect(Buffer.from(await file.arrayBuffer()).equals(png)).toBe(true) + expect((await api(b, `/media/${media.id}`, { method: 'DELETE', headers: admin })).status).toBe( + 200, + ) + expect((await fetch(`${a}${media.url}`)).status).toBe(404) + }) + + it('runs each scheduled publish and each job once when both instances run jobs', async () => { + const posts = await Promise.all( + Array.from({ length: 20 }, (_, i) => cms.create('posts', { title: `Scheduled ${i}` })), + ) + const at = new Date(Date.now() - 1000) + for (const post of posts) await cms.schedule('posts', post.id, { action: 'publish', at }) + + // A cron service and both instances' schedulers, all at once. + const runs = await Promise.all( + Array.from({ length: 10 }, (_, i) => + api(i % 2 ? a : b, '/jobs/run', { + headers: { authorization: `Bearer ${process.env.CRON_SECRET}` }, + }).then((r) => r.json() as Promise<{ scheduled: { ran: number }; jobs: { ran: number } }>), + ), + ) + expect(runs.reduce((sum, run) => sum + run.scheduled.ran, 0)).toBe(20) + expect(runs.reduce((sum, run) => sum + run.jobs.ran, 0)).toBe(1) + expect(await cms.count('runs')).toBe(1) + for (const post of posts) { + expect(await cms.findById('posts', post.id)).toMatchObject({ status: 'published' }) + } + }) +}) diff --git a/packages/integration/multi-instance/vitest.config.ts b/packages/integration/multi-instance/vitest.config.ts new file mode 100644 index 00000000..8d846a1a --- /dev/null +++ b/packages/integration/multi-instance/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' + +// Not part of `vitest run`: needs Postgres and an S3 server (CI's multi-instance job). +export default defineConfig({ + test: { + root: import.meta.dirname, + testTimeout: 60_000, + hookTimeout: 120_000, + // Read by easy-cms.config.ts, in the test and in the instances it starts. + env: { + EASY_CMS_SECRET: 'multi-instance-test-secret-0123456789abcdef', + CRON_SECRET: 'multi-instance-cron-secret-0123456789abcdef', + S3_TEST_BUCKET: `ecms-multi-${Date.now()}`, + }, + }, +}) diff --git a/packages/integration/package.json b/packages/integration/package.json index d8c02cf0..a3fc977d 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -6,15 +6,19 @@ "typecheck": "tsc --noEmit", "test": "vitest run", "bench": "node bench.ts", - "load": "node load.ts" + "load": "node load.ts", + "test:multi-instance": "vitest run --config multi-instance/vitest.config.ts" }, "devDependencies": { "@easy-cms/auth-oauth": "workspace:^", "@easy-cms/core": "workspace:^", "@easy-cms/db-postgres": "workspace:^", "@easy-cms/db-sqlite": "workspace:^", + "@easy-cms/storage-s3": "workspace:^", "@electric-sql/pglite": "^0.5.8", "@libsql/client": "^0.18.0", + "aws4fetch": "^1.0.20", + "easy-cms": "workspace:^", "postgres": "^3.4.9", "sharp": "^0.35.5" } diff --git a/packages/integration/tsconfig.json b/packages/integration/tsconfig.json index cc07dbe5..32d5b74c 100644 --- a/packages/integration/tsconfig.json +++ b/packages/integration/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.base.json", - "include": ["test", "vitest.config.ts", "bench.ts"] + "include": ["test", "multi-instance", "vitest.config.ts", "bench.ts"] } diff --git a/packages/integration/vitest.config.ts b/packages/integration/vitest.config.ts index 327de440..56b73e94 100644 --- a/packages/integration/vitest.config.ts +++ b/packages/integration/vitest.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vitest/config' +import { configDefaults, defineConfig } from 'vitest/config' // The same suites run against every database: SQLite, PGlite (Postgres in WebAssembly), // and a real Postgres server when POSTGRES_URL is set (CI). @@ -9,6 +9,8 @@ export default defineConfig({ // The first test of a file starts PGlite and loads drizzle-kit. testTimeout: 90_000, hookTimeout: 90_000, + // Run on its own: multi-instance/vitest.config.ts. + exclude: [...configDefaults.exclude, 'multi-instance/**'], projects: dialects.map((dialect) => ({ extends: true, test: { name: dialect, env: { EASY_CMS_TEST_DIALECT: dialect } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ceb544f..c4d67b60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -446,12 +446,21 @@ importers: '@easy-cms/db-sqlite': specifier: workspace:^ version: link:../db-sqlite + '@easy-cms/storage-s3': + specifier: workspace:^ + version: link:../storage-s3 '@electric-sql/pglite': specifier: ^0.5.8 version: 0.5.8 '@libsql/client': specifier: ^0.18.0 version: 0.18.0 + aws4fetch: + specifier: ^1.0.20 + version: 1.0.20 + easy-cms: + specifier: workspace:^ + version: link:../cli postgres: specifier: ^3.4.9 version: 3.4.9 @@ -507,7 +516,7 @@ importers: version: 4.3.2(@playwright/test@1.63.0)(@vue/test-utils@2.5.1(@vue/compiler-dom@3.5.44)(@vue/server-renderer@3.5.44)(vue@3.5.43(typescript@6.0.3)))(esbuild@0.28.2)(happy-dom@20.14.5)(magicast@0.5.5)(playwright-core@1.63.0)(rolldown@1.2.14)(rollup@4.64.7)(typescript@6.0.3)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vitest@5.0.1) nuxt: specifier: ^4.6.2 - version: 4.6.2(8341e7b36084563b1d1df6456ea53827) + version: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@biomejs/biome@2.5.14)(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@netlify/blobs@11.1.3)(@oxc-project/types@0.154.0)(@types/node@22.20.4)(@vercel/blob@2.8.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(aws4fetch@1.0.20)(confbox@0.3.1)(db0@0.3.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9)))(dotenv@17.4.2)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9))(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.7))(rollup@4.64.7)(srvx@1.0.5)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1) vue: specifier: ^3.5.0 version: 3.5.43(typescript@6.0.3) @@ -8430,6 +8439,99 @@ snapshots: - webpack - xml2js + '@nuxt/nitro-server@4.6.2(c810ee51f9945db652f39682956d9e9c)': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))) + '@unhead/vue': 3.4.3(@oxc-project/types@0.154.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + consola: 3.4.2 + defu: 6.1.7 + destr: 2.0.5 + devalue: 6.0.3 + errx: 0.2.2 + escape-string-regexp: 5.0.0 + exsolve: 1.1.3 + flru: 1.0.2 + h3: 1.15.11 + impound: 1.2.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + klona: 2.0.6 + mocked-exports: 0.1.1 + my-bad: 0.2.11(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + nitropack: 2.13.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@netlify/blobs@11.1.3)(@vercel/blob@2.8.0)(aws4fetch@1.0.20)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9))(rolldown@1.2.14)(srvx@1.0.5)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + nostics: 1.3.0 + nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@biomejs/biome@2.5.14)(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@netlify/blobs@11.1.3)(@oxc-project/types@0.154.0)(@types/node@22.20.4)(@vercel/blob@2.8.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(aws4fetch@1.0.20)(confbox@0.3.1)(db0@0.3.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9)))(dotenv@17.4.2)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9))(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.7))(rollup@4.64.7)(srvx@1.0.5)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1) + ohash: 2.0.12 + pathe: 2.0.3 + radix3: 1.1.2 + rou3: 0.9.2 + std-env: 4.3.0 + ufo: 1.6.4 + unctx: 3.0.1(magic-string@1.4.3)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))) + unrouting: 0.2.3 + unstorage: 1.17.6(@netlify/blobs@11.1.3)(@vercel/blob@2.8.0)(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9)))(ioredis@5.11.1) + vue: 3.5.44(typescript@6.0.3) + vue-bundle-renderer: 2.4.0 + vue-devtools-stub: 0.1.0 + optionalDependencies: + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@nuxt/schema' + - '@oxc-project/types' + - '@parcel/watcher' + - '@planetscale/database' + - '@rsbuild/core' + - '@rspack/core' + - '@unhead/cli' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools-kit' + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bun-types-no-globals + - confbox + - db0 + - dotenv + - drizzle-orm + - encoding + - esbuild + - giget + - idb-keyval + - ioredis + - jiti + - lightningcss + - magic-string + - mlly + - mysql2 + - oxc-parser + - react-native-b4a + - rolldown + - rollup + - sqlite3 + - srvx + - supports-color + - typescript + - unloader + - unplugin + - uploadthing + - vite + - webpack + - xml2js + '@nuxt/schema@4.5.2': dependencies: '@vue/shared': 3.5.43 @@ -8506,6 +8608,73 @@ snapshots: - vite - webpack + '@nuxt/vite-builder@4.6.2(0d3200de7489b01200fdaee340d678c7)': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))) + '@vitejs/plugin-vue': 6.0.10(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + '@vitejs/plugin-vue-jsx': 5.1.6(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + autoprefixer: 10.6.1(postcss@8.5.30) + clickable-path: 0.1.1 + consola: 3.4.2 + cssnano: 9.9.0(postcss@8.5.30) + defu: 6.1.7 + escape-string-regexp: 5.0.0 + exsolve: 1.1.3 + generic-names: 4.0.0 + get-port-please: 3.2.0 + js-tokens: 10.0.0 + knitwork: 1.3.0 + mocked-exports: 0.1.1 + my-bad: 0.2.11(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@biomejs/biome@2.5.14)(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@netlify/blobs@11.1.3)(@oxc-project/types@0.154.0)(@types/node@22.20.4)(@vercel/blob@2.8.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(aws4fetch@1.0.20)(confbox@0.3.1)(db0@0.3.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9)))(dotenv@17.4.2)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9))(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.7))(rollup@4.64.7)(srvx@1.0.5)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1) + pathe: 2.0.3 + pkg-types: 2.3.3 + postcss: 8.5.30 + rolldown-string: 0.3.1(rolldown@1.2.14) + seroval: 1.6.9 + std-env: 4.3.0 + ufo: 1.6.4 + unenv: 2.0.0-rc.24 + vite: 8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1) + vite-node: 6.0.0(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1) + vite-plugin-checker: 0.14.5(@biomejs/biome@2.5.14)(typescript@6.0.3)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue-tsc@3.3.11(typescript@6.0.3)) + vue: 3.5.44(typescript@6.0.3) + vue-bundle-renderer: 2.4.0 + optionalDependencies: + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + rolldown: 1.2.14 + rollup-plugin-visualizer: 7.1.1(rolldown@1.2.14)(rollup@4.64.7) + transitivePeerDependencies: + - '@biomejs/biome' + - '@nuxt/schema' + - '@types/node' + - '@vitejs/devtools' + - confbox + - dotenv + - esbuild + - eslint + - giget + - jiti + - less + - magic-string + - meow + - mlly + - optionator + - oxc-parser + - oxlint + - sass + - sass-embedded + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - unplugin + - vue-tsc + - yaml + '@nuxt/vite-builder@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@biomejs/biome@2.5.14)(@nuxt/schema@4.6.2)(@types/node@22.20.4)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(nuxt@4.6.2(8341e7b36084563b1d1df6456ea53827))(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.7))(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vue-tsc@3.3.11(typescript@6.0.3))(vue@3.5.44(typescript@6.0.3))(yaml@2.9.1)': dependencies: '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))) @@ -8600,6 +8769,33 @@ snapshots: - rolldown - unplugin + '@nuxt/vite-server@4.6.2(f7633775e65acf2e3ca13481065e583f)': + dependencies: + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))) + clickable-path: 0.1.1 + defu: 6.1.7 + exsolve: 1.1.3 + mocked-exports: 0.1.1 + my-bad: 0.2.11(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + nuxt: 4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@biomejs/biome@2.5.14)(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@netlify/blobs@11.1.3)(@oxc-project/types@0.154.0)(@types/node@22.20.4)(@vercel/blob@2.8.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(aws4fetch@1.0.20)(confbox@0.3.1)(db0@0.3.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9)))(dotenv@17.4.2)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9))(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.7))(rollup@4.64.7)(srvx@1.0.5)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1) + pathe: 2.0.3 + rou3: 0.9.2 + srvx: 1.0.5 + ufo: 1.6.4 + ultrahtml: 1.7.0 + vite: 8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1) + transitivePeerDependencies: + - '@nuxt/schema' + - confbox + - dotenv + - giget + - jiti + - magic-string + - mlly + - oxc-parser + - rolldown + - unplugin + '@one-ini/wasm@0.2.1': {} '@opentelemetry/api-logs@0.221.0': @@ -11566,6 +11762,159 @@ snapshots: - xml2js - yaml + nuxt@4.6.2(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@biomejs/biome@2.5.14)(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@netlify/blobs@11.1.3)(@oxc-project/types@0.154.0)(@types/node@22.20.4)(@vercel/blob@2.8.0)(@vue/compiler-sfc@3.5.44)(acorn@8.19.0)(aws4fetch@1.0.20)(confbox@0.3.1)(db0@0.3.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9)))(dotenv@17.4.2)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9))(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(lightningcss@1.33.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup-plugin-visualizer@7.1.1(rolldown@1.2.14)(rollup@4.64.7))(rollup@4.64.7)(srvx@1.0.5)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(yaml@2.9.1): + dependencies: + '@dxup/nuxt': 0.5.10(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + '@jridgewell/trace-mapping': 0.3.31 + '@nuxt/cli': 4.0.1(@nuxt/schema@4.6.2)(jiti@2.7.0)(rolldown@1.2.14)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + '@nuxt/devalue': 2.0.2 + '@nuxt/devtools': 3.4.3(@netlify/blobs@11.1.3)(@nuxt/schema@4.6.2)(@vercel/blob@2.8.0)(aws4fetch@1.0.20)(confbox@0.3.1)(db0@0.3.4(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(drizzle-orm@0.45.3(@electric-sql/pglite@0.5.8)(@libsql/client@0.18.0)(@opentelemetry/api@1.9.1)(postgres@3.4.9)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + '@nuxt/kit': 4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))) + '@nuxt/nitro-server': 4.6.2(c810ee51f9945db652f39682956d9e9c) + '@nuxt/schema': 4.6.2 + '@nuxt/telemetry': 2.9.1(@nuxt/kit@4.6.2(@nuxt/schema@4.6.2)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.4.3)(mlly@1.8.2)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)))) + '@nuxt/vite-builder': 4.6.2(0d3200de7489b01200fdaee340d678c7) + '@nuxt/vite-server': 4.6.2(f7633775e65acf2e3ca13481065e583f) + '@unhead/vue': 3.4.3(@oxc-project/types@0.154.0)(esbuild@0.28.2)(lightningcss@1.33.0)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + '@vue/shared': 3.5.44 + chokidar: 5.0.0 + clickable-path: 0.1.1 + compatx: 0.2.0 + consola: 3.4.2 + cookie-es: 3.1.1 + defu: 6.1.7 + destr: 2.0.5 + devalue: 6.0.3 + errx: 0.2.2 + escape-string-regexp: 5.0.0 + exsolve: 1.1.3 + fetchdts: 0.2.0 + fnv1a-64: 0.1.2 + hookable: 6.1.2 + ignore: 7.0.12 + impound: 1.2.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + iron-webcrypto: 2.0.0 + jiti: 2.7.0 + klona: 2.0.6 + knitwork: 1.3.0 + magic-string: 1.4.3 + my-bad: 0.2.11(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + nanotar: 0.3.0 + nostics: 1.3.0 + object-identity: 0.2.3 + ofetch: 1.5.1 + ohash: 2.0.12 + on-change: 6.0.2 + oxc-walker: 1.1.1(@oxc-project/types@0.154.0)(rolldown@1.2.14) + pathe: 2.0.3 + perfect-debounce: 2.1.0 + picomatch: 4.0.7 + pkg-types: 2.3.3 + rolldown: 1.2.14 + rolldown-string: 0.3.1(rolldown@1.2.14) + rou3: 0.9.2 + scule: 1.3.0 + std-env: 4.3.0 + tinyglobby: 0.2.17 + ufo: 1.6.4 + ultrahtml: 1.7.0 + uncrypto: 0.1.3 + unctx: 3.0.1(magic-string@1.4.3)(rolldown@1.2.14)(unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))) + undici: 8.12.2 + unhead: 3.4.3(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + unimport: 7.0.2(acorn@8.19.0)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) + unrouting: 0.2.3 + untyped: 2.0.0 + verkit: 0.5.0 + vue: 3.5.44(typescript@6.0.3) + vue-bundle-renderer: 2.4.0 + vue-component-type-helpers: 3.3.13 + vue-router: 5.4.0(@vue/compiler-sfc@3.5.44)(esbuild@0.28.2)(rolldown@1.2.14)(rollup@4.64.7)(vite@8.3.4(@types/node@22.20.4)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.44(typescript@6.0.3)) + optionalDependencies: + '@types/node': 22.20.4 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@babel/plugin-proposal-decorators' + - '@babel/plugin-syntax-jsx' + - '@babel/plugin-syntax-typescript' + - '@biomejs/biome' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@farmfe/core' + - '@libsql/client' + - '@netlify/blobs' + - '@nuxt/docs' + - '@oxc-project/types' + - '@pinia/colada' + - '@planetscale/database' + - '@rollup/plugin-babel' + - '@rsbuild/core' + - '@rspack/core' + - '@unhead/cli' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - '@vitejs/devtools-kit' + - '@vue/compiler-sfc' + - acorn + - aws4fetch + - bare-abort-controller + - bare-buffer + - better-sqlite3 + - bufferutil + - bun-types-no-globals + - confbox + - db0 + - dotenv + - drizzle-orm + - encoding + - esbuild + - eslint + - giget + - idb-keyval + - ioredis + - less + - lightningcss + - meow + - mlly + - mysql2 + - optionator + - oxc-parser + - oxlint + - pinia + - react-native-b4a + - rollup + - rollup-plugin-visualizer + - sass + - sass-embedded + - serve + - sqlite3 + - srvx + - stylelint + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - unloader + - uploadthing + - utf-8-validate + - vite + - vue-tsc + - webpack + - xml2js + - yaml + nypm@0.6.10: dependencies: citty: 0.2.2