@arkstack/common

中文文档
0.22.2发布于 2026/11/07ES Module近 30 天下载 5224

Core utilities, primitives, and shared infrastructure for the Arkstack ecosystem.

原始使用文档
文档目录17 节
  1. Table of Contents
  2. Installation
  3. Modules
  4. System
  5. Logger
  6. ErrorHandler
  7. Exceptions
  8. Hook
  9. Encryption
  10. Hash
  11. Network
  12. Lifecycle
  13. Prototypes
  14. Global Augmentations
  15. Types
  16. Environment Variables Reference
  17. Helpers

@arkstack/common

@arkstack/common

Core utilities, primitives, and shared infrastructure for the Arkstack framework ecosystem. This package provides the building blocks used across all Arkstack packages — error handling, logging, hashing, encryption, lifecycle management, hooks, and more.


Table of Contents


Installation

pnpm add @arkstack/common

Modules

System

src/system.ts

Provides core application-level utilities for environment variable access, configuration loading, path resolution, and dynamic file importing.

env(key, defaultValue?)

Reads a value from process.env with automatic type coercion. Booleans (true, false, on, off), numbers, null, and empty strings are all handled gracefully.

import { env } from '@arkstack/common'

const port = env('PORT', 3000) // number
const debug = env('DEBUG', false) // boolean
const name = env('APP_NAME', 'App') // string

Type coercion rules:

Raw valueResolved type
"true" / "on"true
"false" / "off"false
Numeric stringnumber
"null"null
""undefined (falls back to defaultValue)

config(key?, defaultValue?)

Loads and merges all configuration files from the build output's config/ directory. Supports dot-path key access with full TypeScript inference.

import { config } from '@arkstack/common'

// Get the full config object
const allConfig = config()

// Access a nested key
const dbHost = config('database.host', 'localhost')

Config files are loaded from the resolved outputDir() using createRequire. Middleware config files are skipped when running in CLI context.


Builds a fully-qualified application URL from APP_URL and PORT environment variables.

import { appUrl } from '@arkstack/common'

appUrl() // "http://localhost:3000"
appUrl('/api/health') // "http://localhost:3000/api/health"

nodeEnv()

Returns 'dev' or 'prod' based on the NODE_ENV environment variable. Defaults to 'dev' for any unrecognised value.

import { nodeEnv } from '@arkstack/common'

nodeEnv() // "dev" | "prod"

outputDir(cwd?)

Resolves the build output directory. In development, defaults to .arkstack/build; in production, to dist. Both can be overridden via environment variables:

VariableContextDefault
OUTPUT_DIR_DEVDevelopment.arkstack/build
OUTPUT_DIRProductiondist

importFile<T>(filePath)

Dynamically imports a file using Jiti, with TypeScript and tsconfig path support. Useful for loading user-defined config or plugin files at runtime.

import { importFile } from '@arkstack/common'

const module = await importFile<{ default: MyConfig }>('./config/app.ts')

Logger

src/Logger.ts

A structured, chalk-powered console logger with verbosity control, two-column formatting, and a composable parsing API.

Basic log levels

import { Logger } from '@arkstack/common'

Logger.success('Server started')
Logger.info('Listening on port 3000')
Logger.warn('Deprecated option used')
Logger.error('Something went wrong', false) // false = don't exit
Logger.debug('Internal state dump') // only shown at verbosity >= 3

Each level uses a distinct icon and colour:

MethodIconColour
success✓Green
infoℹBlue
warn⚠Yellow
error✖Red
debug🐛Gray

The second argument for all level methods is exit (boolean). When true, the process exits after logging. error exits by default (exit = true); all others default to false.


Logger.configure(options)

Sets global verbosity and suppression behaviour.

Logger.configure({
  verbosity: 3, // enables debug output
  quiet: true, // suppresses info and success
  silent: true, // suppresses all output
})

Logger.twoColumnDetail(name, value, log?, spacer?)

Renders a right-aligned two-column layout padded to the terminal width.

Logger.twoColumnDetail('Route', 'GET /api/users')
// "Route ......................................... GET /api/users"

const row = Logger.twoColumnDetail('Route', 'GET /api/users', false)
// returns [name, dots, value] without printing

Logger.describe(name, desc, width?, log?)

Similar to twoColumnDetail, but uses a fixed width with space padding rather than dots. Useful for command help listings.

Logger.describe('--port', 'Port to listen on', 40)
// "--port                    Port to listen on"

Logger.split(name, value, status?, exit?, preserveCol?, spacer?)

Like twoColumnDetail, but wraps the left column with a coloured background badge based on status.

Logger.split('Database', 'Connected', 'success')
Logger.split('Migration', 'Failed', 'error', true) // exits after logging

Logger.parse(config, joiner?, log?, sc?)

Composes a styled string from a [text, chalkStyle] pair array.

Logger.parse(
  [
    ['Arkstack', 'bold'],
    ['v1.0.0', 'gray'],
  ],
  ' ',
) // "Arkstack v1.0.0"

// Return instead of print
const str = Logger.parse([['Ready', 'green']], ' ', false)

Logger.log(config, joiner?, log?, sc?)

A flexible polymorphic logger that accepts either a string + style, or a LoggerParseSignature array. Returns the Logger class when called with no arguments.

Logger.log('PORT:3000', 'cyan')
Logger.log(
  [
    ['PORT', 'bold'],
    ['3000', 'cyan'],
  ],
  ' ',
)

Logger.chalker(styles[])

Returns a function that applies a chain of chalk styles to any input.

const highlight = Logger.chalker(['bold', 'green'])
console.log(highlight('Ready'))

Logger.console()

Returns a Console-compatible object with log, debug, warn, info, and error methods. Can be used as a drop-in replacement for globalThis.console.

const console = Logger.console()
console.log('hello')
console.warn('watch out')

ErrorHandler

src/ErrorHandler.ts

A static utility class for normalising, serialising, classifying, and logging errors. Integrates with Pino for persistent error file logging.

ErrorHandler.createErrorPayload(err, fallbackMessage?)

The primary method. Converts any thrown value into a consistent ArkstackErrorPayload object, handling validation errors, model-not-found errors, and generic errors uniformly.

import { ErrorHandler } from '@arkstack/common'

try {
  // ...
} catch (err) {
  const payload = ErrorHandler.createErrorPayload(err, 'Request failed')
  // { status: 'error', code: 422, message: '...', errors: {...} }
}

Payload shape:

{
  status: 'error'
  code: number        // HTTP status code (100–599)
  message: string
  errors?: unknown    // present for validation errors
  stack?: string      // present in development unless HIDE_ERROR_STACK is set
}

Classification logic:

Error typecodeerrors populated
Validation error (has .errors)statusCode / status or 422Yes
Model not found (has .getModelName())404No
Generic errorstatusCode / status or 500Stack trace as object

ErrorHandler.serializeError(value, seen?)

Recursively serialises any value — including Error instances and circular references — into a plain JSON-safe object. Circular references are replaced with '[Circular]'.

const serialized = ErrorHandler.serializeError(new Error('oops'))
// { name: 'Error', message: 'oops', stack: '...' }

ErrorHandler.normalizeStatusCode(value, fallback?)

Ensures a status code is a valid integer in the range 100–599. Returns the fallback (default 500) for anything invalid.

ErrorHandler.normalizeStatusCode('422') // 422
ErrorHandler.normalizeStatusCode('xyz') // 500

ErrorHandler.getErrorLogger()

Returns a Pino logger instance that writes to storage/logs/error.log (created automatically). Instances are cached per destination path.


ErrorHandler.logUnhandledError(err, request, message)

Persists an unhandled error to the error log file, including the serialised error and the associated request context.

ErrorHandler.logUnhandledError(err, { method: 'GET', url: '/api' }, 'Unhandled exception')

Classification helpers

ErrorHandler.isValidationError(err) // true if err.errors is defined
ErrorHandler.isModelNotFoundError(err) // true if err.getModelName is a function
ErrorHandler.shouldLogError(err) // false for validation/model-not-found errors
ErrorHandler.shouldHideStack() // true if HIDE_ERROR_STACK env is set
ErrorHandler.getPrimaryError(err) // unwraps err.cause if present
ErrorHandler.toErrorShape(value) // casts unknown to ArkstackErrorShape if object

All static methods are also exported as named standalone functions for convenience:

import {
  createErrorPayload,
  isValidationError,
  serializeError,
  logUnhandledError,
  // ...
} from '@arkstack/common'

Exceptions

src/Exceptions/

A three-level exception hierarchy for structured error throwing.

Exception

Base class extending Error. Sets .name to 'Exception'.

import { Exception } from '@arkstack/common'

throw new Exception('Something went wrong')

AppException

Extends Exception. Adds statusCode (default 400) and an optional errors map for field-level validation errors.

import { AppException } from '@arkstack/common'

const err = new AppException('Validation failed', 422)
err.errors = { email: ['Email is required'] }

RequestException

Extends AppException. Intended for HTTP request-level errors. Provides two static assertion helpers:

RequestException.assertNotEmpty(value, message, code?)

Throws a RequestException if the value is null or undefined. Narrows the type on success.

import { RequestException } from '@arkstack/common'

const user = await User.find(id)
RequestException.assertNotEmpty(user, 'User not found', 404)
// user is now User (not null | undefined)

RequestException.abortIf(condition, message, code?)

Throws if the condition is truthy.

RequestException.abortIf(!user.isActive, 'Account is suspended', 403)

Hook

src/Hook.ts

Re-exports Hook from @arkstack/foundry; both imports are the same process-wide registry. See the @arkstack/foundry README for the API (on, emit, get, has, off, all, clear). Declare typed hook names by augmenting HookRegistry on @arkstack/foundry.


Encryption

src/utils/encryption.ts

A thin wrapper around @arkstack/encryption, bound to the application key. AES-256-GCM for sensitive values (e.g. two-factor authentication secrets); the package underneath runs on the Web Crypto API, so a value encrypted on the server can be decrypted in the browser and vice versa.

Encryption.encrypt(value, key?)

Encrypts a string. Returns a colon-delimited base64url string: <iv>:<authTag>:<ciphertext>.

import { Encryption } from '@arkstack/common'

const token = Encryption.encrypt('my-secret-value')
// "abc123:def456:ghi789"

Encryption.decrypt(payload, key?)

Decrypts a payload produced by encrypt. Throws if the format is invalid or the key is wrong.

const original = Encryption.decrypt(token)
// "my-secret-value"

Encryption.encryptAsync(value, key?, options?) / Encryption.decryptAsync(payload, key?, options?)

The same operations on the Web Crypto path, in the same payload format, so the two can be mixed freely. Use these when the calling code is (or may become) shared with the browser. options.aad binds additional authenticated data to the ciphertext.


Encryption.cipher(key?)

A Cipher bound to the application key, for encrypting many values without re-deriving the key, and for raw bytes via encryptBytes / decryptBytes.


Key utilities

Encryption.generateKey() // random base64url key
await Encryption.generateKeyPair() // { publicKey, privateKey } ECDH identity
await Encryption.deriveKey(password) // PBKDF2 → { key, salt, iterations }
await Encryption.compareKeys(left, right) // constant time
await Encryption.fingerprint() // displayable digest of the app key

End-to-end encryption

const channel = await Encryption.channel(myPrivateKey, peerPublicKey)

await channel.decrypt(await channel.encrypt('hey'))

await Encryption.seal('anonymous tip', peerPublicKey)
await Encryption.open(payload, myPrivateKey)

await Encryption.safetyNumber(myPublicKey, peerPublicKey)

The full @arkstack/encryption surface — Cipher, Codec, EncryptionKey, KeyPair, Keys, SealedBox, SecureChannel, NodeCipher — is re-exported from this package.

Environment variable:

VariableRequiredDescription
APP_KEYYesRaw secret; hashed to a 256-bit key internally via SHA-256

Generate one with ark key:generate. The legacy TWO_FACTOR_ENCRYPTION_KEY is still honored when APP_KEY is not set.


Hash

src/utils/hash.ts

Password hashing and OTP generation utilities.

Hash.make(value)

Hashes a string using bcrypt with a salt factor of 10.

import { Hash } from '@arkstack/common'

const hashed = await Hash.make('user-password')

Hash.verify(value, hashedValue)

Compares a plain-text value against a bcrypt hash.

const isValid = await Hash.verify('user-password', hashed)

Hash.otp(digits?, label?, period?)

Creates a TOTP instance using the otpauth library with SHA1 and a static secret. Suitable for simple time-based OTP flows.

const totp = Hash.otp(6, '[email protected]', 60)
const token = totp.generate()

Hash.totp(secret, label, issuer?, period?)

Creates a TOTP instance from a base32-encoded secret. Intended for user-specific TOTP (e.g. authenticator app integration).

const totp = Hash.totp(user.totpSecret, user.email)
const isValid = totp.validate({ token: userInput }) !== null

Network

src/network.ts

Utilities for starting an HTTP server with automatic port detection and rendering error views.

bootWithDetectedPort(boot, preferredPort?, app?)

Detects whether the preferred port is available (using detect-port) and boots the server on the first free port. Also initialises key globals: env, config, str, app, and arkctx.

import { bootWithDetectedPort } from '@arkstack/common'

await bootWithDetectedPort(
  async (port) => {
    server.listen(port)
    Logger.success(`Server:http://localhost:${port}`)
  },
  3000,
  appInstance,
)

Globals set:

GlobalValue
globalThis.app() => app
globalThis.envenv
globalThis.configconfig
globalThis.strstr (from @h3ravel/support)
globalThis.arkctx{ runtime: 'HTTP' }

renderError({ message, stack, title, code })

Renders an error page using the ~arkstack/common.error view template. Falls back to a human-readable title from a built-in status code map.

import { renderError } from '@arkstack/common'

const html = renderError({ code: 404, message: 'Page not found' })

Built-in status titles: 400, 401, 403, 404, 500, 502, 503, 504.


Lifecycle

src/lifecycle.ts

bindGracefulShutdown(shutdown)

Registers a cleanup callback for SIGINT, SIGTERM, and SIGQUIT signals, ensuring the application shuts down cleanly.

import { bindGracefulShutdown } from '@arkstack/common'

bindGracefulShutdown(async () => {
  await db.disconnect()
  Logger.info('Server shut down gracefully')
})

Prototypes

src/prototypes.ts

loadPrototypes()

Extends String.prototype with four utility methods. Call this once during application bootstrap.

import { loadPrototypes } from '@arkstack/common'

loadPrototypes()

Methods added:

MethodDescriptionExample
.titleCase()Converts to Title Case (handles _ and -)"hello_world".titleCase() → "Hello World"
.camelCase()Converts to camelCase"Hello World".camelCase() → "helloWorld"
.pascalCase()Converts to PascalCase"hello world".pascalCase() → "HelloWorld"
.truncate(len, suffix?)Truncates at word boundary"Hello World".truncate(7) → "Hello..."

Global Augmentations

src/app.d.ts

When loadPrototypes() is called and bootWithDetectedPort() initialises the runtime, the following globals and String prototype extensions are available throughout the application:

// Globals (set by bootWithDetectedPort)
globalThis.env // GlobalEnv — typed env() accessor
globalThis.config // GlobalConfig — typed config() accessor

// String prototype extensions (set by loadPrototypes)
'my_string'.titleCase()
'my_string'.camelCase()
'my_string'.pascalCase()
'my long string'.truncate(10, '…')

Types

src/types.ts

Key exported types from the package:

TypeDescription
GlobalEnvTyped signature for the env() function
GlobalConfigTyped signature for the config() function with dot-path support
ArkstackErrorShapeUnion of common error properties across frameworks (statusCode, status, errors, cause, etc.)
ArkstackErrorPayloadNormalised HTTP error response shape produced by ErrorHandler
LoggerChalkChalk style identifier(s) accepted by Logger methods
LoggerParseSignatureArray of [string, LoggerChalk] pairs for Logger.parse()
LoggerLogOverloaded function type for Logger.log()

Environment Variables Reference

VariableModuleRequiredDescription
PORTsystem, networkNoHTTP server port (default: 3000)
APP_URLsystemNoBase application URL
APP_NAMEhashNoApplication name used as TOTP issuer
NODE_ENVsystemNodevelopment or production
OUTPUT_DIRsystemNoProduction build output directory (default: dist)
OUTPUT_DIR_DEVsystemNoDevelopment build output directory (default: .arkstack/build)
TWO_FACTOR_ENCRYPTION_KEYencryptionYes*Secret key for AES-256-GCM encryption (*required only if using Encryption)
HIDE_ERROR_STACKErrorHandlerNoSet to true, 1, or on to suppress stack traces in error payloads

Helpers

src/utils/helpers.ts

perPage(query, defaults?)

Extracts a safe pagination limit from a query object. Clamps the result between 1 and 50 or the configured default maxPerPage, defaulting to 25 or the configured default perPage.

import { perPage } from '@arkstack/common'

const limit = perPage({ limit: 100 }) // 50 (clamped)
const limit2 = perPage({ limit: 100 }, { maxPerPage: 100 }) // 100
const limit3 = perPage({}) // 25 (default)
const limit3 = perPage({}, { perPage: 100 }) // 100

resolvePagination(query, defaults?)

Extracts the current page and a safe pagination limit from a query object. Clamps the resulting perPage between 1 and 50 or the configured default maxPerPage, defaulting to 25 or the configured default perPage.

import { resolvePagination } from '@arkstack/common'

const limit = resolvePagination({ limit: 100 }) // {perPage: 50, page: 1} (clamped)
const limi2 = resolvePagination({ limit: 100 }, { maxPerPage: 100 }) // {perPage: 100, page: 1}
const limit3 = resolvePagination({}) // {perPage: 50, page: 1} (default)
const limit4 = resolvePagination({}, { perPage: 100, page: 2 }) // {perPage: 100, page: 2}

getModel(modelName)

Dynamically imports an application model by name from the configured models directory (default: ./src/app/models). Supports augmenting ModelRegistry for type-safe lookups.

import { getModel } from '@arkstack/common'

const User = await getModel('User')
const users = await User.findAll()

// With type augmentation:
declare module '@arkstack/common' {
  interface ModelRegistry {
    User: typeof User
  }
}

const TypedUser = await getModel('User') // typeof User

版本与发布记录

@arkstack/common 共发布 238 个版本,首次发布于 2026/03/04;过去 12 个月发布了 238 个版本。

版本发布日期距今
0.22.2latest2026/11/07今天
0.22.12026/11/052 天前
0.22.02026/11/052 天前
0.21.12026/11/052 天前
0.21.02026/10/2810 天前
0.20.22026/10/2315 天前

关于 @arkstack/common 的常见问题

@arkstack/common 是什么?

Core utilities, primitives, and shared infrastructure for the Arkstack ecosystem.

如何安装 @arkstack/common?

在项目目录运行 npm install @arkstack/common,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@h3ravel/support(^2.2.7)、@arkstack/foundry(^0.22.2)、@arkstack/contract(^0.22.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@arkstack/common 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/arkstack__common 是否存在。

@arkstack/common 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@arkstack/common 可以商用吗?

@arkstack/common 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@arkstack/common 的下载量有多少?

根据 npm 官方下载统计,@arkstack/common 在 2026/10/07 – 2026/11/05 这 30 天内被下载 5,224 次,日均约 174 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@arkstack/common 还在维护吗?

最新版本 0.22.2 发布于 2026/11/07(今天);至今共发布 238 个版本,其中过去 12 个月发布了 238 个;首次发布于 2026/03/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。