Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/0/src/composables/createContext/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions packages/0/src/composables/createContext/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*/

// Utilities
import { isObject, isString, isSymbol, isUndefined } from '#v0/utilities'
import { isObject, isString, isSymbol, isUndefined, V0Error } from '#v0/utilities'
import { inject, provide } from 'vue'

// Types
Expand Down Expand Up @@ -64,7 +64,10 @@ export function useContext<Z> (key: ContextKey<Z>, defaultValue?: Z) {
const context = inject<Z>(key, defaultValue as Z)

if (isUndefined(context)) {
throw new Error(`Context "${String(key)}" not found. Ensure it's provided by an ancestor.`)
throw new V0Error(`Context "${String(key)}" not found. Ensure it's provided by an ancestor.`, {
code: 'V0_CONTEXT_MISSING',
key,
})
}

return context
Expand Down
18 changes: 18 additions & 0 deletions packages/0/src/composables/useDate/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/0/src/composables/useDate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ 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
Expand Down Expand Up @@ -329,12 +329,16 @@ export function useDate<
E extends DateContext<Z> = DateContext<Z>,
> (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() }))',
{
code: 'V0_PLUGIN_MISSING',
plugin: 'createDatePlugin',
},
)
}

Expand Down
8 changes: 7 additions & 1 deletion packages/0/src/composables/useLogger/adapters/consola.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
7 changes: 5 additions & 2 deletions packages/0/src/composables/useLogger/adapters/pino.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { LoggerAdapter } from './adapter'

// Utilities
import { isObject } from '#v0/utilities'
import { isObject, V0Error } from '#v0/utilities'

/**
* Pino logger adapter implementation
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion packages/0/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
9 changes: 8 additions & 1 deletion packages/0/src/palettes/ant/generate/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { generate } from '@ant-design/colors'

// Utilities
import { V0Error } from '#v0/utilities'

// Types
import type { PaletteDefinition } from '#v0/palettes'

Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/0/src/palettes/leonardo/generate/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -77,7 +77,11 @@ function mapSemanticColors (colors: Record<number, string>, 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
Expand Down
15 changes: 13 additions & 2 deletions packages/0/src/palettes/material/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -74,7 +77,11 @@ function extractSchemeColors (scheme: InstanceType<typeof SchemeTonalSpot>): 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
Expand All @@ -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)
Expand Down
49 changes: 49 additions & 0 deletions packages/0/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,52 @@ export type Extensible<T extends string> = T | (string & {})
* ```
*/
export type Activation = 'automatic' | 'manual'

/**
* Discriminated union of structured details attached to every v0-thrown error
*
* @remarks
* 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.
*
* 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
* const details: V0ErrorDetails = { code: 'V0_CONTEXT_MISSING', key: 'theme' }
* ```
*/
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
*
* @remarks
* 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 = V0ErrorDetails['code']
Loading
Loading