Skip to content

The core package responsible for type definitions, sheet data parsing, variable substitution, and validation of the error content JSON DSL. Has zero external dependencies.

Installation

bash
pnpm add @sanghyuk-2i/huh-core
bash
npm install @sanghyuk-2i/huh-core
bash
yarn add @sanghyuk-2i/huh-core

CDN

html
<!-- unpkg -->
<script src="https://unpkg.com/@sanghyuk-2i/huh-core"></script>

<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@sanghyuk-2i/huh-core"></script>

<!-- Pin version -->
<script src="https://unpkg.com/@sanghyuk-2i/huh-core@0.1.0"></script>

When loaded via CDN, all APIs are available through the window.HuhCore global variable:

js
const { resolveError, parseSheetData, validateConfig, renderTemplate } = HuhCore;

Types

ERROR_TYPES (constant)

Uppercase constants for built-in error types.

ts
const ERROR_TYPES = {
  TOAST: 'TOAST',
  MODAL: 'MODAL',
  PAGE: 'PAGE',
} as const;

ACTION_TYPES (constant)

Uppercase constants for built-in action types.

ts
const ACTION_TYPES = {
  REDIRECT: 'REDIRECT',
  RETRY: 'RETRY',
  BACK: 'BACK',
  DISMISS: 'DISMISS',
} as const;

SEVERITY_LEVELS (constant)

Uppercase constants for built-in severity levels.

ts
const SEVERITY_LEVELS = {
  INFO: 'INFO',
  WARNING: 'WARNING',
  ERROR: 'ERROR',
  CRITICAL: 'CRITICAL',
} as const;

ErrorType

An extensible string type that allows built-in types + custom types. Values are managed in uppercase.

ts
type BuiltInErrorType = 'TOAST' | 'MODAL' | 'PAGE';
type ErrorType = BuiltInErrorType | (string & {}); // Freely extensible: 'BANNER', 'SNACKBAR', etc.

ActionType

Allows built-in action types + custom action types.

ts
type BuiltInActionType = 'REDIRECT' | 'RETRY' | 'BACK' | 'DISMISS';
type ActionType = BuiltInActionType | (string & {}); // Freely extensible: 'OPEN_CHAT', 'SHARE', etc.

Severity

Allows built-in severity levels + custom severity levels.

ts
type BuiltInSeverity = 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';
type Severity = BuiltInSeverity | (string & {}); // Freely extensible custom severity

ErrorAction

ts
interface ErrorAction {
  label: string; // Action button text
  type: ActionType; // Action kind
  target?: string; // URL to navigate to for redirect
}

ErrorEntry

Represents a single error entry.

ts
interface ErrorEntry {
  type: ErrorType; // Error display type
  message: string; // Error message (may contain template variables)
  title?: string; // Title (for modal, page)
  image?: string; // Image URL (for page)
  severity?: Severity; // Severity level
  action?: ErrorAction; // User action
}

ErrorConfig

The complete error configuration. A map of ErrorEntry keyed by trackId.

ts
type ErrorConfig = Record<string, ErrorEntry>;
json
{
  "ERR_LOGIN_FAILED": {
    "type": "TOAST",
    "message": "Login failed"
  },
  "ERR_NOT_FOUND": {
    "type": "PAGE",
    "message": "Page not found",
    "title": "404"
  },
  "ERR_MAINTENANCE": {
    "type": "BANNER",
    "message": "Server is under maintenance"
  }
}

ResolvedError

The return type of the resolveError function. An ErrorEntry with trackId added.

ts
type ResolvedError = ErrorEntry & {
  trackId: string;
}

LocalizedErrorConfig

Multi-language error configuration. A map of ErrorConfig keyed by locale code.

ts
type LocalizedErrorConfig = Record<string, ErrorConfig>;
ts
const locales: LocalizedErrorConfig = {
  ko: {
    ERR_AUTH: { type: 'MODAL', message: 'Session expired', title: 'Auth Error' },
  },
  en: {
    ERR_AUTH: { type: 'MODAL', message: 'Session expired', title: 'Auth Error' },
  },
};

CrossLocaleValidationResult

The return type of the validateLocales function.

ts
interface CrossLocaleValidationResult {
  valid: boolean; // true if no errors
  errors: CrossLocaleValidationError[]; // Error list (missing trackId, etc.)
  warnings: CrossLocaleValidationError[]; // Warning list (type/actionType mismatch, etc.)
}

interface CrossLocaleValidationError {
  trackId: string;
  field?: string;
  locales: string[]; // List of locales with the issue
  message: string;
}

ValidationResult

ts
interface ValidationResult {
  valid: boolean; // true if no errors
  errors: ValidationError[]; // Error list (missing type, invalid values, etc.)
  warnings: ValidationError[]; // Warning list (title used on toast, etc.)
}

interface ValidationError {
  trackId?: string;
  field?: string;
  message: string;
}

HuhPlugin

Defines a plugin that hooks into the error and action lifecycle.

ts
interface HuhPlugin {
  name: string;
  onError?: (error: ResolvedError, context: HuhErrorContext) => void;
  onAction?: (error: ResolvedError, action: ErrorAction) => void;
}
  • name — Plugin identifier (used in warning messages)
  • onError — Called when huh resolves an error
  • onAction — Called when a user triggers an action

HuhErrorContext

Context passed to the onError plugin hook.

ts
interface HuhErrorContext {
  trackId: string;
  variables?: Record<string, string>;
  locale?: string;
  severity?: Severity;
}

Functions

parseSheetData(rows: string[][]): ErrorConfig

Converts raw data from a data source (header + data row array) into an ErrorConfig. All sources (Google Sheets, Airtable, Notion, CSV, XLSX) use the same 2D string array format.

rows string[][] (required)

2D string array where the first row is the header.

Throws

  • Error if fewer than 2 rows
  • Error if trackId, type, message headers are missing
  • Error if type is empty

TIP

type and actionType values are automatically converted to uppercase. Whether you enter toast, Toast, or TOAST in the data source, it will be processed as TOAST. severity values are also automatically converted to uppercase. Custom types (BANNER, SNACKBAR, etc.) can be freely used in addition to built-in types.

Example

ts
import { parseSheetData } from '@sanghyuk-2i/huh-core';

const rows = [
  [
    'trackId',
    'type',
    'message',
    'title',
    'image',
    'severity',
    'actionLabel',
    'actionType',
    'actionTarget',
  ],
  ['ERR_001', 'toast', 'An error occurred', '', '', '', '', ''],
  ['ERR_002', 'modal', 'Session expired', 'Notice', '', 'OK', 'dismiss', ''],
  ['ERR_003', 'banner', 'Server under maintenance', '', '', 'Contact Us', 'open_chat', ''],
];

const config = parseSheetData(rows);
// {
//   ERR_001: { type: 'TOAST', message: 'An error occurred' },
//   ERR_002: { type: 'MODAL', message: 'Session expired', title: 'Notice', action: { label: 'OK', type: 'DISMISS' } },
//   ERR_003: { type: 'BANNER', message: 'Server under maintenance', action: { label: 'Contact Us', type: 'OPEN_CHAT' } }
// }

resolveError(config, trackId, variables?): ResolvedError

Looks up an error by trackId and substitutes template variables in the message/title/action.

config ErrorConfig (required)

Error configuration object.

trackId string (required)

Error ID to look up.

variables Record<string, string>

Template variables to substitute (optional).

Throws

  • Unknown trackId error if trackId is not found in config

Example

ts
import { resolveError } from '@sanghyuk-2i/huh-core';

const config = {
  ERR_SESSION: {
    type: 'MODAL',
    message: "{{userName}}'s session has expired",
    title: '{{userName}}',
    action: { label: 'Login Again', type: 'REDIRECT', target: '/login' },
  },
};

const resolved = resolveError(config, 'ERR_SESSION', { userName: 'Jane' });
// resolved.message -> "Jane's session has expired"
// resolved.title   -> "Jane"
// resolved.trackId -> "ERR_SESSION"

renderTemplate(template, variables): string

Substitutes placeholders in a string. Unmatched variables are left as-is.

template string (required)

Template string containing placeholders.

variables Record<string, string> (required)

Variable map to substitute.

Example

ts
import { renderTemplate } from '@sanghyuk-2i/huh-core';

renderTemplate('Hello {{name}}!', { name: 'Jane' });
// -> "Hello Jane!"

renderTemplate('{{a}} and {{b}}', { a: 'Hello' });
// -> "Hello and {{b}}"  (unmatched variables are preserved)

validateConfig(config): ValidationResult

Validates an ErrorConfig.

Validation Rules (errors)

ConditionMessage
type is emptyMissing required field: type
message is emptyMissing required field: message
action.label is emptyAction is missing required field: label
REDIRECT type without targetAction type "REDIRECT" requires a target URL

TIP

Custom types (BANNER, SNACKBAR, etc.) pass without errors. The validator does not check the type value itself -- it only raises an error when the value is empty.

Warning Rules (warnings)

Warnings are only generated for built-in types. Custom types do not trigger warnings.

ConditionMessage
Config is emptyConfig is empty
TOAST has titleToast errors typically do not display a title
TOAST has imageToast errors typically do not display an image
PAGE has no actionPage errors should provide an action
Unrecognized severity valueUnrecognized severity "X". Built-in levels: INFO, WARNING, ERROR, CRITICAL

Example

ts
import { validateConfig } from '@sanghyuk-2i/huh-core';

const result = validateConfig({
  ERR_001: { type: 'TOAST', message: '' },
  ERR_002: { type: 'MODAL', message: 'OK', action: { label: 'Go', type: 'REDIRECT' } },
  ERR_003: { type: 'BANNER', message: 'Custom type works' }, // Custom type — passes without errors
});

// result.valid -> false
// result.errors -> [
//   { trackId: 'ERR_001', field: 'message', message: 'Missing required field: message' },
//   { trackId: 'ERR_002', field: 'action.target', message: 'Action type "REDIRECT" requires a target URL' },
// ]

validateLocales(locales): CrossLocaleValidationResult

Validates cross-locale consistency of multi-language error configurations.

locales LocalizedErrorConfig (required)

Map of error configurations keyed by locale code.

Validation Rules (errors)

ConditionMessage
A trackId in one locale is missing in anothertrackId "XXX" is missing in locale(s): ...

Warning Rules (warnings)

ConditionMessage
Same trackId has different typestrackId "XXX" has different "type" across locales
Same trackId has different action typestrackId "XXX" has different "action.type" across locales

TIP

If there is only one locale, validation is skipped and returns valid: true.

Example

ts
import { validateLocales } from '@sanghyuk-2i/huh-core';

const result = validateLocales({
  ko: {
    ERR_AUTH: { type: 'MODAL', message: 'Session expired' },
    ERR_NETWORK: { type: 'TOAST', message: 'Network error' },
  },
  en: {
    ERR_AUTH: { type: 'MODAL', message: 'Session expired' },
    // ERR_NETWORK missing -> error
  },
});

// result.valid -> false
// result.errors -> [{ trackId: 'ERR_NETWORK', locales: ['en'], message: '...' }]

runPluginHook(plugins, hook, ...args): void

Executes a specific hook on all plugins, catching and warning on any errors thrown by individual plugins.

plugins HuhPlugin[] (required)

Array of plugins to execute.

hook 'onError' | 'onAction' (required)

The hook name to execute.

args Parameters (required)

Arguments to pass to the hook function.

Example

ts
import { runPluginHook } from '@sanghyuk-2i/huh-core';
import type { HuhPlugin } from '@sanghyuk-2i/huh-core';

const plugins: HuhPlugin[] = [myPlugin(), anotherPlugin()];

// Called internally by framework providers — typically not called directly
runPluginHook(plugins, 'onError', resolvedError, { trackId: 'ERR_001' });
runPluginHook(plugins, 'onAction', resolvedError, action);

Released under the MIT License.