diff --git a/.craft.yml b/.craft.yml index 7e2ee3217533..355b545e02be 100644 --- a/.craft.yml +++ b/.craft.yml @@ -139,6 +139,9 @@ targets: - name: npm id: '@sentry/react-router' includeNames: /^sentry-react-router-\d.*\.tgz$/ + - name: npm + id: '@sentry/nitro' + includeNames: /^sentry-nitro-\d.*\.tgz$/ ## 7. Other Packages ## 7.1 @@ -256,3 +259,9 @@ targets: packageUrl: 'https://www.npmjs.com/package/@sentry/elysia' mainDocsUrl: 'https://docs.sentry.io/platforms/javascript/guides/elysia/' onlyIfPresent: /^sentry-elysia-\d.*\.tgz$/ + 'npm:@sentry/nitro': + name: 'Sentry Nitro SDK' + sdkName: 'sentry.javascript.nitro' + packageUrl: 'https://www.npmjs.com/package/@sentry/nitro' + mainDocsUrl: 'https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro' + onlyIfPresent: /^sentry-nitro-\d.*\.tgz$/ diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 47edbfeed264..499244434f82 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -52,6 +52,7 @@ body: - '@sentry/google-cloud-serverless' - '@sentry/nestjs' - '@sentry/nextjs' + - '@sentry/nitro' - '@sentry/nuxt' - '@sentry/react' - '@sentry/react-router' diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index e28d6988d9a1..5b16ebe112e2 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -120,6 +120,9 @@ jobs: - test-application: 'nestjs-microservices' build-command: 'test:build-latest' label: 'nestjs-microservices (latest)' + - test-application: 'nitro-3' + build-command: 'test:build-canary' + label: 'nitro-3 (canary)' steps: - name: Check out current commit diff --git a/.github/workflows/issue-package-label.yml b/.github/workflows/issue-package-label.yml index 323b17219b1a..fb77747f336f 100644 --- a/.github/workflows/issue-package-label.yml +++ b/.github/workflows/issue-package-label.yml @@ -65,6 +65,9 @@ jobs: "@sentry.nestjs": { "label": "Nest.js" }, + "@sentry.nitro": { + "label": "Nitro" + }, "@sentry.nextjs": { "label": "Next.js" }, diff --git a/README.md b/README.md index 841a6380b5e2..71ae65bbe406 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ package. Please refer to the README and instructions of those SDKs for more deta - [`@sentry/gatsby`](https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby): SDK for Gatsby - [`@sentry/nestjs`](https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs): SDK for NestJS - [`@sentry/nextjs`](https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs): SDK for Next.js +- [`@sentry/nitro`](https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro): SDK for Nitro - [`@sentry/remix`](https://github.com/getsentry/sentry-javascript/tree/master/packages/remix): SDK for Remix - [`@sentry/tanstackstart-react`](https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart-react): SDK for TanStack Start React - [`@sentry/aws-serverless`](https://github.com/getsentry/sentry-javascript/tree/master/packages/aws-serverless): SDK diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/.npmrc b/dev-packages/e2e-tests/test-applications/nitro-3/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/index.html b/dev-packages/e2e-tests/test-applications/nitro-3/index.html new file mode 100644 index 000000000000..4e9315ac391e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/index.html @@ -0,0 +1,11 @@ + + + + + Nitro E2E Test + + +

Nitro E2E Test App

+ + + diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs new file mode 100644 index 000000000000..53b80d309a5b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/nitro'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/package.json b/dev-packages/e2e-tests/test-applications/nitro-3/package.json new file mode 100644 index 000000000000..ab92769115d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/package.json @@ -0,0 +1,29 @@ +{ + "name": "nitro-3", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "start": "PORT=3030 NODE_OPTIONS='--import ./instrument.mjs' node .output/server/index.mjs", + "clean": "npx rimraf node_modules pnpm-lock.yaml .output", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/browser": "latest || *", + "@sentry/nitro": "latest || *" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/core": "latest || *", + "nitro": "^3.0.260415-beta", + "rolldown": "latest", + "vite": "latest" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/index.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/index.ts new file mode 100644 index 000000000000..a9fca21eecfb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/index.ts @@ -0,0 +1,5 @@ +import { defineHandler } from 'nitro/h3'; + +export default defineHandler(() => { + return { status: 'ok' }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error.ts new file mode 100644 index 000000000000..170efb1977ab --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error.ts @@ -0,0 +1,5 @@ +import { defineHandler } from 'nitro/h3'; + +export default defineHandler(() => { + throw new Error('This is a test error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-isolation/[id].ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-isolation/[id].ts new file mode 100644 index 000000000000..a8c2cd7a99f5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-isolation/[id].ts @@ -0,0 +1,10 @@ +import { getDefaultIsolationScope, setTag } from '@sentry/core'; +import { defineHandler } from 'nitro/h3'; + +export default defineHandler(() => { + setTag('my-isolated-tag', true); + // Check if the tag leaked into the default (global) isolation scope + setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); + + throw new Error('Isolation test error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-nesting.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-nesting.ts new file mode 100644 index 000000000000..687c6f3f1e9a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-nesting.ts @@ -0,0 +1,16 @@ +import { startSpan } from '@sentry/nitro'; +import { defineHandler } from 'nitro/h3'; + +export default defineHandler(() => { + startSpan({ name: 'db.select', op: 'db' }, () => { + // simulate a select query + }); + + startSpan({ name: 'db.insert', op: 'db' }, () => { + startSpan({ name: 'db.serialize', op: 'serialize' }, () => { + // simulate serializing data before insert + }); + }); + + return { status: 'ok', nesting: true }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-param/[id].ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-param/[id].ts new file mode 100644 index 000000000000..ef67525b36ba --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-param/[id].ts @@ -0,0 +1,6 @@ +import { defineHandler } from 'nitro/h3'; + +export default defineHandler(event => { + const id = event.req.url; + return { id }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-transaction.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-transaction.ts new file mode 100644 index 000000000000..b488b371310d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-transaction.ts @@ -0,0 +1,5 @@ +import { defineHandler } from 'nitro/h3'; + +export default defineHandler(() => { + return { status: 'ok', transaction: true }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/middleware/test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/middleware/test.ts new file mode 100644 index 000000000000..92d8f80c3756 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/middleware/test.ts @@ -0,0 +1,10 @@ +import { defineHandler, getQuery, setResponseHeader } from 'nitro/h3'; + +export default defineHandler(event => { + setResponseHeader(event, 'x-sentry-test-middleware', 'executed'); + + const query = getQuery(event); + if (query['middleware-error'] === '1') { + throw new Error('Middleware error'); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts b/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts new file mode 100644 index 000000000000..d27d0ba1763a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +// Let's us test trace propagation +Sentry.init({ + environment: 'qa', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tunnel: 'http://localhost:3031/', // proxy server + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nitro-3/start-event-proxy.mjs new file mode 100644 index 000000000000..928e68908661 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nitro-3', +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts new file mode 100644 index 000000000000..8e419ac9ba62 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends an error event to Sentry', async ({ request }) => { + const errorEventPromise = waitForError('nitro-3', event => { + return !event.type && !!event.exception?.values?.some(v => v.value === 'This is a test error'); + }); + + await request.get('/api/test-error'); + + const errorEvent = await errorEventPromise; + + // Nitro wraps thrown errors in an HTTPError with .cause, producing a chained exception + expect(errorEvent.exception?.values).toHaveLength(2); + + // The innermost exception (values[0]) is the original thrown error + expect(errorEvent.exception?.values?.[0]?.type).toBe('Error'); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is a test error'); + expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual( + expect.objectContaining({ + handled: false, + type: 'auto.function.nitro.captureErrorHook', + }), + ); + + // The outermost exception (values[1]) is the HTTPError wrapper + expect(errorEvent.exception?.values?.[1]?.type).toBe('HTTPError'); + expect(errorEvent.exception?.values?.[1]?.value).toBe('This is a test error'); +}); + +test('Does not send 404 errors to Sentry', async ({ request }) => { + let errorReceived = false; + + void waitForError('nitro-3', event => { + if (!event.type) { + errorReceived = true; + return true; + } + return false; + }); + + await request.get('/api/non-existent-route'); + + expect(errorReceived).toBe(false); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts new file mode 100644 index 000000000000..7234fa0948ca --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Isolation scope prevents tag leaking between requests', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', event => { + return event?.transaction === 'GET /api/test-isolation/:id'; + }); + + const errorPromise = waitForError('nitro-3', event => { + return !event.type && !!event.exception?.values?.some(v => v.value === 'Isolation test error'); + }); + + await request.get('/api/test-isolation/1').catch(() => { + // noop - route throws + }); + + const transactionEvent = await transactionEventPromise; + const error = await errorPromise; + + // Assert that isolation scope works properly + expect(error.tags?.['my-isolated-tag']).toBe(true); + expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); + expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts new file mode 100644 index 000000000000..eec281d28f98 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/middleware.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Creates middleware spans for requests', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', event => { + return event?.transaction === 'GET /api/test-transaction'; + }); + + const response = await request.get('/api/test-transaction'); + + expect(response.headers()['x-sentry-test-middleware']).toBe('executed'); + + const transactionEvent = await transactionEventPromise; + + // h3 middleware spans have origin auto.http.nitro.h3 and op middleware.nitro + const h3MiddlewareSpans = transactionEvent.spans?.filter( + span => span.origin === 'auto.http.nitro.h3' && span.op === 'middleware.nitro', + ); + expect(h3MiddlewareSpans?.length).toBeGreaterThanOrEqual(1); +}); + +test('Captures errors thrown in middleware with error status on span', async ({ request }) => { + const errorEventPromise = waitForError('nitro-3', event => { + return !event.type && !!event.exception?.values?.some(v => v.value === 'Middleware error'); + }); + + const transactionEventPromise = waitForTransaction('nitro-3', event => { + return event?.transaction === 'GET /api/test-transaction' && event?.contexts?.trace?.status === 'internal_error'; + }); + + await request.get('/api/test-transaction?middleware-error=1'); + + const errorEvent = await errorEventPromise; + expect(errorEvent.exception?.values?.some(v => v.value === 'Middleware error')).toBe(true); + + const transactionEvent = await transactionEventPromise; + + // The transaction span should have error status + expect(transactionEvent.contexts?.trace?.status).toBe('internal_error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts new file mode 100644 index 000000000000..090f8af36fb2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts @@ -0,0 +1,146 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Span nesting: all spans share the same trace_id', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', event => { + return event?.transaction === 'GET /api/test-nesting'; + }); + + await request.get('/api/test-nesting'); + + const event = await transactionEventPromise; + const traceId = event.contexts?.trace?.trace_id; + + expect(traceId).toMatch(/[a-f0-9]{32}/); + + // Every child span must belong to the same trace + for (const span of event.spans ?? []) { + expect(span.trace_id).toBe(traceId); + } +}); + +test('Span nesting: h3 middleware spans are children of the srvx request span', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', event => { + return event?.transaction === 'GET /api/test-nesting'; + }); + + await request.get('/api/test-nesting'); + + const event = await transactionEventPromise; + + // Find the srvx request span + const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server'); + expect(srvxSpan).toBeDefined(); + + // All h3 middleware spans should be children of the srvx span + const h3Spans = event.spans?.filter(span => span.origin === 'auto.http.nitro.h3'); + expect(h3Spans?.length).toBeGreaterThanOrEqual(1); + + for (const span of h3Spans ?? []) { + expect(span.parent_span_id).toBe(srvxSpan!.span_id); + } +}); + +test('Span nesting: manual startSpan calls inside route handler are children of the srvx request span', async ({ + request, +}) => { + const transactionEventPromise = waitForTransaction('nitro-3', event => { + return event?.transaction === 'GET /api/test-nesting'; + }); + + await request.get('/api/test-nesting'); + + const event = await transactionEventPromise; + + // Find the srvx request span — this is the parent of all h3 and manual spans + const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server'); + expect(srvxSpan).toBeDefined(); + const srvxSpanId = srvxSpan!.span_id; + + // Find the manually created db spans + const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select'); + const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert'); + expect(dbSelectSpan).toBeDefined(); + expect(dbInsertSpan).toBeDefined(); + + // FIXME: Once nitro's h3 tracing plugin emits a separate span for route handlers (type: "route"), + // the db spans should be children of the h3 route handler span, not the srvx span directly. + // Currently nitro bypasses h3's ~routes for file-based routing, so h3 only emits middleware spans. + // Both db spans should be children of the srvx request span + expect(dbSelectSpan!.parent_span_id).toBe(srvxSpanId); + expect(dbInsertSpan!.parent_span_id).toBe(srvxSpanId); + + // Both db spans should be siblings (same parent) + expect(dbSelectSpan!.parent_span_id).toBe(dbInsertSpan!.parent_span_id); + + // The serialize span should be nested inside the db.insert span + const serializeSpan = event.spans?.find(span => span.op === 'serialize' && span.description === 'db.serialize'); + expect(serializeSpan).toBeDefined(); + expect(serializeSpan!.parent_span_id).toBe(dbInsertSpan!.span_id); +}); + +// FIXME: Nitro's file-based routing bypasses h3's ~routes, so h3's tracing plugin never wraps +// route handlers with type: "route". Once this is fixed upstream or we add our own wrapping, +// uncomment these tests to verify the h3 route handler span exists and is the parent of manual spans. +// +// test('Span nesting: h3 route handler span is a child of the srvx request span', async ({ request }) => { +// const transactionEventPromise = waitForTransaction('nitro-3', event => { +// return event?.transaction === 'GET /api/test-nesting'; +// }); +// +// await request.get('/api/test-nesting'); +// +// const event = await transactionEventPromise; +// +// const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server'); +// expect(srvxSpan).toBeDefined(); +// +// const h3HandlerSpan = event.spans?.find( +// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server', +// ); +// expect(h3HandlerSpan).toBeDefined(); +// expect(h3HandlerSpan!.parent_span_id).toBe(srvxSpan!.span_id); +// }); +// +// test('Span nesting: manual startSpan calls are children of the h3 route handler span', async ({ request }) => { +// const transactionEventPromise = waitForTransaction('nitro-3', event => { +// return event?.transaction === 'GET /api/test-nesting'; +// }); +// +// await request.get('/api/test-nesting'); +// +// const event = await transactionEventPromise; +// +// const h3HandlerSpan = event.spans?.find( +// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server', +// ); +// expect(h3HandlerSpan).toBeDefined(); +// +// const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select'); +// const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert'); +// expect(dbSelectSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id); +// expect(dbInsertSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id); +// }); + +test('Span nesting: middleware spans start before manual spans in the span tree', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', event => { + return event?.transaction === 'GET /api/test-nesting'; + }); + + await request.get('/api/test-nesting'); + + const event = await transactionEventPromise; + + // Middleware spans should start before the manual db spans + const middlewareSpans = event.spans?.filter(span => span.op === 'middleware.nitro') ?? []; + const dbSpans = event.spans?.filter(span => span.op === 'db') ?? []; + + expect(middlewareSpans.length).toBeGreaterThanOrEqual(1); + expect(dbSpans.length).toBeGreaterThanOrEqual(1); + + const earliestMiddlewareStart = Math.min(...middlewareSpans.map(s => s.start_timestamp)); + const earliestDbStart = Math.min(...dbSpans.map(s => s.start_timestamp)); + + // Middleware should start before the db spans + expect(earliestMiddlewareStart).toBeLessThanOrEqual(earliestDbStart); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts new file mode 100644 index 000000000000..705521ad759d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/trace-propagation.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Propagates server trace to client pageload via Server-Timing headers', async ({ page }) => { + const clientTxnPromise = waitForTransaction('nitro-3', event => { + return event?.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + const clientTxn = await clientTxnPromise; + + expect(clientTxn.contexts?.trace?.trace_id).toBeDefined(); + expect(clientTxn.contexts?.trace?.trace_id).toMatch(/[a-f0-9]{32}/); + expect(clientTxn.contexts?.trace?.op).toBe('pageload'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts new file mode 100644 index 000000000000..48de9c4349df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/transactions.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a transaction event for a successful route', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => { + return transactionEvent?.transaction === 'GET /api/test-transaction'; + }); + + await request.get('/api/test-transaction'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /api/test-transaction', + type: 'transaction', + }), + ); + + // srvx.request creates a span for the request + const srvxSpans = transactionEvent.spans?.filter(span => span.origin === 'auto.http.nitro.srvx'); + expect(srvxSpans?.length).toBeGreaterThanOrEqual(1); + + // h3 creates a child span for the route handler + const h3Spans = transactionEvent.spans?.filter(span => span.origin === 'auto.http.nitro.h3'); + expect(h3Spans?.length).toBeGreaterThanOrEqual(1); +}); + +test('Sets correct HTTP status code on transaction', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => { + return transactionEvent?.transaction === 'GET /api/test-transaction'; + }); + + await request.get('/api/test-transaction'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace?.data).toEqual( + expect.objectContaining({ + 'http.response.status_code': 200, + }), + ); + + expect(transactionEvent.contexts?.trace?.status).toBe('ok'); +}); + +test('Uses parameterized route for transaction name', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nitro-3', transactionEvent => { + return transactionEvent?.transaction === 'GET /api/test-param/:id'; + }); + + await request.get('/api/test-param/123'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /api/test-param/:id', + transaction_info: expect.objectContaining({ source: 'route' }), + type: 'transaction', + }), + ); + + expect(transactionEvent.contexts?.trace?.data).toEqual( + expect.objectContaining({ + 'http.route': '/api/test-param/:id', + }), + ); +}); + +test('Sets Server-Timing response headers for trace propagation', async ({ request }) => { + const response = await request.get('/api/test-transaction'); + const headers = response.headers(); + + expect(headers['server-timing']).toBeDefined(); + expect(headers['server-timing']).toContain('sentry-trace;desc="'); + expect(headers['server-timing']).toContain('baggage;desc="'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tsconfig.json b/dev-packages/e2e-tests/test-applications/nitro-3/tsconfig.json new file mode 100644 index 000000000000..b9a951fbebb1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "paths": { + "~/*": ["./*"] + } + }, + "include": ["src/**/*.ts", "routes/**/*.ts", "vite.config.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/vite.config.ts b/dev-packages/e2e-tests/test-applications/nitro-3/vite.config.ts new file mode 100644 index 000000000000..d488f8298777 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nitro-3/vite.config.ts @@ -0,0 +1,15 @@ +import { withSentryConfig } from '@sentry/nitro'; +import { nitro } from 'nitro/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + nitro( + // FIXME: Nitro plugin has a type issue + // @ts-expect-error + withSentryConfig({ + serverDir: './server', + }), + ), + ], +}); diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml new file mode 100644 index 000000000000..8878490df729 --- /dev/null +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -0,0 +1,294 @@ +# Taken from https://github.com/babel/babel/blob/624c78d99e8f42b2543b8943ab1b62bd71cf12d8/scripts/integration-tests/verdaccio-config.yml + +# +# This is the default config file. It allows all users to do anything, +# so don't use it on production systems. +# +# Look here for more config file examples: +# https://github.com/verdaccio/verdaccio/tree/master/conf +# + +# Repo-local storage (relative to this file). Absolute /verdaccio/... matches Docker-only templates and is not writable on typical dev machines. +storage: ./storage/data + +# https://verdaccio.org/docs/configuration#authentication +auth: + htpasswd: + file: ./storage/htpasswd + +# https://verdaccio.org/docs/configuration#uplinks +# a list of other known repositories we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + +# Learn how to protect your packages +# https://verdaccio.org/docs/protect-your-dependencies/ +# https://verdaccio.org/docs/configuration#packages +packages: + # To not use a proxy (e.g. npm) but instead use verdaccio for package hosting we need to define rules here without the + # `proxy` field. Sadly we can't use a wildcard like "@sentry/*" because we have some dependencies (@sentry/cli, + # @sentry/webpack-plugin) that fall under that wildcard but don't live in this repository. If we were to use that + # wildcard, we would get a 404 when attempting to install them, since they weren't uploaded to verdaccio, and also + # don't have a proxy configuration. + + '@sentry/angular': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/astro': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/browser': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/bun': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/core': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/cloudflare': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/deno': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/effect': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/elysia': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/ember': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/gatsby': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/hono': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/nestjs': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/nextjs': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/node': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/node-core': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/node-native': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/opentelemetry': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/profiling-node': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/react': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/react-router': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/remix': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/aws-serverless': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/google-cloud-serverless': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/solid': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/solidstart': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/svelte': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/sveltekit': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/tanstackstart': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/tanstackstart-react': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/types': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/vercel-edge': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/vue': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/nuxt': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/wasm': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry/nitro': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@sentry-internal/*': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + + '@*/*': + # scoped packages + access: $all + publish: $all + unpublish: $all + proxy: npmjs + + '**': + # allow all users (including non-authenticated users) to read and + # publish all packages + # + # you can specify usernames/groupnames (depending on your auth plugin) + # and three keywords: "$all", "$anonymous", "$authenticated" + access: $all + + # allow all known users to publish/publish packages + # (anyone can register by default, remember?) + publish: $all + unpublish: $all + proxy: npmjs + +# https://verdaccio.org/docs/configuration#server +# You can specify HTTP/1.1 server keep alive timeout in seconds for incoming connections. +# A value of 0 makes the http server behave similarly to Node.js versions prior to 8.0.0, which did not have a keep-alive timeout. +# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough. +server: + keepAliveTimeout: 60 + +middlewares: + audit: + enabled: false + +# https://verdaccio.org/docs/logger +# log settings +log: { type: stdout, format: pretty, level: http } +#experiments: +# # support for npm token command +# token: false diff --git a/package.json b/package.json index 511970b04c2f..662e0fa94c95 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "packages/integration-shims", "packages/nestjs", "packages/nextjs", + "packages/nitro", "packages/node", "packages/node-core", "packages/node-native", diff --git a/packages/nitro/LICENSE b/packages/nitro/LICENSE new file mode 100644 index 000000000000..0ecae617386e --- /dev/null +++ b/packages/nitro/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/nitro/README.md b/packages/nitro/README.md new file mode 100644 index 000000000000..367e70b456e4 --- /dev/null +++ b/packages/nitro/README.md @@ -0,0 +1,128 @@ +

+ + Sentry + +

+ +> NOTICE: This package is in beta state and may be subject to breaking changes. + +# Official Sentry SDK for Nitro + +[![npm version](https://img.shields.io/npm/v/@sentry/nitro.svg)](https://www.npmjs.com/package/@sentry/nitro) +[![npm dm](https://img.shields.io/npm/dm/@sentry/nitro.svg)](https://www.npmjs.com/package/@sentry/nitro) +[![npm dt](https://img.shields.io/npm/dt/@sentry/nitro.svg)](https://www.npmjs.com/package/@sentry/nitro) + +## Links + +- [Official Nitro SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nitro/) + +## Compatibility + +The minimum supported version of Nitro is `3.0.0-alpha.1`. + +## General + +This package is a wrapper around `@sentry/node` with added instrumentation for Nitro's features like: + +- HTTP handlers and error capturing. +- [Middleware instrumentation](https://nitro.build/guide/routing#middleware). + + + +## Manual Setup + +### 1. Prerequisites & Installation + +1. Install the Sentry Nitro SDK: + + ```bash + # Using npm + npm install @sentry/nitro + + # Using yarn + yarn add @sentry/nitro + + # Using pnpm + pnpm add @sentry/nitro + ``` + +### 2. Build-Time Nitro Config Setup + +1. Import `withSentryConfig` from `@sentry/nitro` and call it with your Nitro config. + +#### In `nitro.config.ts` + +If you are using a dedicated `nitro.config.ts` file, you can import `withSentryConfig` from `@sentry/nitro` and call it with your Nitro config. + +```javascript +import { defineNitroConfig } from 'nitro/config'; +import { withSentryConfig } from '@sentry/nitro'; + +const config = defineNitroConfig({ + // ... +}); + +export default withSentryConfig(config, { + // Sentry Build Options +}); +``` + +#### In `vite.config.ts` + +If you are using Nitro as a Vite plugin, you can import `withSentryConfig` from `@sentry/nitro` and call it with your Nitro config. + +```ts +import { defineConfig } from 'vite'; +import { nitro } from 'nitro/vite'; +import { withSentryConfig } from '@sentry/nitro'; + +export default defineConfig({ + plugins: [nitro()], + nitro: withSentryConfig( + { + // Nitro options + }, + { + // Sentry Build Options + }, + ), +}); +``` + +### 3. Sentry Config Setup + +Create an `instrument.mjs` file in your project root to initialize the Sentry SDK: + +```javascript +import * as Sentry from '@sentry/nitro'; + +Sentry.init({ + dsn: '__YOUR_DSN__', + tracesSampleRate: 1.0, +}); +``` + +Then use `--import` in `NODE_OPTIONS` to load the instrumentation before your app code: + +```bash +NODE_OPTIONS='--import ./instrument.mjs' npx nitro dev +``` + +This works with any Nitro command (`nitro dev`, `nitro preview`, or a production start script). + +## Uploading Source Maps + +The `withSentryConfig` function automatically configures source map uploading when the `authToken`, `org`, and `project` +options are provided: + +```javascript +export default withSentryConfig(config, { + org: 'your-sentry-org', + project: 'your-sentry-project', + authToken: process.env.SENTRY_AUTH_TOKEN, +}); +``` + +## Troubleshoot + +If you encounter any issues with error tracking or integrations, refer to the official [Sentry Nitro SDK documentation](https://docs.sentry.io/platforms/javascript/guides/nitro/). If the documentation does not provide the necessary information, consider opening an issue on GitHub. diff --git a/packages/nitro/package.json b/packages/nitro/package.json new file mode 100644 index 000000000000..54dab47a7157 --- /dev/null +++ b/packages/nitro/package.json @@ -0,0 +1,73 @@ +{ + "name": "@sentry/nitro", + "version": "10.49.0", + "description": "Official Sentry SDK for Nitro", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=18.19.1" + }, + "files": [ + "/build" + ], + "main": "build/esm/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "typesVersions": { + "*": { + "plugins": [ + "build/types/plugins.d.ts" + ] + } + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./build/types/index.d.ts", + "node": { + "import": "./build/esm/index.js" + } + } + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "nitro": ">=3.0.0-0 <4.0.0 || 3.0.260311-beta || 3.0.260415-beta" + }, + "dependencies": { + "@sentry/bundler-plugin-core": "^5.2.0", + "@sentry/core": "10.49.0", + "@sentry/node": "10.49.0", + "@sentry/opentelemetry": "10.49.0" + }, + "devDependencies": { + "nitro": "^3.0.260415-beta", + "h3": "^2.0.1-rc.13" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "run-s build:types:core", + "build:types:core": "tsc -p tsconfig.types.json", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "npm pack", + "clean": "rimraf build coverage sentry-nitro-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/esm/*.js --module", + "test": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/nitro/rollup.npm.config.mjs b/packages/nitro/rollup.npm.config.mjs new file mode 100644 index 000000000000..3be46cb5f15b --- /dev/null +++ b/packages/nitro/rollup.npm.config.mjs @@ -0,0 +1,12 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default [ + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.ts', 'src/runtime/plugins/server.ts'], + packageSpecificConfig: { + external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/, '@sentry/bundler-plugin-core'], + }, + }), + ), +]; diff --git a/packages/nitro/src/common/debug-build.ts b/packages/nitro/src/common/debug-build.ts new file mode 100644 index 000000000000..60aa50940582 --- /dev/null +++ b/packages/nitro/src/common/debug-build.ts @@ -0,0 +1,8 @@ +declare const __DEBUG_BUILD__: boolean; + +/** + * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code. + * + * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking. + */ +export const DEBUG_BUILD = __DEBUG_BUILD__; diff --git a/packages/nitro/src/config.ts b/packages/nitro/src/config.ts new file mode 100644 index 000000000000..cdc0f2b00dfb --- /dev/null +++ b/packages/nitro/src/config.ts @@ -0,0 +1,33 @@ +import type { BuildTimeOptionsBase } from '@sentry/core'; +import type { NitroConfig } from 'nitro/types'; +import { createNitroModule } from './module'; +import { configureSourcemapSettings } from './sourceMaps'; + +export type SentryNitroOptions = BuildTimeOptionsBase; + +/** + * Modifies the passed in Nitro configuration with automatic build-time instrumentation. + */ +export function withSentryConfig(config: NitroConfig, sentryOptions?: SentryNitroOptions): NitroConfig { + return setupSentryNitroModule(config, sentryOptions); +} + +/** + * Sets up the Sentry Nitro module, useful for meta framework integrations. + */ +export function setupSentryNitroModule( + config: NitroConfig, + moduleOptions?: SentryNitroOptions, + _serverConfigFile?: string, +): NitroConfig { + if (!config.tracingChannel) { + config.tracingChannel = true; + } + + const { sentryEnabledSourcemaps } = configureSourcemapSettings(config, moduleOptions); + + config.modules = config.modules || []; + config.modules.push(createNitroModule(moduleOptions, sentryEnabledSourcemaps)); + + return config; +} diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts new file mode 100644 index 000000000000..51f10f7ba5b5 --- /dev/null +++ b/packages/nitro/src/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable import/export */ +export * from './config'; +export * from '@sentry/node'; +export { init } from './sdk'; diff --git a/packages/nitro/src/instruments/instrumentServer.ts b/packages/nitro/src/instruments/instrumentServer.ts new file mode 100644 index 000000000000..ec891055558b --- /dev/null +++ b/packages/nitro/src/instruments/instrumentServer.ts @@ -0,0 +1,12 @@ +import type { Nitro } from 'nitro/types'; +import { addPlugin } from '../utils/plugin'; +import { createResolver } from '../utils/resolver'; + +/** + * Sets up the Nitro server instrumentation plugin + * @param nitro - The Nitro instance. + */ +export function instrumentServer(nitro: Nitro): void { + const moduleResolver = createResolver(import.meta.url); + addPlugin(nitro, moduleResolver.resolve('../runtime/plugins/server')); +} diff --git a/packages/nitro/src/module.ts b/packages/nitro/src/module.ts new file mode 100644 index 000000000000..1a4e5b0478d1 --- /dev/null +++ b/packages/nitro/src/module.ts @@ -0,0 +1,17 @@ +import type { NitroModule } from 'nitro/types'; +import type { SentryNitroOptions } from './config'; +import { instrumentServer } from './instruments/instrumentServer'; +import { setupSourceMaps } from './sourceMaps'; + +/** + * Creates a Nitro module to setup the Sentry SDK. + */ +export function createNitroModule(sentryOptions?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): NitroModule { + return { + name: 'sentry', + setup: nitro => { + instrumentServer(nitro); + setupSourceMaps(nitro, sentryOptions, sentryEnabledSourcemaps); + }, + }; +} diff --git a/packages/nitro/src/runtime/README.md b/packages/nitro/src/runtime/README.md new file mode 100644 index 000000000000..43c190e6d015 --- /dev/null +++ b/packages/nitro/src/runtime/README.md @@ -0,0 +1,5 @@ +# Nitro Runtime + +This directory contains the runtime code for Nitro, this includes plugins or any runtime code they may use. + +Do not mix runtime code with other code, this directory will be packaged with the SDK and shipped as-is. diff --git a/packages/nitro/src/runtime/hooks/captureErrorHook.ts b/packages/nitro/src/runtime/hooks/captureErrorHook.ts new file mode 100644 index 000000000000..38ab4fd699fa --- /dev/null +++ b/packages/nitro/src/runtime/hooks/captureErrorHook.ts @@ -0,0 +1,84 @@ +import { + captureException, + flushIfServerless, + getActiveSpan, + getClient, + getCurrentScope, + getRootSpan, + parseUrl, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; +import { HTTPError } from 'h3'; +import type { CapturedErrorContext } from 'nitro/types'; + +/** + * Extracts the relevant context information from the error context (HTTPEvent in Nitro Error) + * and creates a structured context object. + */ +function extractErrorContext(errorContext: CapturedErrorContext | undefined): Record { + const ctx: Record = {}; + + if (!errorContext) { + return ctx; + } + + if (errorContext.event) { + ctx.method = errorContext.event.req.method; + ctx.path = parseUrl(errorContext.event.req.url).path; + } + + if (Array.isArray(errorContext.tags)) { + ctx.tags = errorContext.tags; + } + + return ctx; +} + +/** + * Hook that can be added in a Nitro plugin. It captures an error and sends it to Sentry. + */ +export async function captureErrorHook(error: Error, errorContext: CapturedErrorContext): Promise { + const sentryClient = getClient(); + const sentryClientOptions = sentryClient?.getOptions(); + + if ( + sentryClientOptions && + 'enableNitroErrorHandler' in sentryClientOptions && + sentryClientOptions.enableNitroErrorHandler === false + ) { + return; + } + + // Do not report HTTPErrors with 3xx or 4xx status codes + if (HTTPError.isError(error) && error.status >= 300 && error.status < 500) { + return; + } + + const method = errorContext.event?.req.method ?? ''; + let path: string | null = null; + + try { + if (errorContext.event?.req.url) { + path = new URL(errorContext.event.req.url).pathname; + } + } catch { + // If URL parsing fails, leave path as null + } + + if (path) { + getCurrentScope().setTransactionName(`${method} ${path}`); + const activeSpan = getActiveSpan(); + const activeRootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; + activeRootSpan?.updateName(`${method} ${path}`); + activeRootSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + } + + const structuredContext = extractErrorContext(errorContext); + + captureException(error, { + captureContext: { contexts: { nitro: structuredContext } }, + mechanism: { handled: false, type: 'auto.function.nitro.captureErrorHook' }, + }); + + await flushIfServerless(); +} diff --git a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts new file mode 100644 index 000000000000..bf70536b7800 --- /dev/null +++ b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts @@ -0,0 +1,280 @@ +import { + captureException, + getActiveSpan, + getClient, + getHttpSpanDetailsFromUrlObject, + getRootSpan, + GLOBAL_OBJ, + httpHeadersToSpanAttributes, + parseStringToURLObject, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + setHttpStatus, + type Span, + SPAN_STATUS_ERROR, + startSpanManual, + updateSpanName, +} from '@sentry/core'; +import { tracingChannel, type TracingChannelContextWithSpan } from '@sentry/opentelemetry/tracing-channel'; +import type { TracingRequestEvent as H3TracingRequestEvent } from 'h3/tracing'; +import type { RequestEvent as SrvxRequestEvent } from 'srvx/tracing'; +import { setServerTimingHeaders } from './setServerTimingHeaders'; + +/** + * Global object with the trace channels + */ +const globalWithTraceChannels = GLOBAL_OBJ as typeof GLOBAL_OBJ & { + __SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__: boolean; +}; + +/** + * Captures tracing events emitted by Nitro tracing channels. + */ +export function captureTracingEvents(): void { + if (globalWithTraceChannels.__SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__) { + return; + } + + setupH3TracingChannels(); + setupSrvxTracingChannels(); + globalWithTraceChannels.__SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__ = true; +} + +/** + * No-op function to satisfy the tracing channel subscribe callbacks + */ +const NOOP = (): void => {}; + +/** + * Extracts the HTTP status code from a tracing channel result. + * The result is the return value of the traced handler, which is a Response for srvx + * and may or may not be a Response for h3. + */ +function getResponseStatusCode(result: unknown): number | undefined { + if (result && typeof result === 'object' && 'status' in result && typeof result.status === 'number') { + return result.status; + } + return undefined; +} + +function onTraceEnd(data: TracingChannelContextWithSpan<{ result?: unknown }>): void { + const statusCode = getResponseStatusCode(data.result); + if (data._sentrySpan && statusCode !== undefined) { + setHttpStatus(data._sentrySpan, statusCode); + } + + data._sentrySpan?.end(); +} + +function onTraceError(data: TracingChannelContextWithSpan<{ error: unknown }>): void { + captureException(data.error, { mechanism: { type: 'auto.http.nitro.onTraceError', handled: false } }); + data._sentrySpan?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + data._sentrySpan?.end(); +} + +/** + * Extracts the parameterized route pattern from the h3 event context. + */ +function getParameterizedRoute(event: H3TracingRequestEvent['event']): string | undefined { + const matchedRoute = event.context?.matchedRoute; + if (!matchedRoute) { + return undefined; + } + + const routePath = matchedRoute.route; + + // Skip catch-all routes as they're not useful for transaction grouping + if (!routePath || routePath === '/**') { + return undefined; + } + + return routePath; +} + +function setupH3TracingChannels(): void { + const h3Channel = tracingChannel('h3.request', data => { + const parsedUrl = parseStringToURLObject(data.event.url.href); + const routePattern = getParameterizedRoute(data.event); + + const [spanName, urlAttributes] = getHttpSpanDetailsFromUrlObject( + parsedUrl, + 'server', + 'auto.http.nitro.h3', + { method: data.event.req.method }, + routePattern, + ); + + return startSpanManual( + { + name: spanName, + attributes: { + ...urlAttributes, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.h3', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: data?.type === 'middleware' ? 'middleware.nitro' : 'http.server', + }, + }, + span => { + setParameterizedRouteAttributes(span, data.event); + + return span; + }, + ); + }); + + h3Channel.subscribe({ + start: (data: H3TracingRequestEvent) => { + setServerTimingHeaders(data.event); + }, + asyncStart: NOOP, + end: NOOP, + asyncEnd: (data: TracingChannelContextWithSpan) => { + onTraceEnd(data); + + if (!data._sentrySpan) { + return; + } + + // Update the root span (srvx transaction) with the parameterized route name. + // The srvx span is created before h3 resolves the route, so it initially has the raw URL. + // Note: data.type is always 'middleware' in asyncEnd regardless of handler type, + // so we rely on getParameterizedRoute() to filter out catch-all routes instead. + const rootSpan = getRootSpan(data._sentrySpan); + if (rootSpan && rootSpan !== data._sentrySpan) { + const routePattern = getParameterizedRoute(data.event); + if (routePattern) { + const method = data.event.req.method || 'GET'; + updateSpanName(rootSpan, `${method} ${routePattern}`); + rootSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + 'http.route': routePattern, + }); + } + } + }, + error: onTraceError, + }); +} + +function setupSrvxTracingChannels(): void { + // Store the parent span per-request so middleware and fetch share the same parent. + // WeakMap ensures per-request isolation in concurrent environments and automatic cleanup. + const requestParentSpans = new WeakMap(); + + const fetchChannel = tracingChannel('srvx.request', data => { + const parsedUrl = data.request._url ? parseStringToURLObject(data.request._url.href) : undefined; + const [spanName, urlAttributes] = getHttpSpanDetailsFromUrlObject(parsedUrl, 'server', 'auto.http.nitro.srvx', { + method: data.request.method, + }); + + const sendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false; + const headerAttributes = httpHeadersToSpanAttributes( + Object.fromEntries(data.request.headers.entries()), + sendDefaultPii, + ); + + return startSpanManual( + { + name: spanName, + attributes: { + ...urlAttributes, + ...headerAttributes, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: data.middleware ? 'middleware.nitro' : 'http.server', + 'server.port': data.server.options.port, + }, + // Use the same parent span as middleware to make them siblings + parentSpan: requestParentSpans.get(data.request) || undefined, + }, + span => span, + ); + }); + + // Subscribe to events (span already created in bindStore) + fetchChannel.subscribe({ + start: () => {}, + asyncStart: () => {}, + end: () => {}, + asyncEnd: data => { + onTraceEnd(data); + + // Clean up parent span reference after the fetch handler completes. + requestParentSpans.delete(data.request); + }, + error: data => { + onTraceError(data); + // Clean up parent span reference on error too + requestParentSpans.delete(data.request); + }, + }); + + const middlewareChannel = tracingChannel('srvx.middleware', data => { + // For the first middleware, capture the current parent span per-request + if (data.middleware?.index === 0) { + const activeSpan = getActiveSpan(); + if (activeSpan) { + requestParentSpans.set(data.request, activeSpan); + } + } + + const parsedUrl = data.request._url ? parseStringToURLObject(data.request._url.href) : undefined; + const [, urlAttributes] = getHttpSpanDetailsFromUrlObject(parsedUrl, 'server', 'auto.http.nitro.srvx', { + method: data.request.method, + }); + + // Create span as a child of the original parent, not the previous middleware + return startSpanManual( + { + name: `${data.middleware?.handler.name ?? 'unknown'} - ${data.request.method} ${data.request._url?.pathname}`, + attributes: { + ...urlAttributes, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nitro', + }, + parentSpan: requestParentSpans.get(data.request) || undefined, + }, + span => span, + ); + }); + + // Subscribe to events (span already created in bindStore) + middlewareChannel.subscribe({ + start: () => {}, + asyncStart: () => {}, + end: () => {}, + asyncEnd: onTraceEnd, + error: onTraceError, + }); +} + +/** + * Sets the parameterized route attributes on the span. + */ +function setParameterizedRouteAttributes(span: Span, event: H3TracingRequestEvent['event']): void { + const rootSpan = getRootSpan(span); + if (!rootSpan) { + return; + } + + const matchedRoutePath = getParameterizedRoute(event); + if (!matchedRoutePath) { + return; + } + + rootSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + 'http.route': matchedRoutePath, + }); + + const params = event.context?.params; + + if (params && typeof params === 'object') { + Object.entries(params).forEach(([key, value]) => { + // Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey + rootSpan.setAttributes({ + [`url.path.parameter.${key}`]: String(value), + [`params.${key}`]: String(value), + }); + }); + } +} diff --git a/packages/nitro/src/runtime/hooks/setServerTimingHeaders.ts b/packages/nitro/src/runtime/hooks/setServerTimingHeaders.ts new file mode 100644 index 000000000000..4573f8171c19 --- /dev/null +++ b/packages/nitro/src/runtime/hooks/setServerTimingHeaders.ts @@ -0,0 +1,27 @@ +import { getTraceData } from '@sentry/core'; +import type { TracingRequestEvent as H3TracingRequestEvent } from 'h3/tracing'; + +/** + * Sets Server-Timing response headers for trace propagation to the client. + * The browser SDK reads these via the Performance API to connect pageload traces. + */ +export function setServerTimingHeaders(event: H3TracingRequestEvent['event']): void { + if (event.context._sentryServerTimingSet) { + return; + } + + const headers = event.res?.headers; + if (!headers) { + return; + } + + const traceData = getTraceData(); + if (traceData['sentry-trace']) { + headers.append('Server-Timing', `sentry-trace;desc="${traceData['sentry-trace']}"`); + } + if (traceData.baggage) { + headers.append('Server-Timing', `baggage;desc="${traceData.baggage}"`); + } + + event.context._sentryServerTimingSet = true; +} diff --git a/packages/nitro/src/runtime/plugins/server.ts b/packages/nitro/src/runtime/plugins/server.ts new file mode 100644 index 000000000000..2feee84bcc55 --- /dev/null +++ b/packages/nitro/src/runtime/plugins/server.ts @@ -0,0 +1,9 @@ +import { definePlugin } from 'nitro'; +import { captureErrorHook } from '../hooks/captureErrorHook'; +import { captureTracingEvents } from '../hooks/captureTracingEvents'; + +export default definePlugin(nitroApp => { + nitroApp.hooks.hook('error', captureErrorHook); + + captureTracingEvents(); +}); diff --git a/packages/nitro/src/sdk.ts b/packages/nitro/src/sdk.ts new file mode 100644 index 000000000000..e3b884512d52 --- /dev/null +++ b/packages/nitro/src/sdk.ts @@ -0,0 +1,29 @@ +import type { Integration } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; +import type { NodeClient, NodeOptions } from '@sentry/node'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit } from '@sentry/node'; + +/** + * Initializes the Nitro SDK + */ +export function init(options: NodeOptions | undefined = {}): NodeClient | undefined { + const opts: NodeOptions = { + defaultIntegrations: getDefaultIntegrations(options), + ...options, + }; + + applySdkMetadata(opts, 'nitro', ['nitro', 'node']); + + const client = nodeInit(opts); + + return client; +} + +/** + * Get the default integrations for the Nitro SDK. + * + * @returns The default integrations for the Nitro SDK. + */ +export function getDefaultIntegrations(options: NodeOptions): Integration[] | undefined { + return [...getDefaultNodeIntegrations(options)]; +} diff --git a/packages/nitro/src/sourceMaps.ts b/packages/nitro/src/sourceMaps.ts new file mode 100644 index 000000000000..9aa470a88d90 --- /dev/null +++ b/packages/nitro/src/sourceMaps.ts @@ -0,0 +1,192 @@ +import type { Options as BundlerPluginOptions } from '@sentry/bundler-plugin-core'; +import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core'; +import type { Nitro, NitroConfig } from 'nitro/types'; +import type { SentryNitroOptions } from './config'; + +/** + * Registers a `compiled` hook to upload source maps after the build completes. + */ +export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): void { + // The `compiled` hook fires on EVERY rebuild during `nitro dev` watch mode. + // nitro.options.dev is reliably set by the time module setup runs. + if (shouldSkipSourcemapUpload(nitro, options)) { + return; + } + + nitro.hooks.hook('compiled', async (_nitro: Nitro) => { + await handleSourceMapUpload(_nitro, options, sentryEnabledSourcemaps); + }); +} + +/** + * Determines if sourcemap uploads should be skipped. + */ +function shouldSkipSourcemapUpload(nitro: Nitro, options?: SentryNitroOptions): boolean { + return !!( + nitro.options.dev || + nitro.options.preset === 'nitro-prerender' || + nitro.options.sourcemap === false || + (nitro.options.sourcemap as unknown) === 'inline' || + options?.sourcemaps?.disable === true + ); +} + +/** + * Handles the actual source map upload after the build completes. + */ +async function handleSourceMapUpload( + nitro: Nitro, + options?: SentryNitroOptions, + sentryEnabledSourcemaps?: boolean, +): Promise { + const outputDir = nitro.options.output.serverDir; + const pluginOptions = getPluginOptions(options, sentryEnabledSourcemaps, outputDir); + + const sentryBuildPluginManager = createSentryBuildPluginManager(pluginOptions, { + buildTool: 'nitro', + loggerPrefix: '[@sentry/nitro]', + }); + + await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); + await sentryBuildPluginManager.createRelease(); + + await sentryBuildPluginManager.injectDebugIds([outputDir]); + + if (options?.sourcemaps?.disable !== 'disable-upload') { + await sentryBuildPluginManager.uploadSourcemaps([outputDir], { + // We don't prepare the artifacts because we injected debug IDs manually before + prepareArtifacts: false, + }); + await sentryBuildPluginManager.deleteArtifacts(); + } +} + +/** + * Normalizes the beginning of a path from e.g. ../../../ to ./ + */ +function normalizePath(path: string): string { + return path.replace(/^(\.\.\/)+/, './'); +} + +/** + * Removes a trailing slash from a path so glob patterns can be appended cleanly. + */ +function removeTrailingSlash(path: string): string { + return path.replace(/\/$/, ''); +} + +/** + * Builds the plugin options for `createSentryBuildPluginManager` from the Sentry Nitro options. + * + * Only exported for testing purposes. + */ +// oxlint-disable-next-line complexity +export function getPluginOptions( + options?: SentryNitroOptions, + sentryEnabledSourcemaps?: boolean, + outputDir?: string, +): BundlerPluginOptions { + const defaultFilesToDelete = + sentryEnabledSourcemaps && outputDir ? [`${removeTrailingSlash(outputDir)}/**/*.map`] : undefined; + + if (options?.debug && defaultFilesToDelete && options?.sourcemaps?.filesToDeleteAfterUpload === undefined) { + // eslint-disable-next-line no-console + console.log( + `[@sentry/nitro] Setting \`sourcemaps.filesToDeleteAfterUpload: ["${defaultFilesToDelete[0]}"]\` to delete generated source maps after they were uploaded to Sentry.`, + ); + } + + return { + org: options?.org ?? process.env.SENTRY_ORG, + project: options?.project ?? process.env.SENTRY_PROJECT, + authToken: options?.authToken ?? process.env.SENTRY_AUTH_TOKEN, + url: options?.sentryUrl ?? process.env.SENTRY_URL, + headers: options?.headers, + telemetry: options?.telemetry ?? true, + debug: options?.debug ?? false, + silent: options?.silent ?? false, + errorHandler: options?.errorHandler, + sourcemaps: { + disable: options?.sourcemaps?.disable, + assets: options?.sourcemaps?.assets, + ignore: options?.sourcemaps?.ignore, + filesToDeleteAfterUpload: options?.sourcemaps?.filesToDeleteAfterUpload ?? defaultFilesToDelete, + rewriteSources: options?.sourcemaps?.rewriteSources ?? ((source: string) => normalizePath(source)), + }, + release: options?.release, + bundleSizeOptimizations: options?.bundleSizeOptimizations, + _metaOptions: { + telemetry: { + metaFramework: 'nitro', + }, + }, + }; +} + +/* Source map configuration rules: + 1. User explicitly disabled source maps (sourcemap: false) + - Keep their setting, emit a warning that errors won't be unminified in Sentry + - We will not upload anything + 2. User enabled source map generation (true) + - Keep their setting (don't modify besides uploading) + 3. User did not set source maps (undefined) + - We enable source maps for Sentry + - Configure `filesToDeleteAfterUpload` to clean up .map files after upload +*/ +export function configureSourcemapSettings( + config: NitroConfig, + moduleOptions?: SentryNitroOptions, +): { sentryEnabledSourcemaps: boolean } { + const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true; + if (sourcemapUploadDisabled) { + return { sentryEnabledSourcemaps: false }; + } + + // Nitro types `sourcemap` as `boolean`, but it forwards the value to Vite which also accepts `'hidden'` and `'inline'`. + const userSourcemap = (config as { sourcemap?: boolean | 'hidden' | 'inline' }).sourcemap; + + if (userSourcemap === false) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nitro] You have explicitly disabled source maps (`sourcemap: false`). Sentry will not upload source maps, and errors will not be unminified. To let Sentry handle source maps, remove the `sourcemap` option from your Nitro config, or use `sourcemaps: { disable: true }` in your Sentry options to silence this warning.', + ); + return { sentryEnabledSourcemaps: false }; + } + + if (userSourcemap === 'inline') { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nitro] You have set `sourcemap: "inline"`. Inline source maps are embedded in the output bundle, so there are no `.map` files to upload. Sentry will not upload source maps. Set `sourcemap: "hidden"` (or leave it unset) to let Sentry upload source maps and un-minify errors.', + ); + return { sentryEnabledSourcemaps: false }; + } + + let sentryEnabledSourcemaps = false; + if (userSourcemap === true || userSourcemap === 'hidden') { + if (moduleOptions?.debug) { + // eslint-disable-next-line no-console + console.log( + `[@sentry/nitro] Source maps are already enabled (\`sourcemap: ${JSON.stringify(userSourcemap)}\`). Sentry will upload them for error unminification.`, + ); + } + } else { + // User did not explicitly set sourcemap, enable hidden source maps for Sentry. + // `'hidden'` emits .map files without adding a `//# sourceMappingURL=` comment to the output, avoiding public exposure. + (config as { sourcemap?: unknown }).sourcemap = 'hidden'; + sentryEnabledSourcemaps = true; + if (moduleOptions?.debug) { + // eslint-disable-next-line no-console + console.log( + '[@sentry/nitro] Enabled hidden source map generation for Sentry. Source map files will be deleted after upload.', + ); + } + } + + // Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`, + // `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`. + // This makes sourcemaps unusable for Sentry. + config.experimental = config.experimental || {}; + config.experimental.sourcemapMinify = false; + + return { sentryEnabledSourcemaps }; +} diff --git a/packages/nitro/src/utils/plugin.ts b/packages/nitro/src/utils/plugin.ts new file mode 100644 index 000000000000..443e3f430ba1 --- /dev/null +++ b/packages/nitro/src/utils/plugin.ts @@ -0,0 +1,9 @@ +import type { Nitro } from 'nitro/types'; + +/** + * Adds a Nitro plugin + */ +export function addPlugin(nitro: Nitro, plugin: string): void { + nitro.options.plugins = nitro.options.plugins || []; + nitro.options.plugins.push(plugin); +} diff --git a/packages/nitro/src/utils/resolver.ts b/packages/nitro/src/utils/resolver.ts new file mode 100644 index 000000000000..f0bde304d929 --- /dev/null +++ b/packages/nitro/src/utils/resolver.ts @@ -0,0 +1,25 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export interface Resolver { + resolve(...path: string[]): string; +} + +/** + * Creates a resolver for the given base path. + * @example + * ```ts + * const resolver = createResolver(import.meta.url); + * resolver.resolve('foo/bar.js'); + * ``` + */ +export function createResolver(base: string): Resolver { + let resolvedBase = base; + if (base.startsWith('file://')) { + resolvedBase = dirname(fileURLToPath(base)); + } + + return { + resolve: (...path) => resolve(resolvedBase, ...path), + }; +} diff --git a/packages/nitro/test/index.test.ts b/packages/nitro/test/index.test.ts new file mode 100644 index 000000000000..bc9db1cddfbb --- /dev/null +++ b/packages/nitro/test/index.test.ts @@ -0,0 +1,11 @@ +// Dummy test to satisfy the test runner +import { describe, expect, test } from 'vitest'; +import * as NitroServer from '../src'; + +describe('Nitro SDK', () => { + // This is a place holder test at best to satisfy the test runner + test('exports client and server SDKs', () => { + expect(NitroServer).toBeDefined(); + expect(NitroServer.init).toBeDefined(); + }); +}); diff --git a/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts b/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts new file mode 100644 index 000000000000..804ef569a619 --- /dev/null +++ b/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts @@ -0,0 +1,168 @@ +import * as SentryCore from '@sentry/core'; +import { HTTPError } from 'h3'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { captureErrorHook } from '../../../src/runtime/hooks/captureErrorHook'; + +vi.mock('@sentry/core', async importOriginal => { + const mod = await importOriginal(); + return { + ...(mod as any), + captureException: vi.fn(), + flushIfServerless: vi.fn(), + getClient: vi.fn(), + getCurrentScope: vi.fn(() => ({ + setTransactionName: vi.fn(), + })), + }; +}); + +describe('captureErrorHook', () => { + const mockErrorContext = { + event: { + req: { method: 'GET', url: 'http://localhost/test-path' }, + }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + (SentryCore.getClient as any).mockReturnValue({ + getOptions: () => ({}), + }); + (SentryCore.flushIfServerless as any).mockResolvedValue(undefined); + }); + + it('should capture regular errors', async () => { + const error = new Error('Test error'); + + await captureErrorHook(error, mockErrorContext); + + expect(SentryCore.captureException).toHaveBeenCalledWith( + error, + expect.objectContaining({ + mechanism: { handled: false, type: 'auto.function.nitro.captureErrorHook' }, + }), + ); + }); + + it('should include structured context with method and path', async () => { + const error = new Error('Test error'); + + await captureErrorHook(error, mockErrorContext); + + expect(SentryCore.captureException).toHaveBeenCalledWith( + error, + expect.objectContaining({ + captureContext: { + contexts: { + nitro: { method: 'GET', path: '/test-path' }, + }, + }, + }), + ); + }); + + it('should set transaction name from method and path', async () => { + const mockSetTransactionName = vi.fn(); + (SentryCore.getCurrentScope as any).mockReturnValue({ + setTransactionName: mockSetTransactionName, + }); + + const error = new Error('Test error'); + + await captureErrorHook(error, mockErrorContext); + + expect(mockSetTransactionName).toHaveBeenCalledWith('GET /test-path'); + }); + + it('should skip HTTPError with 4xx status codes', async () => { + const error = new HTTPError({ status: 404, message: 'Not found' }); + + await captureErrorHook(error, mockErrorContext); + + expect(SentryCore.captureException).not.toHaveBeenCalled(); + }); + + it('should skip HTTPError with 3xx status codes', async () => { + const error = new HTTPError({ status: 302, message: 'Redirect' }); + + await captureErrorHook(error, mockErrorContext); + + expect(SentryCore.captureException).not.toHaveBeenCalled(); + }); + + it('should capture HTTPError with 5xx status codes', async () => { + const error = new HTTPError({ status: 500, message: 'Server error' }); + + await captureErrorHook(error, mockErrorContext); + + expect(SentryCore.captureException).toHaveBeenCalledWith( + error, + expect.objectContaining({ + mechanism: { handled: false, type: 'auto.function.nitro.captureErrorHook' }, + }), + ); + }); + + it('should skip when enableNitroErrorHandler is false', async () => { + (SentryCore.getClient as any).mockReturnValue({ + getOptions: () => ({ enableNitroErrorHandler: false }), + }); + + const error = new Error('Test error'); + + await captureErrorHook(error, mockErrorContext); + + expect(SentryCore.captureException).not.toHaveBeenCalled(); + }); + + it('should call flushIfServerless after capturing', async () => { + const error = new Error('Test error'); + + await captureErrorHook(error, mockErrorContext); + + expect(SentryCore.flushIfServerless).toHaveBeenCalled(); + }); + + it('should handle missing event in error context', async () => { + const error = new Error('Test error'); + const contextWithoutEvent = { + event: undefined, + }; + + await captureErrorHook(error, contextWithoutEvent); + + expect(SentryCore.captureException).toHaveBeenCalledWith( + error, + expect.objectContaining({ + captureContext: { + contexts: { + nitro: {}, + }, + }, + }), + ); + }); + + it('should include tags in structured context when available', async () => { + const error = new Error('Test error'); + const contextWithTags = { + event: { + req: { method: 'POST', url: 'http://localhost/api/test' }, + } as any, + tags: ['tag1', 'tag2'], + }; + + await captureErrorHook(error, contextWithTags); + + expect(SentryCore.captureException).toHaveBeenCalledWith( + error, + expect.objectContaining({ + captureContext: { + contexts: { + nitro: { method: 'POST', path: '/api/test', tags: ['tag1', 'tag2'] }, + }, + }, + }), + ); + }); +}); diff --git a/packages/nitro/test/sourceMaps.test.ts b/packages/nitro/test/sourceMaps.test.ts new file mode 100644 index 000000000000..421994c4e2f1 --- /dev/null +++ b/packages/nitro/test/sourceMaps.test.ts @@ -0,0 +1,367 @@ +import type { NitroConfig } from 'nitro/types'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { SentryNitroOptions } from '../src/config'; +import { setupSentryNitroModule } from '../src/config'; +import { configureSourcemapSettings, getPluginOptions, setupSourceMaps } from '../src/sourceMaps'; + +vi.mock('../src/instruments/instrumentServer', () => ({ + instrumentServer: vi.fn(), +})); + +describe('getPluginOptions', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('returns default options when no options are provided', () => { + const options = getPluginOptions(undefined, true, '/project/.output/server'); + + expect(options).toEqual( + expect.objectContaining({ + telemetry: true, + debug: false, + silent: false, + sourcemaps: expect.objectContaining({ + filesToDeleteAfterUpload: ['/project/.output/server/**/*.map'], + rewriteSources: expect.any(Function), + }), + _metaOptions: expect.objectContaining({ + telemetry: expect.objectContaining({ + metaFramework: 'nitro', + }), + }), + }), + ); + expect(options.org).toBeUndefined(); + expect(options.project).toBeUndefined(); + expect(options.authToken).toBeUndefined(); + expect(options.url).toBeUndefined(); + }); + + it('does not default filesToDeleteAfterUpload when user enabled sourcemaps themselves', () => { + const options = getPluginOptions(undefined, false); + + expect(options.sourcemaps?.filesToDeleteAfterUpload).toBeUndefined(); + }); + + it('respects user-provided filesToDeleteAfterUpload even when Sentry enabled sourcemaps', () => { + const options = getPluginOptions( + { sourcemaps: { filesToDeleteAfterUpload: ['dist/**/*.map'] } }, + true, + '/project/.output/server', + ); + + expect(options.sourcemaps?.filesToDeleteAfterUpload).toEqual(['dist/**/*.map']); + }); + + it('logs the default filesToDeleteAfterUpload glob in debug mode', () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + getPluginOptions({ debug: true }, true, '/project/.output/server'); + + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('/project/.output/server/**/*.map')); + logSpy.mockRestore(); + }); + + it('does not log the default glob when user provides filesToDeleteAfterUpload', () => { + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + getPluginOptions( + { debug: true, sourcemaps: { filesToDeleteAfterUpload: ['dist/**/*.map'] } }, + true, + '/project/.output/server', + ); + + expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining('filesToDeleteAfterUpload')); + logSpy.mockRestore(); + }); + + it('uses environment variables as fallback', () => { + process.env.SENTRY_ORG = 'env-org'; + process.env.SENTRY_PROJECT = 'env-project'; + process.env.SENTRY_AUTH_TOKEN = 'env-token'; + process.env.SENTRY_URL = 'https://custom.sentry.io'; + + const options = getPluginOptions(); + + expect(options.org).toBe('env-org'); + expect(options.project).toBe('env-project'); + expect(options.authToken).toBe('env-token'); + expect(options.url).toBe('https://custom.sentry.io'); // sentryUrl maps to url + }); + + it('prefers direct options over environment variables', () => { + process.env.SENTRY_ORG = 'env-org'; + process.env.SENTRY_AUTH_TOKEN = 'env-token'; + process.env.SENTRY_URL = 'https://env.sentry.io'; + + const options = getPluginOptions({ + org: 'direct-org', + authToken: 'direct-token', + sentryUrl: 'https://direct.sentry.io', + }); + + expect(options.org).toBe('direct-org'); + expect(options.authToken).toBe('direct-token'); + expect(options.url).toBe('https://direct.sentry.io'); + }); + + it('passes through all user options', () => { + const sentryOptions: SentryNitroOptions = { + org: 'my-org', + project: 'my-project', + authToken: 'my-token', + sentryUrl: 'https://my-sentry.io', + headers: { 'X-Custom': 'header' }, + debug: true, + silent: true, + telemetry: false, + errorHandler: () => {}, + release: { name: 'v1.0.0' }, + sourcemaps: { + assets: ['dist/**'], + ignore: ['dist/test/**'], + filesToDeleteAfterUpload: ['dist/**/*.map'], + }, + }; + + const options = getPluginOptions(sentryOptions); + + expect(options.org).toBe('my-org'); + expect(options.project).toBe('my-project'); + expect(options.authToken).toBe('my-token'); + expect(options.url).toBe('https://my-sentry.io'); + expect(options.headers).toEqual({ 'X-Custom': 'header' }); + expect(options.debug).toBe(true); + expect(options.silent).toBe(true); + expect(options.telemetry).toBe(false); + expect(options.errorHandler).toBeDefined(); + expect(options.release).toEqual({ name: 'v1.0.0' }); + expect(options.sourcemaps?.assets).toEqual(['dist/**']); + expect(options.sourcemaps?.ignore).toEqual(['dist/test/**']); + expect(options.sourcemaps?.filesToDeleteAfterUpload).toEqual(['dist/**/*.map']); + }); + + it('normalizes source paths via rewriteSources', () => { + const options = getPluginOptions(); + const rewriteSources = options.sourcemaps?.rewriteSources; + + expect(rewriteSources?.('../../../src/index.ts', undefined)).toBe('./src/index.ts'); + expect(rewriteSources?.('../../lib/utils.ts', undefined)).toBe('./lib/utils.ts'); + expect(rewriteSources?.('./src/index.ts', undefined)).toBe('./src/index.ts'); + expect(rewriteSources?.('src/index.ts', undefined)).toBe('src/index.ts'); + }); + + it('uses user-provided rewriteSources when given', () => { + const customRewrite = (source: string) => `/custom/${source}`; + const options = getPluginOptions({ sourcemaps: { rewriteSources: customRewrite } }); + + expect(options.sourcemaps?.rewriteSources?.('../../../src/index.ts', undefined)).toBe( + '/custom/../../../src/index.ts', + ); + }); + + it('always sets metaFramework to nitro', () => { + const options = getPluginOptions(); + + expect(options._metaOptions?.telemetry?.metaFramework).toBe('nitro'); + }); + + it('passes through sourcemaps.disable', () => { + const options = getPluginOptions({ sourcemaps: { disable: 'disable-upload' } }); + + expect(options.sourcemaps?.disable).toBe('disable-upload'); + }); +}); + +describe('configureSourcemapSettings', () => { + it('enables hidden sourcemap generation on the config', () => { + const config: NitroConfig = {}; + const result = configureSourcemapSettings(config); + + expect(config.sourcemap).toBe('hidden'); + expect(result.sentryEnabledSourcemaps).toBe(true); + }); + + it('respects user explicitly disabling sourcemaps and warns', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const config: NitroConfig = { sourcemap: false }; + const result = configureSourcemapSettings(config); + + expect(config.sourcemap).toBe(false); + expect(result.sentryEnabledSourcemaps).toBe(false); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('explicitly disabled source maps')); + warnSpy.mockRestore(); + }); + + it('does not modify experimental config when user disabled sourcemaps', () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}); + const config: NitroConfig = { sourcemap: false }; + configureSourcemapSettings(config); + + expect(config.experimental).toBeUndefined(); + vi.restoreAllMocks(); + }); + + it('keeps sourcemap true when user already set it', () => { + const config: NitroConfig = { sourcemap: true }; + const result = configureSourcemapSettings(config); + + expect(config.sourcemap).toBe(true); + expect(result.sentryEnabledSourcemaps).toBe(false); + }); + + it('keeps sourcemap "hidden" when user already set it and does not enable deletion', () => { + const config = { sourcemap: 'hidden' } as unknown as NitroConfig; + const result = configureSourcemapSettings(config); + + expect((config as { sourcemap?: unknown }).sourcemap).toBe('hidden'); + expect(result.sentryEnabledSourcemaps).toBe(false); + }); + + it('keeps sourcemap "inline", warns, and does not enable uploads', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const config = { sourcemap: 'inline' } as unknown as NitroConfig; + const result = configureSourcemapSettings(config); + + expect((config as { sourcemap?: unknown }).sourcemap).toBe('inline'); + expect(result.sentryEnabledSourcemaps).toBe(false); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('`sourcemap: "inline"`')); + warnSpy.mockRestore(); + }); + + it('disables experimental sourcemapMinify', () => { + const config: NitroConfig = {}; + configureSourcemapSettings(config); + + expect(config.experimental?.sourcemapMinify).toBe(false); + }); + + it('preserves existing experimental config', () => { + const config: NitroConfig = { + experimental: { + sourcemapMinify: undefined, + }, + }; + configureSourcemapSettings(config); + + expect(config.experimental?.sourcemapMinify).toBe(false); + }); + + it('skips sourcemap config when sourcemaps.disable is true', () => { + const config: NitroConfig = { sourcemap: false }; + configureSourcemapSettings(config, { sourcemaps: { disable: true } }); + + expect(config.sourcemap).toBe(false); + }); + + it('still configures sourcemaps when sourcemaps.disable is disable-upload', () => { + const config: NitroConfig = {}; + configureSourcemapSettings(config, { sourcemaps: { disable: 'disable-upload' } }); + + expect(config.sourcemap).toBe('hidden'); + }); +}); + +describe('setupSentryNitroModule', () => { + it('enables tracing', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config); + + expect(config.tracingChannel).toBe(true); + }); + + it('adds the sentry module', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config); + + expect(config.modules).toBeDefined(); + expect(config.modules?.length).toBe(1); + }); + + it('still adds module when sourcemaps are disabled', () => { + const config: NitroConfig = {}; + setupSentryNitroModule(config, { sourcemaps: { disable: true } }); + + expect(config.modules).toBeDefined(); + expect(config.modules?.length).toBe(1); + }); +}); + +describe('setupSourceMaps', () => { + it('does not register hook in dev mode', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: true, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro); + + expect(hookFn).not.toHaveBeenCalled(); + }); + + it('does not register hook when sourcemaps.disable is true', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro, { sourcemaps: { disable: true } }); + + expect(hookFn).not.toHaveBeenCalled(); + }); + + it('does not register hook when nitro sourcemap is disabled', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, sourcemap: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro); + + expect(hookFn).not.toHaveBeenCalled(); + }); + + it('does not register hook in nitro-prerender preset', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, preset: 'nitro-prerender', output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro); + + expect(hookFn).not.toHaveBeenCalled(); + }); + + it('registers compiled hook in production mode', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro); + + expect(hookFn).toHaveBeenCalledWith('compiled', expect.any(Function)); + }); + + it('registers compiled hook with custom options', () => { + const hookFn = vi.fn(); + const nitro = { + options: { dev: false, output: { serverDir: '/output/server' } }, + hooks: { hook: hookFn }, + } as any; + + setupSourceMaps(nitro, { org: 'my-org', project: 'my-project' }); + + expect(hookFn).toHaveBeenCalledWith('compiled', expect.any(Function)); + }); +}); diff --git a/packages/nitro/test/tsconfig.json b/packages/nitro/test/tsconfig.json new file mode 100644 index 000000000000..38ca0b13bcdd --- /dev/null +++ b/packages/nitro/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.test.json" +} diff --git a/packages/nitro/tsconfig.json b/packages/nitro/tsconfig.json new file mode 100644 index 000000000000..202590772b10 --- /dev/null +++ b/packages/nitro/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + // package-specific options + "module": "esnext", + "moduleResolution": "bundler" + } +} diff --git a/packages/nitro/tsconfig.test.json b/packages/nitro/tsconfig.test.json new file mode 100644 index 000000000000..c41efeacd92f --- /dev/null +++ b/packages/nitro/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "vite.config.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node"] + } +} diff --git a/packages/nitro/tsconfig.types.json b/packages/nitro/tsconfig.types.json new file mode 100644 index 000000000000..6240cd92efaa --- /dev/null +++ b/packages/nitro/tsconfig.types.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + }, + + "//": "This is built separately in tsconfig.setup-types.json", + "exclude": ["src/setup.ts"] +} diff --git a/packages/nitro/vite.config.ts b/packages/nitro/vite.config.ts new file mode 100644 index 000000000000..4c0db8cdc068 --- /dev/null +++ b/packages/nitro/vite.config.ts @@ -0,0 +1,11 @@ +import baseConfig from '../../vite/vite.config'; + +export default { + ...baseConfig, + test: { + typecheck: { + enabled: true, + tsconfig: './tsconfig.test.json', + }, + }, +}; diff --git a/packages/opentelemetry/src/tracingChannel.ts b/packages/opentelemetry/src/tracingChannel.ts index 984986b7cdcb..5548201c5f4c 100644 --- a/packages/opentelemetry/src/tracingChannel.ts +++ b/packages/opentelemetry/src/tracingChannel.ts @@ -18,7 +18,7 @@ import { DEBUG_BUILD } from './debug-build'; */ export type OtelTracingChannelTransform = (data: TData) => Span; -type WithSpan = TData & { _sentrySpan?: Span }; +export type TracingChannelContextWithSpan = TContext & { _sentrySpan?: Span }; /** * A TracingChannel whose `subscribe` / `unsubscribe` accept partial subscriber @@ -26,7 +26,7 @@ type WithSpan = TData & { _sentrySpan?: Span }; */ export interface OtelTracingChannel< TData extends object = object, - TDataWithSpan extends object = WithSpan, + TDataWithSpan extends object = TracingChannelContextWithSpan, > extends Omit, 'subscribe' | 'unsubscribe'> { subscribe(subscribers: Partial>): void; unsubscribe(subscribers: Partial>): void; @@ -52,10 +52,10 @@ interface ContextApi { export function tracingChannel( channelNameOrInstance: string, transformStart: OtelTracingChannelTransform, -): OtelTracingChannel> { - const channel = nativeTracingChannel, WithSpan>( +): OtelTracingChannel> { + const channel = nativeTracingChannel, TracingChannelContextWithSpan>( channelNameOrInstance, - ) as unknown as OtelTracingChannel>; + ) as unknown as OtelTracingChannel>; let lookup: AsyncLocalStorageLookup | undefined; try { @@ -78,7 +78,7 @@ export function tracingChannel( // Bind the start channel so that each trace invocation runs the transform // and stores the resulting context (with span) in AsyncLocalStorage. // @ts-expect-error bindStore types don't account for AsyncLocalStorage of a different generic type - channel.start.bindStore(otelStorage, (data: WithSpan) => { + channel.start.bindStore(otelStorage, (data: TracingChannelContextWithSpan) => { const span = transformStart(data); // Store the span on data so downstream event handlers (asyncEnd, error, etc.) can access it. diff --git a/yarn.lock b/yarn.lock index d655cea5359d..8060d7cd8b2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3158,12 +3158,12 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== -"@dabh/diagnostics@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" - integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== +"@dabh/diagnostics@^2.0.2", "@dabh/diagnostics@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz#ead97e72ca312cf0e6dd7af0d300b58993a31a5e" + integrity sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q== dependencies: - colorspace "1.1.x" + "@so-ric/colorspace" "^1.1.6" enabled "2.0.x" kuler "^2.0.0" @@ -3305,25 +3305,25 @@ lodash "^4.17.21" resolve "^1.20.0" -"@emnapi/core@^1.1.0", "@emnapi/core@^1.4.3", "@emnapi/core@^1.7.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.9.1.tgz#2143069c744ca2442074f8078462e51edd63c7bd" - integrity sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA== +"@emnapi/core@1.9.2", "@emnapi/core@^1.1.0", "@emnapi/core@^1.4.3": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.9.2.tgz#3870265ecffc7352d01ead62d8d83d8358a2d034" + integrity sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA== dependencies: - "@emnapi/wasi-threads" "1.2.0" + "@emnapi/wasi-threads" "1.2.1" tslib "^2.4.0" -"@emnapi/runtime@^1.1.0", "@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.7.0", "@emnapi/runtime@^1.7.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.1.tgz#115ff2a0d589865be6bd8e9d701e499c473f2a8d" - integrity sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA== +"@emnapi/runtime@1.9.2", "@emnapi/runtime@^1.1.0", "@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.7.0": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.9.2.tgz#8b469a3db160817cadb1de9050211a9d1ea84fa2" + integrity sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz#a19d9772cc3d195370bf6e2a805eec40aa75e18e" - integrity sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg== +"@emnapi/wasi-threads@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" + integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== dependencies: tslib "^2.4.0" @@ -5338,13 +5338,11 @@ "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.10.0" -"@napi-rs/wasm-runtime@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz#c3705ab549d176b8dc5172723d6156c3dc426af2" - integrity sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A== +"@napi-rs/wasm-runtime@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz#1eeb8699770481306e5fcd84471f20fcb6177336" + integrity sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ== dependencies: - "@emnapi/core" "^1.7.1" - "@emnapi/runtime" "^1.7.1" "@tybys/wasm-util" "^0.10.1" "@nestjs/common@^10.0.0": @@ -6468,10 +6466,10 @@ resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.76.0.tgz#3dbef82283f871c9cb59325c9daf4f740d11a6e9" integrity sha512-0jLzzmnu8/mqNhKBnNS2lFUbPEzRdj5ReiZwHGHpjma0+ullmmwP2AqSEqx3ssHDK9CpcEMdKOK2LsbCfhHKIA== -"@oxc-project/types@=0.120.0": - version "0.120.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.120.0.tgz#af521b0e689dd0eaa04fe4feef9b68d98b74783d" - integrity sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg== +"@oxc-project/types@=0.124.0": + version "0.124.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.124.0.tgz#1dfd7b3fbb98febc2f91b505f48c940db73c8701" + integrity sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg== "@oxc-project/types@^0.76.0": version "0.76.0" @@ -6823,9 +6821,9 @@ integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== "@poppinss/colors@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@poppinss/colors/-/colors-4.1.5.tgz#09273b845a4816f5fd9c53c78a3bc656650fe18f" - integrity sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw== + version "4.1.6" + resolved "https://registry.yarnpkg.com/@poppinss/colors/-/colors-4.1.6.tgz#bf8546e30cfc5ee8dfe68988ce58eb0ad9d7c21b" + integrity sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg== dependencies: kleur "^4.1.5" @@ -7183,87 +7181,94 @@ dependencies: web-streams-polyfill "^3.1.1" -"@rolldown/binding-android-arm64@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz#0bbd3380f49a6d0dc96c9b32fb7dad26ae0dfaa7" - integrity sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg== - -"@rolldown/binding-darwin-arm64@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz#a30b051784fbb13635e652ba4041c6ce7a4ce7ab" - integrity sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w== - -"@rolldown/binding-darwin-x64@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz#2d9dea982d5be90b95b6d8836ff26a4b0959d94b" - integrity sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A== - -"@rolldown/binding-freebsd-x64@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz#4efc3aca43ae4dfb90729eeca6e84ef6e6b38c4a" - integrity sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w== - -"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz#4a19a5d24537e925b25e9583b6cd575b2ad9fa27" - integrity sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA== - -"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz#01a41e5e905838353ae9a3da10dc8242dcd61453" - integrity sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg== - -"@rolldown/binding-linux-arm64-musl@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz#bd059e5f83471de29ce35b0ba254995d8091ca40" - integrity sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g== - -"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz#fe726a540631015f269a989c0cfb299283190390" - integrity sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w== - -"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz#825ced028bad3f1fa9ce83b1f3dac76e0424367f" - integrity sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg== - -"@rolldown/binding-linux-x64-gnu@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz#b700dae69274aa3d54a16ca5e00e30f47a089119" - integrity sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw== - -"@rolldown/binding-linux-x64-musl@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz#eb875660ad68a2348acab36a7005699e87f6e9dd" - integrity sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA== - -"@rolldown/binding-openharmony-arm64@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz#72aa24b412f83025087bcf83ce09634b2bd93c5c" - integrity sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q== - -"@rolldown/binding-wasm32-wasi@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz#7f3303a96c5dc01d1f4c539b1dcbc16392c6f17d" - integrity sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA== - dependencies: - "@napi-rs/wasm-runtime" "^1.1.1" - -"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz#3419144a04ad12c69c48536b01fc21ac9d87ecf4" - integrity sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ== - -"@rolldown/binding-win32-x64-msvc@1.0.0-rc.10": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz#09bee46e6a32c6086beeabc3da12e67be714f882" - integrity sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w== - -"@rolldown/pluginutils@1.0.0-rc.10", "@rolldown/pluginutils@^1.0.0-beta.9": - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz#eed997f37f928a3300bbe2161f42687d8a3ae759" - integrity sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg== +"@rolldown/binding-android-arm64@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz#ca20574c469ade7b941f90c9af5e83e7c67f06b7" + integrity sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA== + +"@rolldown/binding-darwin-arm64@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz#ce2c5c7fc4958dfc94783dc09b3d09f3c2e1d072" + integrity sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg== + +"@rolldown/binding-darwin-x64@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz#251ecdf1fdb751031cb6486907c105daaf9dab21" + integrity sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw== + +"@rolldown/binding-freebsd-x64@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz#dbcfe95f409bf671a77bd83bff0fdc877d217728" + integrity sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw== + +"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz#ea002b45445be6f9ed1883a834b335bc2ccd510f" + integrity sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA== + +"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz#12b96e7e7821a9dc2cd5c670ad56882987ed5c62" + integrity sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w== + +"@rolldown/binding-linux-arm64-musl@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz#738b0f62f0b65bf676dfe48595017f1883859d1f" + integrity sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ== + +"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz#3088b9fbc2783033985b558316f87f39281bc533" + integrity sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ== + +"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz#ac0aa6f1b72e3151d56c43145a71c745cf862a9a" + integrity sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ== + +"@rolldown/binding-linux-x64-gnu@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz#b8cf27aa5be6da641c22dad5665d0240551d2dec" + integrity sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA== + +"@rolldown/binding-linux-x64-musl@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz#4531f9eca77963935026634ba9b61c2535340534" + integrity sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw== + +"@rolldown/binding-openharmony-arm64@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz#66ff691a65f9325171bced98e353b4cc4b0095c3" + integrity sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg== + +"@rolldown/binding-wasm32-wasi@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz#7db6c90aa510eef65d7d0f14e8ca23775e8e5eee" + integrity sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q== + dependencies: + "@emnapi/core" "1.9.2" + "@emnapi/runtime" "1.9.2" + "@napi-rs/wasm-runtime" "^1.1.3" + +"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz#81f9097abbd4493cc13373b26f5a3da8461dbb47" + integrity sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA== + +"@rolldown/binding-win32-x64-msvc@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz#cef11bc89149f3a77771727be75490fbb13ae193" + integrity sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g== + +"@rolldown/pluginutils@1.0.0-rc.15": + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz#e75d7731593e195d23710f9ff49bf5c745c96682" + integrity sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g== + +"@rolldown/pluginutils@^1.0.0-beta.9": + version "1.0.0-rc.16" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz#bc27c8f906309b57c6c10eddb21043fd8e86b87e" + integrity sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA== "@rollup/plugin-alias@^5.0.0": version "5.1.1" @@ -8377,6 +8382,14 @@ dependencies: tslib "^2.6.2" +"@so-ric/colorspace@^1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@so-ric/colorspace/-/colorspace-1.1.6.tgz#62515d8b9f27746b76950a83bde1af812d91923b" + integrity sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw== + dependencies: + color "^5.0.2" + text-hex "1.0.x" + "@socket.io/component-emitter@~3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" @@ -13423,7 +13436,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0, color-convert@^1.9.3: +color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -13437,6 +13450,13 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-convert@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-3.1.3.tgz#db6627b97181cb8facdfce755ae26f97ab0711f1" + integrity sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg== + dependencies: + color-name "^2.0.0" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -13447,7 +13467,12 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0, color-string@^1.9.0: +color-name@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.1.0.tgz#0b677385c1c4b4edfdeaf77e38fa338e3a40b693" + integrity sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg== + +color-string@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -13455,19 +13480,18 @@ color-string@^1.6.0, color-string@^1.9.0: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-string@^2.1.3: + version "2.1.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-2.1.4.tgz#9dcf566ff976e23368c8bd673f5c35103ab41058" + integrity sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg== + dependencies: + color-name "^2.0.0" + color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -color@^3.1.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - color@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" @@ -13476,6 +13500,14 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +color@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/color/-/color-5.0.3.tgz#f79390b1b778e222ffbb54304d3dbeaef633f97f" + integrity sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA== + dependencies: + color-convert "^3.1.3" + color-string "^2.1.3" + colord@^2.9.3: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" @@ -13501,14 +13533,6 @@ colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -colorspace@1.1.x: - version "1.1.4" - resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" - integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== - dependencies: - color "^3.1.3" - text-hex "1.0.x" - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -13983,10 +14007,10 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: dependencies: uncrypto "^0.1.3" -crossws@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.4.4.tgz#d62574bcc6de75f0e45fe08b5133d9ba8436a30c" - integrity sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg== +crossws@^0.4.4, crossws@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.4.5.tgz#e300fec909cd93fe377a1cee84f6813c9c786edf" + integrity sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA== crypto-random-string@^2.0.0: version "2.0.0" @@ -15653,14 +15677,15 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== -env-runner@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/env-runner/-/env-runner-0.1.6.tgz#b2acc95c00bc9a00457d7ad5220f10bd75595b2d" - integrity sha512-fSb7X1zdda8k6611a6/SdSQpDe7a/bqMz2UWdbHjk9YWzpUR4/fn9YtE/hqgGQ2nhvVN0zUtcL1SRMKwIsDbAA== +env-runner@^0.1.6, env-runner@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/env-runner/-/env-runner-0.1.7.tgz#ab26aa711cf195c9d8e158b6e864291fbd8d202e" + integrity sha512-i7h96jxETJYhXy5grgHNJ9xNzCzWIn9Ck/VkkYgOlE4gOqknsLX3CmlVb5LmwNex8sOoLFVZLz+TIw/+b5rktA== dependencies: crossws "^0.4.4" - httpxy "^0.3.1" - srvx "^0.11.9" + exsolve "^1.0.8" + httpxy "^0.5.0" + srvx "^0.11.13" err-code@^2.0.2: version "2.0.3" @@ -18372,13 +18397,13 @@ h3@^1.10.0, h3@^1.12.0, h3@^1.15.3, h3@^1.15.5: ufo "^1.6.3" uncrypto "^0.1.3" -h3@^2.0.1-rc.16: - version "2.0.1-rc.17" - resolved "https://registry.yarnpkg.com/h3/-/h3-2.0.1-rc.17.tgz#86fb5a5261a38f59e0fb3384581e345285be3b61" - integrity sha512-9rPJs68qMj7HJH78z7uSIAw6rl3EElLdVSirTeAf6B5ogwiFVIr9AKMMS4u00Gp8DYIPnnjtw3ZWN7EkYcPBrQ== +h3@^2.0.1-rc.13, h3@^2.0.1-rc.16, h3@^2.0.1-rc.20: + version "2.0.1-rc.20" + resolved "https://registry.yarnpkg.com/h3/-/h3-2.0.1-rc.20.tgz#51050db30afb0b6e69718d88cccc23666fbe8039" + integrity sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg== dependencies: rou3 "^0.8.1" - srvx "^0.11.12" + srvx "^0.11.13" handle-thing@^2.0.0: version "2.0.1" @@ -18737,10 +18762,10 @@ hookable@^5.5.3: resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== -hookable@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/hookable/-/hookable-6.0.1.tgz#be950f1b8ef38af24d4354657e9e3590d2a5b5e6" - integrity sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw== +hookable@^6.0.1, hookable@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/hookable/-/hookable-6.1.1.tgz#825f966b4b426db2e622d94d7a31a70f196f9d2f" + integrity sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ== hosted-git-info@^5.0.0: version "5.1.0" @@ -18965,10 +18990,10 @@ httpxy@^0.1.7: resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.1.7.tgz#02d02e57eda10e8b5c0e3f9f10860e3d7a5991a4" integrity sha512-pXNx8gnANKAndgga5ahefxc++tJvNL87CXoRwxn1cJE2ZkWEojF3tNfQIEhZX/vfpt+wzeAzpUI4qkediX1MLQ== -httpxy@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.3.1.tgz#da1bb1a4a26cb44d7835a9297c845a0e06372083" - integrity sha512-XjG/CEoofEisMrnFr0D6U6xOZ4mRfnwcYQ9qvvnT4lvnX8BoeA3x3WofB75D+vZwpaobFVkBIHrZzoK40w8XSw== +httpxy@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.5.0.tgz#a9c53543760dee498611827a464e56e14639c0d0" + integrity sha512-qwX7QX/rK2visT10/b7bSeZWQOMlSm3svTD0pZpU+vJjNUP0YHtNv4c3z+MO+MSnGuRFWJFdCZiV+7F7dXIOzg== human-signals@^1.1.1: version "1.1.1" @@ -22625,10 +22650,10 @@ next@14.2.35: "@next/swc-win32-ia32-msvc" "14.2.33" "@next/swc-win32-x64-msvc" "14.2.33" -nf3@^0.3.11: - version "0.3.13" - resolved "https://registry.yarnpkg.com/nf3/-/nf3-0.3.13.tgz#9dfbc08158c9f12583ebf82bd89c97dc362b7df1" - integrity sha512-drDt0yl4d/yUhlpD0GzzqahSpA5eUNeIfFq0/aoZb0UlPY0ZwP4u1EfREVvZrYdEnJ3OU9Le9TrzbvWgEkkeKw== +nf3@^0.3.11, nf3@^0.3.16: + version "0.3.16" + resolved "https://registry.yarnpkg.com/nf3/-/nf3-0.3.16.tgz#36e3d1bb36d98ee78b47627b7967864c2ea01720" + integrity sha512-Gs0xRPpUm2nDkqbi40NJ9g7qDIcjcJzgExiydnq6LAyqhI2jfno8wG3NKTL+IiJsx799UHOb1CnSd4Wg4SG4Pw== ng-packagr@^14.2.2: version "14.3.0" @@ -22695,6 +22720,26 @@ nitro@^3.0.260311-beta: unenv "^2.0.0-rc.24" unstorage "^2.0.0-alpha.6" +nitro@^3.0.260415-beta: + version "3.0.260415-beta" + resolved "https://registry.yarnpkg.com/nitro/-/nitro-3.0.260415-beta.tgz#2a40c38c9a2d6ae14b259ebe78e5ce1142d0c5e5" + integrity sha512-J0ntJERWtIdvweZdmkCiF8eOFvP9fIAJR2gpeIDrHbAlYavK41WQfADo/YoZ/LF7RMTZBiPaH/pt2s/nPru9Iw== + dependencies: + consola "^3.4.2" + crossws "^0.4.5" + db0 "^0.3.4" + env-runner "^0.1.7" + h3 "^2.0.1-rc.20" + hookable "^6.1.1" + nf3 "^0.3.16" + ocache "^0.1.4" + ofetch "^2.0.0-alpha.3" + ohash "^2.0.11" + rolldown "^1.0.0-rc.15" + srvx "^0.11.15" + unenv "^2.0.0-rc.24" + unstorage "^2.0.0-alpha.7" + nitropack@^2.11.10, nitropack@^2.11.13, nitropack@^2.13.1: version "2.13.1" resolved "https://registry.yarnpkg.com/nitropack/-/nitropack-2.13.1.tgz#70be1b14eb0d2fed9c670fe7cfff3741c384ecf2" @@ -23440,7 +23485,7 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -ocache@^0.1.2: +ocache@^0.1.2, ocache@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/ocache/-/ocache-0.1.4.tgz#d4a71be84ceaeb5685cc0128c197d44713dda9a7" integrity sha512-e7geNdWjxSnvsSgvLuPvgKgu7ubM10ZmTPOgpr7mz2BXYtvjMKTiLhjFi/gWU8chkuP6hNkZBsa9LzOusyaqkQ== @@ -26565,29 +26610,29 @@ roarr@^7.0.4: safe-stable-stringify "^2.4.1" semver-compare "^1.0.0" -rolldown@^1.0.0-rc.8: - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.10.tgz#41c55e52d833c52c90131973047250548e35f2bf" - integrity sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA== +rolldown@^1.0.0-rc.15, rolldown@^1.0.0-rc.8: + version "1.0.0-rc.15" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.15.tgz#ea3526443b2dbe834e9f8f6c1fde6232ec687170" + integrity sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g== dependencies: - "@oxc-project/types" "=0.120.0" - "@rolldown/pluginutils" "1.0.0-rc.10" + "@oxc-project/types" "=0.124.0" + "@rolldown/pluginutils" "1.0.0-rc.15" optionalDependencies: - "@rolldown/binding-android-arm64" "1.0.0-rc.10" - "@rolldown/binding-darwin-arm64" "1.0.0-rc.10" - "@rolldown/binding-darwin-x64" "1.0.0-rc.10" - "@rolldown/binding-freebsd-x64" "1.0.0-rc.10" - "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.10" - "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.10" - "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.10" - "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.10" - "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.10" - "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.10" - "@rolldown/binding-linux-x64-musl" "1.0.0-rc.10" - "@rolldown/binding-openharmony-arm64" "1.0.0-rc.10" - "@rolldown/binding-wasm32-wasi" "1.0.0-rc.10" - "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.10" - "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.10" + "@rolldown/binding-android-arm64" "1.0.0-rc.15" + "@rolldown/binding-darwin-arm64" "1.0.0-rc.15" + "@rolldown/binding-darwin-x64" "1.0.0-rc.15" + "@rolldown/binding-freebsd-x64" "1.0.0-rc.15" + "@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.15" + "@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.15" + "@rolldown/binding-linux-arm64-musl" "1.0.0-rc.15" + "@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.15" + "@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.15" + "@rolldown/binding-linux-x64-gnu" "1.0.0-rc.15" + "@rolldown/binding-linux-x64-musl" "1.0.0-rc.15" + "@rolldown/binding-openharmony-arm64" "1.0.0-rc.15" + "@rolldown/binding-wasm32-wasi" "1.0.0-rc.15" + "@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.15" + "@rolldown/binding-win32-x64-msvc" "1.0.0-rc.15" rollup-plugin-cleanup@^3.2.1: version "3.2.1" @@ -27906,10 +27951,10 @@ sqlstring@2.3.1: resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= -srvx@^0.11.12, srvx@^0.11.2, srvx@^0.11.9: - version "0.11.13" - resolved "https://registry.yarnpkg.com/srvx/-/srvx-0.11.13.tgz#cc77a98cb9a459c34f75ee4345bd0eef9f613a54" - integrity sha512-oknN6qduuMPafxKtHucUeG32Q963pjriA5g3/Bl05cwEsUe5VVbIU4qR9LrALHbipSCyBe+VmfDGGydqazDRkw== +srvx@^0.11.13, srvx@^0.11.15, srvx@^0.11.2, srvx@^0.11.9: + version "0.11.15" + resolved "https://registry.yarnpkg.com/srvx/-/srvx-0.11.15.tgz#51c08f993bb116f5821ec929a466a29e8d5c7b61" + integrity sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg== ssri@^9.0.0: version "9.0.1" @@ -29836,7 +29881,7 @@ unstorage@^1.16.0, unstorage@^1.17.4: ofetch "^1.5.1" ufo "^1.6.3" -unstorage@^2.0.0-alpha.6: +unstorage@^2.0.0-alpha.6, unstorage@^2.0.0-alpha.7: version "2.0.0-alpha.7" resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-2.0.0-alpha.7.tgz#803ea90176683bf2175bb01065cb07df6d65280a" integrity sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog== @@ -30895,12 +30940,12 @@ winston@3.13.0: winston-transport "^4.7.0" winston@^3.17.0: - version "3.17.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.17.0.tgz#74b8665ce9b4ea7b29d0922cfccf852a08a11423" - integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== + version "3.19.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.19.0.tgz#cc1d1262f5f45946904085cfffe73efb4b7a581d" + integrity sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA== dependencies: "@colors/colors" "^1.6.0" - "@dabh/diagnostics" "^2.0.2" + "@dabh/diagnostics" "^2.0.8" async "^3.2.3" is-stream "^2.0.0" logform "^2.7.0"