From 46004955e8bf9a4bb7ae8a6a280d7b4e509488cd Mon Sep 17 00:00:00 2001 From: John Leider Date: Thu, 28 May 2026 11:14:36 -0500 Subject: [PATCH 1/5] refactor: attach structured cause to v0 throws Adopt Error.cause on the two user-facing throws where a stable discriminant beats regex-matching the message: - useContext: cause = { code: 'V0_CONTEXT_MISSING', key } - useDate: cause = { code: 'V0_PLUGIN_MISSING', plugin: 'createDatePlugin' } Tooling (devtools panels, error overlays, error trackers) can now identify v0 errors stably without parsing strings. --- packages/0/src/composables/createContext/index.ts | 4 +++- packages/0/src/composables/useDate/index.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/0/src/composables/createContext/index.ts b/packages/0/src/composables/createContext/index.ts index 75e6935bb..7e279c6be 100644 --- a/packages/0/src/composables/createContext/index.ts +++ b/packages/0/src/composables/createContext/index.ts @@ -64,7 +64,9 @@ export function useContext (key: ContextKey, defaultValue?: Z) { const context = inject(key, defaultValue as Z) if (isUndefined(context)) { - throw new Error(`Context "${String(key)}" not found. Ensure it's provided by an ancestor.`) + throw new Error(`Context "${String(key)}" not found. Ensure it's provided by an ancestor.`, { + cause: { code: 'V0_CONTEXT_MISSING', key }, + }) } return context diff --git a/packages/0/src/composables/useDate/index.ts b/packages/0/src/composables/useDate/index.ts index fa077bb96..74e2cc18a 100644 --- a/packages/0/src/composables/useDate/index.ts +++ b/packages/0/src/composables/useDate/index.ts @@ -335,6 +335,9 @@ export function useDate< ' import { V0DateAdapter } from \'@vuetify/v0/date\'\n' + ' import { createDatePlugin } from \'@vuetify/v0\'\n\n' + ' app.use(createDatePlugin({ adapter: new V0DateAdapter() }))', + { + cause: { code: 'V0_PLUGIN_MISSING', plugin: 'createDatePlugin' }, + }, ) } From 0a065b8429bbf2d2aeccc986be4c4bf07610a45d Mon Sep 17 00:00:00 2001 From: John Leider Date: Thu, 28 May 2026 11:22:08 -0500 Subject: [PATCH 2/5] refactor(types): catalog v0 error codes in a discriminated union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce V0ErrorCause as the central registry of every `Error.cause` payload v0 throws, with V0ErrorCode as a convenience alias for the discriminant field. Updated throws use `satisfies V0ErrorCause` so the compiler verifies the discriminant and the payload shape at the call site — typos in 'V0_*' codes no longer compile, and adding a new code requires extending the union (a single place to audit). Consumers gain narrowed payload typing on `err.cause` per code. --- .../0/src/composables/createContext/index.ts | 3 +- packages/0/src/composables/useDate/index.ts | 4 +- packages/0/src/types/index.ts | 43 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/0/src/composables/createContext/index.ts b/packages/0/src/composables/createContext/index.ts index 7e279c6be..cfe8c52d2 100644 --- a/packages/0/src/composables/createContext/index.ts +++ b/packages/0/src/composables/createContext/index.ts @@ -30,6 +30,7 @@ import { isObject, isString, isSymbol, isUndefined } from '#v0/utilities' import { inject, provide } from 'vue' // Types +import type { V0ErrorCause } from '#v0/types' import type { App, InjectionKey } from 'vue' export type ContextKey = InjectionKey | string @@ -65,7 +66,7 @@ export function useContext (key: ContextKey, defaultValue?: Z) { if (isUndefined(context)) { throw new Error(`Context "${String(key)}" not found. Ensure it's provided by an ancestor.`, { - cause: { code: 'V0_CONTEXT_MISSING', key }, + cause: { code: 'V0_CONTEXT_MISSING', key } satisfies V0ErrorCause, }) } diff --git a/packages/0/src/composables/useDate/index.ts b/packages/0/src/composables/useDate/index.ts index 74e2cc18a..dbb92b9ae 100644 --- a/packages/0/src/composables/useDate/index.ts +++ b/packages/0/src/composables/useDate/index.ts @@ -51,7 +51,7 @@ import { computed, watchEffect, onScopeDispose } from 'vue' // Types import type { ContextTrinity } from '#v0/composables/createTrinity' import type { DateAdapter } from '#v0/composables/useDate/adapters' -import type { ID } from '#v0/types' +import type { ID, V0ErrorCause } from '#v0/types' import type { App, ComputedRef, Ref } from 'vue' // Exports @@ -336,7 +336,7 @@ export function useDate< ' import { createDatePlugin } from \'@vuetify/v0\'\n\n' + ' app.use(createDatePlugin({ adapter: new V0DateAdapter() }))', { - cause: { code: 'V0_PLUGIN_MISSING', plugin: 'createDatePlugin' }, + cause: { code: 'V0_PLUGIN_MISSING', plugin: 'createDatePlugin' } satisfies V0ErrorCause, }, ) } diff --git a/packages/0/src/types/index.ts b/packages/0/src/types/index.ts index 20b60c7db..2117c8da9 100644 --- a/packages/0/src/types/index.ts +++ b/packages/0/src/types/index.ts @@ -135,3 +135,46 @@ export type Extensible = T | (string & {}) * ``` */ export type Activation = 'automatic' | 'manual' + +/** + * Discriminated union of all `Error.cause` payloads thrown by v0 + * + * @remarks + * Every error v0 throws attaches a structured `cause` with a `code` + * discriminant so consumers (devtools panels, error overlays, error trackers) + * can identify the error stably without parsing the message string. + * + * When authoring a new v0 throw, append the literal with + * `satisfies V0ErrorCause` so TypeScript verifies the discriminant and the + * payload shape at the call site. + * + * @example + * ```ts + * try { + * useContext(myKey) + * } catch (err) { + * if ( + * err instanceof Error + * && err.cause + * && typeof err.cause === 'object' + * && 'code' in err.cause + * ) { + * const cause = err.cause as V0ErrorCause + * if (cause.code === 'V0_CONTEXT_MISSING') { + * console.log('missing context key:', cause.key) + * } + * } + * } + * ``` + */ +export type V0ErrorCause = + | { code: 'V0_CONTEXT_MISSING', key: string | symbol } + | { code: 'V0_PLUGIN_MISSING', plugin: string } + +/** + * Union of all error codes thrown by v0 + * + * @remarks + * Convenience alias for the discriminant field of {@link V0ErrorCause}. + */ +export type V0ErrorCode = V0ErrorCause['code'] From 1be94ccbdc919d40faedd2d7564e193648494d53 Mon Sep 17 00:00:00 2001 From: John Leider Date: Thu, 28 May 2026 12:10:58 -0500 Subject: [PATCH 3/5] refactor(types): pivot to V0Error class, reserve cause for upstream wrapping Drop the `cause: { code, ... }` discriminant pattern. Code lives on a new V0Error class as a top-level field; Error.cause stays reserved for wrapping the genuine upstream error so Sentry's LinkedErrors, Datadog, and Rollbar render the chain correctly (the previous design dropped or mis-attached the payload because cause-as-data isn't an Error instance). Modeled on tRPC's TRPCError and Node's error code registry. Discriminated union (renamed V0ErrorCause -> V0ErrorDetails) is now the constructor input shape, and exports are surfaced from the package root for typed consumer catches. isV0Error(err, code?) collapses the manual instanceof + property checks into one call and intersects the instance with the matching details arm, so per-code fields (key, plugin) narrow to required after the guard. Refs: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause - https://nodejs.org/api/errors.html#nodejs-error-codes - https://trpc.io/docs/server/error-handling - https://docs.sentry.io/platforms/javascript/configuration/integrations/linkederrors/ --- .../composables/createContext/index.test.ts | 20 +++ .../0/src/composables/createContext/index.ts | 8 +- .../0/src/composables/useDate/index.test.ts | 18 +++ packages/0/src/composables/useDate/index.ts | 9 +- packages/0/src/index.ts | 2 +- packages/0/src/types/index.ts | 55 ++++---- packages/0/src/utilities/errors.ts | 117 ++++++++++++++++++ packages/0/src/utilities/index.ts | 1 + 8 files changed, 195 insertions(+), 35 deletions(-) create mode 100644 packages/0/src/utilities/errors.ts diff --git a/packages/0/src/composables/createContext/index.test.ts b/packages/0/src/composables/createContext/index.test.ts index 72f25f5c4..63c66a6a4 100644 --- a/packages/0/src/composables/createContext/index.test.ts +++ b/packages/0/src/composables/createContext/index.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createContext, provideContext, useContext } from './index' // Utilities +import { isV0Error, V0Error } from '#v0/utilities' import { inject, provide } from 'vue' // Types @@ -69,6 +70,25 @@ describe('createContext', () => { ) }) + it('should throw a V0Error tagged V0_CONTEXT_MISSING with the key payload', () => { + mockInject.mockReturnValue(undefined) + + const [injectContext] = createContext('v0:missing-key') + + let caught: unknown + try { + injectContext() + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(V0Error) + expect(isV0Error(caught, 'V0_CONTEXT_MISSING')).toBe(true) + if (isV0Error(caught, 'V0_CONTEXT_MISSING')) { + expect(caught.code).toBe('V0_CONTEXT_MISSING') + expect(caught.key).toBe('v0:missing-key') + } + }) + it('should handle symbol key in error message', () => { const symbolKey = Symbol('symbol-key') mockInject.mockReturnValue(undefined) diff --git a/packages/0/src/composables/createContext/index.ts b/packages/0/src/composables/createContext/index.ts index cfe8c52d2..bcb3ed706 100644 --- a/packages/0/src/composables/createContext/index.ts +++ b/packages/0/src/composables/createContext/index.ts @@ -26,11 +26,10 @@ */ // Utilities -import { isObject, isString, isSymbol, isUndefined } from '#v0/utilities' +import { isObject, isString, isSymbol, isUndefined, V0Error } from '#v0/utilities' import { inject, provide } from 'vue' // Types -import type { V0ErrorCause } from '#v0/types' import type { App, InjectionKey } from 'vue' export type ContextKey = InjectionKey | string @@ -65,8 +64,9 @@ export function useContext (key: ContextKey, defaultValue?: Z) { const context = inject(key, defaultValue as Z) if (isUndefined(context)) { - throw new Error(`Context "${String(key)}" not found. Ensure it's provided by an ancestor.`, { - cause: { code: 'V0_CONTEXT_MISSING', key } satisfies V0ErrorCause, + throw new V0Error(`Context "${String(key)}" not found. Ensure it's provided by an ancestor.`, { + code: 'V0_CONTEXT_MISSING', + key, }) } diff --git a/packages/0/src/composables/useDate/index.test.ts b/packages/0/src/composables/useDate/index.test.ts index 3b3542596..3b8534953 100644 --- a/packages/0/src/composables/useDate/index.test.ts +++ b/packages/0/src/composables/useDate/index.test.ts @@ -6,6 +6,9 @@ import { V0DateAdapter } from './adapters/v0' import { createDate, createDateContext, createDatePlugin, useDate } from './index' +// Utilities +import { isV0Error, V0Error } from '#v0/utilities' + describe('createDate', () => { describe('v0DateAdapter', () => { let adapter: V0DateAdapter @@ -1592,6 +1595,21 @@ describe('createDate', () => { it('should throw with helpful error message', () => { expect(() => useDate()).toThrow('createDatePlugin') }) + + it('should throw a V0Error tagged V0_PLUGIN_MISSING with the plugin payload', () => { + let caught: unknown + try { + useDate() + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(V0Error) + expect(isV0Error(caught, 'V0_PLUGIN_MISSING')).toBe(true) + if (isV0Error(caught, 'V0_PLUGIN_MISSING')) { + expect(caught.code).toBe('V0_PLUGIN_MISSING') + expect(caught.plugin).toBe('createDatePlugin') + } + }) }) describe('component lifecycle integration', () => { diff --git a/packages/0/src/composables/useDate/index.ts b/packages/0/src/composables/useDate/index.ts index dbb92b9ae..e17b98d95 100644 --- a/packages/0/src/composables/useDate/index.ts +++ b/packages/0/src/composables/useDate/index.ts @@ -45,13 +45,13 @@ import { createTrinity } from '#v0/composables/createTrinity' import { useLocale } from '#v0/composables/useLocale' // Utilities -import { instanceExists, isNullOrUndefined, isUndefined } from '#v0/utilities' +import { instanceExists, isNullOrUndefined, isUndefined, V0Error } from '#v0/utilities' import { computed, watchEffect, onScopeDispose } from 'vue' // Types import type { ContextTrinity } from '#v0/composables/createTrinity' import type { DateAdapter } from '#v0/composables/useDate/adapters' -import type { ID, V0ErrorCause } from '#v0/types' +import type { ID } from '#v0/types' import type { App, ComputedRef, Ref } from 'vue' // Exports @@ -329,14 +329,15 @@ export function useDate< E extends DateContext = DateContext, > (namespace = 'v0:date'): E { if (!instanceExists()) { - throw new Error( + throw new V0Error( '[v0] useDate() must be called inside a Vue component with createDatePlugin installed.\n\n' + 'Example:\n' + ' import { V0DateAdapter } from \'@vuetify/v0/date\'\n' + ' import { createDatePlugin } from \'@vuetify/v0\'\n\n' + ' app.use(createDatePlugin({ adapter: new V0DateAdapter() }))', { - cause: { code: 'V0_PLUGIN_MISSING', plugin: 'createDatePlugin' } satisfies V0ErrorCause, + code: 'V0_PLUGIN_MISSING', + plugin: 'createDatePlugin', }, ) } diff --git a/packages/0/src/index.ts b/packages/0/src/index.ts index fdb445c93..249a31779 100644 --- a/packages/0/src/index.ts +++ b/packages/0/src/index.ts @@ -1,5 +1,5 @@ export * from './components' export * from './composables' export * from './constants' -export type { ID } from './types' +export type { ID, V0ErrorCode, V0ErrorDetails } from './types' export * from './utilities' diff --git a/packages/0/src/types/index.ts b/packages/0/src/types/index.ts index 2117c8da9..d26419fcf 100644 --- a/packages/0/src/types/index.ts +++ b/packages/0/src/types/index.ts @@ -137,44 +137,47 @@ export type Extensible = T | (string & {}) export type Activation = 'automatic' | 'manual' /** - * Discriminated union of all `Error.cause` payloads thrown by v0 + * Discriminated union of structured details attached to every v0-thrown error * * @remarks - * Every error v0 throws attaches a structured `cause` with a `code` - * discriminant so consumers (devtools panels, error overlays, error trackers) - * can identify the error stably without parsing the message string. + * Each arm pairs a stable `code` discriminant with the domain context for that + * code. Consumed by the `V0Error` constructor in `#v0/utilities` — the union + * is the source of truth for both what codes exist and what payload each code + * carries. * - * When authoring a new v0 throw, append the literal with - * `satisfies V0ErrorCause` so TypeScript verifies the discriminant and the - * payload shape at the call site. + * Prefer `isV0Error(err, code)` over manual narrowing — see + * `V0Error` and `isV0Error` in `#v0/utilities` for the consumer-facing API + * and worked examples. + * + * Inspiration: tRPC's `TRPCError.code`, Node's `error.code` registry. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause + * @see https://nodejs.org/api/errors.html#nodejs-error-codes + * @see https://trpc.io/docs/server/error-handling * * @example * ```ts - * try { - * useContext(myKey) - * } catch (err) { - * if ( - * err instanceof Error - * && err.cause - * && typeof err.cause === 'object' - * && 'code' in err.cause - * ) { - * const cause = err.cause as V0ErrorCause - * if (cause.code === 'V0_CONTEXT_MISSING') { - * console.log('missing context key:', cause.key) - * } - * } - * } + * const details: V0ErrorDetails = { code: 'V0_CONTEXT_MISSING', key: 'theme' } * ``` */ -export type V0ErrorCause = +export type V0ErrorDetails = | { code: 'V0_CONTEXT_MISSING', key: string | symbol } | { code: 'V0_PLUGIN_MISSING', plugin: string } /** - * Union of all error codes thrown by v0 + * Union of every error code thrown by v0 * * @remarks - * Convenience alias for the discriminant field of {@link V0ErrorCause}. + * Convenience alias for the discriminant field of {@link V0ErrorDetails}. + * + * @example + * ```ts + * function describe (code: V0ErrorCode): string { + * switch (code) { + * case 'V0_CONTEXT_MISSING': return 'missing provider' + * case 'V0_PLUGIN_MISSING': return 'plugin not installed' + * } + * } + * ``` */ -export type V0ErrorCode = V0ErrorCause['code'] +export type V0ErrorCode = V0ErrorDetails['code'] diff --git a/packages/0/src/utilities/errors.ts b/packages/0/src/utilities/errors.ts new file mode 100644 index 000000000..0a69e03f7 --- /dev/null +++ b/packages/0/src/utilities/errors.ts @@ -0,0 +1,117 @@ +/** + * @module utilities/errors + * + * @remarks + * Structured error class thrown by v0 internals. Pattern modeled on + * tRPC's `TRPCError` and Node's built-in `Error` codes — `code` is a + * top-level discriminant field on the error, *not* a payload nested + * inside `Error.cause`. `cause` stays reserved for wrapping the + * genuine upstream error so error trackers (Sentry's `LinkedErrors`, + * Datadog RUM, Rollbar) render the chain correctly. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause + * @see https://nodejs.org/api/errors.html#nodejs-error-codes + * @see https://trpc.io/docs/server/error-handling + * @see https://docs.sentry.io/platforms/javascript/configuration/integrations/linkederrors/ + */ + +// Types +import type { V0ErrorCode, V0ErrorDetails } from '#v0/types' + +/** + * Structured error thrown by v0 internals. + * + * @remarks + * Carries a stable {@link V0ErrorCode} discriminant on `code` so + * consumers, devtools panels, and error trackers can identify the + * error without parsing the message string. Domain context lives in + * top-level fields per code (`key` for `V0_CONTEXT_MISSING`, `plugin` + * for `V0_PLUGIN_MISSING`), populated from a {@link V0ErrorDetails} + * argument and merged onto the instance at construction time. + * + * `cause` is forwarded to `Error`'s standard `ErrorOptions.cause` slot + * and is intended to carry a wrapped upstream error, not metadata. + * + * Prefer {@link isV0Error} over manual `instanceof` checks at the + * narrowing site — the guard intersects the instance type with the + * matching {@link V0ErrorDetails} arm, so per-code fields are typed. + * + * @example Author site + * ```ts + * throw new V0Error('Context not found.', { code: 'V0_CONTEXT_MISSING', key }) + * ``` + * + * @example Author site with upstream wrap + * ```ts + * try { + * JSON.parse(raw) + * } catch (err) { + * throw new V0Error('Failed to hydrate registry.', { + * code: 'V0_CONTEXT_MISSING', + * key: 'v0:registry', + * cause: err, + * }) + * } + * ``` + * + * @example Consumer site + * ```ts + * try { + * useContext(myKey) + * } catch (err) { + * if (isV0Error(err, 'V0_CONTEXT_MISSING')) { + * console.log(err.key) + * } + * } + * ``` + */ +export class V0Error extends Error { + override name = 'V0Error' + readonly code: V0ErrorCode + readonly key?: string | symbol + readonly plugin?: string + + constructor (message: string, details: V0ErrorDetails & { cause?: unknown }) { + const { cause, ...rest } = details + super(message, cause === undefined ? undefined : { cause }) + this.code = details.code + Object.assign(this, rest) + } +} + +/* #__NO_SIDE_EFFECTS__ */ +/** + * Type guard for v0-thrown errors. + * + * @param value The value to test. + * @param code Optional discriminant — narrow the result to a specific + * {@link V0ErrorCode} so per-code fields become required. + * + * @remarks + * Without the optional `code` argument, the guard verifies the value + * is a {@link V0Error} instance. With the `code` argument, the guard + * also narrows the instance type to the intersection with the matching + * {@link V0ErrorDetails} arm — fields like `key` or `plugin` change + * from optional to required on the narrowed type. + * + * @example Narrow to any v0 error + * ```ts + * if (isV0Error(err)) { + * console.log(err.code) + * } + * ``` + * + * @example Narrow to a specific code + * ```ts + * if (isV0Error(err, 'V0_CONTEXT_MISSING')) { + * console.log(err.key) // typed as string | symbol, not string | symbol | undefined + * } + * ``` + */ +export function isV0Error ( + value: unknown, + code?: C, +): value is V0Error & Extract { + if (!(value instanceof V0Error)) return false + return code === undefined || value.code === code +} diff --git a/packages/0/src/utilities/index.ts b/packages/0/src/utilities/index.ts index 691325936..1bbad6154 100644 --- a/packages/0/src/utilities/index.ts +++ b/packages/0/src/utilities/index.ts @@ -1,5 +1,6 @@ // Utilities export * from './apca' export * from './color' +export * from './errors' export * from './helpers' export * from './instance' From eb34d6e4a6b7d03283eafeac7f3f40844bd716eb Mon Sep 17 00:00:00 2001 From: John Leider Date: Thu, 28 May 2026 12:25:01 -0500 Subject: [PATCH 4/5] refactor(types): use isUndefined helper in V0Error / isV0Error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comply with the project rule that type guards from #v0/utilities replace raw === undefined comparisons (style.md, PHILOSOPHY §2.3). Same-directory sibling import (./helpers) rather than the #v0/utilities barrel to avoid a circular dependency through utilities/index.ts. --- packages/0/src/utilities/errors.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/0/src/utilities/errors.ts b/packages/0/src/utilities/errors.ts index 0a69e03f7..7a74d1583 100644 --- a/packages/0/src/utilities/errors.ts +++ b/packages/0/src/utilities/errors.ts @@ -15,6 +15,9 @@ * @see https://docs.sentry.io/platforms/javascript/configuration/integrations/linkederrors/ */ +// Utilities +import { isUndefined } from './helpers' + // Types import type { V0ErrorCode, V0ErrorDetails } from '#v0/types' @@ -73,7 +76,7 @@ export class V0Error extends Error { constructor (message: string, details: V0ErrorDetails & { cause?: unknown }) { const { cause, ...rest } = details - super(message, cause === undefined ? undefined : { cause }) + super(message, isUndefined(cause) ? undefined : { cause }) this.code = details.code Object.assign(this, rest) } @@ -113,5 +116,5 @@ export function isV0Error ( code?: C, ): value is V0Error & Extract { if (!(value instanceof V0Error)) return false - return code === undefined || value.code === code + return isUndefined(code) || value.code === code } From 27f8130021efa9f9a4f5d6be0104868c8ad0d4cf Mon Sep 17 00:00:00 2001 From: John Leider Date: Thu, 28 May 2026 12:31:29 -0500 Subject: [PATCH 5/5] refactor(types): extend V0Error to palette generators and logger adapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweep the remaining bare `throw new Error(...)` sites in packages/0/src so isV0Error(err) holds for every error v0 throws. Adds three V0ErrorDetails arms: - V0_PALETTE_INVALID_SEED → ant, leonardo, material seed validation - V0_PALETTE_UNKNOWN_VARIANT → material variant lookup - V0_ADAPTER_INSTANCE_MISSING → Pino/Consola logger adapter constructors Pattern is now uniform: 8/8 throw sites in packages/0/src use V0Error with a code drawn from the typed registry. The only bare Errors that remain are in node_modules / third-party APIs we don't own. --- .../src/composables/useLogger/adapters/consola.ts | 8 +++++++- .../0/src/composables/useLogger/adapters/pino.ts | 7 +++++-- packages/0/src/palettes/ant/generate/index.ts | 9 ++++++++- .../0/src/palettes/leonardo/generate/index.ts | 8 ++++++-- .../0/src/palettes/material/generate/index.ts | 15 +++++++++++++-- packages/0/src/types/index.ts | 3 +++ packages/0/src/utilities/errors.ts | 4 ++++ 7 files changed, 46 insertions(+), 8 deletions(-) diff --git a/packages/0/src/composables/useLogger/adapters/consola.ts b/packages/0/src/composables/useLogger/adapters/consola.ts index cc668a77e..6827db097 100644 --- a/packages/0/src/composables/useLogger/adapters/consola.ts +++ b/packages/0/src/composables/useLogger/adapters/consola.ts @@ -1,13 +1,19 @@ // Adapters import { LoggerAdapter } from './adapter' +// Utilities +import { V0Error } from '#v0/utilities' + export class ConsolaLoggerAdapter extends LoggerAdapter { private consola: LoggerAdapter constructor (consolaInstance: LoggerAdapter | null | undefined) { super() if (!consolaInstance) { - throw new Error('Consola instance is required for ConsolaLoggerAdapter') + throw new V0Error('Consola instance is required for ConsolaLoggerAdapter', { + code: 'V0_ADAPTER_INSTANCE_MISSING', + adapter: 'ConsolaLoggerAdapter', + }) } this.consola = consolaInstance } diff --git a/packages/0/src/composables/useLogger/adapters/pino.ts b/packages/0/src/composables/useLogger/adapters/pino.ts index 7141a303d..f18d478da 100644 --- a/packages/0/src/composables/useLogger/adapters/pino.ts +++ b/packages/0/src/composables/useLogger/adapters/pino.ts @@ -2,7 +2,7 @@ import { LoggerAdapter } from './adapter' // Utilities -import { isObject } from '#v0/utilities' +import { isObject, V0Error } from '#v0/utilities' /** * Pino logger adapter implementation @@ -26,7 +26,10 @@ export class PinoLoggerAdapter extends LoggerAdapter { constructor (pinoInstance: PinoInstance | null | undefined) { super() if (!pinoInstance) { - throw new Error('Pino instance is required for PinoLoggerAdapter') + throw new V0Error('Pino instance is required for PinoLoggerAdapter', { + code: 'V0_ADAPTER_INSTANCE_MISSING', + adapter: 'PinoLoggerAdapter', + }) } this.pino = pinoInstance } diff --git a/packages/0/src/palettes/ant/generate/index.ts b/packages/0/src/palettes/ant/generate/index.ts index 22fb0e28a..39e24dc15 100644 --- a/packages/0/src/palettes/ant/generate/index.ts +++ b/packages/0/src/palettes/ant/generate/index.ts @@ -1,5 +1,8 @@ import { generate } from '@ant-design/colors' +// Utilities +import { V0Error } from '#v0/utilities' + // Types import type { PaletteDefinition } from '#v0/palettes' @@ -30,7 +33,11 @@ function contrast (hex: string): string { /* #__NO_SIDE_EFFECTS__ */ export function ant (seed: string, options: AntGenerateOptions = {}): PaletteDefinition { if (!HEX_RE.test(seed)) { - throw new Error(`[@vuetify/v0] Invalid seed color: "${seed}". Expected a hex string (e.g., "#1677ff").`) + throw new V0Error(`[@vuetify/v0] Invalid seed color: "${seed}". Expected a hex string (e.g., "#1677ff").`, { + code: 'V0_PALETTE_INVALID_SEED', + palette: 'ant', + seed, + }) } const { background = '#141414' } = options diff --git a/packages/0/src/palettes/leonardo/generate/index.ts b/packages/0/src/palettes/leonardo/generate/index.ts index 87d1d1aa8..1fa1f1903 100644 --- a/packages/0/src/palettes/leonardo/generate/index.ts +++ b/packages/0/src/palettes/leonardo/generate/index.ts @@ -1,7 +1,7 @@ import { Color, Theme } from '@adobe/leonardo-contrast-colors' // Utilities -import { isArray, isObject } from '#v0/utilities' +import { isArray, isObject, V0Error } from '#v0/utilities' // Types import type { PaletteDefinition } from '#v0/palettes' @@ -77,7 +77,11 @@ function mapSemanticColors (colors: Record, ratios: number[]): R /* #__NO_SIDE_EFFECTS__ */ export function leonardo (seed: string, options: LeonardoGenerateOptions = {}): PaletteDefinition { if (!HEX_RE.test(seed)) { - throw new Error(`[@vuetify/v0] Invalid seed color: "${seed}". Expected a hex string (e.g., "#0ea5e9").`) + throw new V0Error(`[@vuetify/v0] Invalid seed color: "${seed}". Expected a hex string (e.g., "#0ea5e9").`, { + code: 'V0_PALETTE_INVALID_SEED', + palette: 'leonardo', + seed, + }) } const { ratios = DEFAULT_RATIOS, colorSpace = 'OKLCH' } = options diff --git a/packages/0/src/palettes/material/generate/index.ts b/packages/0/src/palettes/material/generate/index.ts index d684cd03a..10d30cf1d 100644 --- a/packages/0/src/palettes/material/generate/index.ts +++ b/packages/0/src/palettes/material/generate/index.ts @@ -10,6 +10,9 @@ import { hexFromArgb, } from '@material/material-color-utilities' +// Utilities +import { V0Error } from '#v0/utilities' + // Types import type { PaletteDefinition } from '#v0/palettes' import type { TonalPalette } from '@material/material-color-utilities' @@ -74,7 +77,11 @@ function extractSchemeColors (scheme: InstanceType): Rec /* #__NO_SIDE_EFFECTS__ */ export function material (seed: string, options: MaterialGenerateOptions = {}): PaletteDefinition { if (!HEX_RE.test(seed)) { - throw new Error(`[@vuetify/v0] Invalid seed color: "${seed}". Expected a hex string (e.g., "#6750A4").`) + throw new V0Error(`[@vuetify/v0] Invalid seed color: "${seed}". Expected a hex string (e.g., "#6750A4").`, { + code: 'V0_PALETTE_INVALID_SEED', + palette: 'material', + seed, + }) } const { variant = 'tonalSpot', contrast = 0 } = options @@ -83,7 +90,11 @@ export function material (seed: string, options: MaterialGenerateOptions = {}): const SchemeClass = VARIANTS[variant] if (!SchemeClass) { - throw new Error(`[@vuetify/v0] Unknown material variant: "${variant}"`) + throw new V0Error(`[@vuetify/v0] Unknown material variant: "${variant}"`, { + code: 'V0_PALETTE_UNKNOWN_VARIANT', + palette: 'material', + variant, + }) } const light = new SchemeClass(hct, false, contrast) diff --git a/packages/0/src/types/index.ts b/packages/0/src/types/index.ts index d26419fcf..eaea818fd 100644 --- a/packages/0/src/types/index.ts +++ b/packages/0/src/types/index.ts @@ -163,6 +163,9 @@ export type Activation = 'automatic' | 'manual' export type V0ErrorDetails = | { code: 'V0_CONTEXT_MISSING', key: string | symbol } | { code: 'V0_PLUGIN_MISSING', plugin: string } + | { code: 'V0_PALETTE_INVALID_SEED', palette: 'material' | 'leonardo' | 'ant', seed: string } + | { code: 'V0_PALETTE_UNKNOWN_VARIANT', palette: 'material', variant: string } + | { code: 'V0_ADAPTER_INSTANCE_MISSING', adapter: string } /** * Union of every error code thrown by v0 diff --git a/packages/0/src/utilities/errors.ts b/packages/0/src/utilities/errors.ts index 7a74d1583..ca7cd387f 100644 --- a/packages/0/src/utilities/errors.ts +++ b/packages/0/src/utilities/errors.ts @@ -73,6 +73,10 @@ export class V0Error extends Error { readonly code: V0ErrorCode readonly key?: string | symbol readonly plugin?: string + readonly palette?: string + readonly seed?: string + readonly variant?: string + readonly adapter?: string constructor (message: string, details: V0ErrorDetails & { cause?: unknown }) { const { cause, ...rest } = details