@ubean/shared 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -0
- package/README.zh-CN.md +255 -0
- package/dist/logger/index.js +1 -2
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# @ubean/shared
|
|
2
|
+
|
|
3
|
+
**English | [中文](./README.zh-CN.md)**
|
|
4
|
+
|
|
5
|
+
Shared protocol types, errors, environment handling and universal utilities for ubean.
|
|
6
|
+
|
|
7
|
+
`@ubean/shared` is the foundation leaf of the monorepo: every other `@ubean/*` package may depend on it, and it depends on **no** `@ubean/*` package. Its main entry stays browser-safe — Node-only helpers live behind the `/node` subpath, and the tslog-based logger behind `/logger`.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineEnv, createError, matchGlob } from '@ubean/shared';
|
|
11
|
+
import { logger } from '@ubean/shared/logger';
|
|
12
|
+
import { findAvailablePort } from '@ubean/shared/node';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Package Positioning](#package-positioning)
|
|
18
|
+
- [Install](#install)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Environment Variables](#environment-variables)
|
|
21
|
+
- [Errors](#errors)
|
|
22
|
+
- [Route Rules & Shared Types](#route-rules--shared-types)
|
|
23
|
+
- [Server Actions Shared Types](#server-actions-shared-types)
|
|
24
|
+
- [Glob Matching](#glob-matching)
|
|
25
|
+
- [Path & String Utilities](#path--string-utilities)
|
|
26
|
+
- [Logging](#logging)
|
|
27
|
+
- [Node Utilities](#node-utilities)
|
|
28
|
+
- [Type Safety](#type-safety)
|
|
29
|
+
- [Architecture Notes](#architecture-notes)
|
|
30
|
+
|
|
31
|
+
## Package Positioning
|
|
32
|
+
|
|
33
|
+
| What this package is responsible for | What it is not — owner instead |
|
|
34
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| Protocol **types** shared across packages: `PageHead`, `RouteMeta` / `BaseRouteMeta` / `RateLimitMeta` / `CacheMeta`, `RouteRule` / `IsrRule`, `Span*`, `UbeanEnv` / `UbeanContext` / `UbeanHandler` / `UbeanMiddleware` / `UbeanVariables` / `UbeanBindings` / `UbeanMiddlewareStep`, `GenericSchema` / `Input` | Page data protocol at runtime → `@ubean/pages`; handler runtime → `@ubean/routes`; SEO metadata construction → `@ubean/seo` |
|
|
36
|
+
| Dependency-free universal **utilities**: `normalizePath`, `getDirname`, `getBasename`, `getExtension`, `getStem`, `pathToTitle`, `capitalize`, `matchGlob`, `matchAnyGlob` | Route scanning → `@ubean/scan`; prerender link discovery → `@ubean/builder` |
|
|
37
|
+
| **Error primitives**: `UbeanError`, `createError`, `isUbeanError`, `errorToResponse` | Hono `onError` wiring and the `handleError` hook → `@ubean/app` |
|
|
38
|
+
| **Environment** helpers: `defineEnv`, `setRuntimeEnv`, `useRuntimeEnv`, `isNodeRuntime` + the `EnvSchema` / `InferEnvOutput` / `EnvValidationError` types | `ubean.config.ts` loading and module resolution → `@ubean/config` |
|
|
39
|
+
| **Logging** facade (`/logger`, `/logger/hono`): `createUbeanLogger` / `logger` / `getLogger` / `setMinLevel` / `setDebugLogging` and the Hono request-logger middleware, wrapping tslog@5 | Server-side request lifecycle, cache, and tracing → `@ubean/server`; DevTools log UI → `@ubean/devtools` |
|
|
40
|
+
| **Node-only** helpers (`/node`): port probing (`findAvailablePort`, `waitForPort`, `isPortReachable`, `sleep`), LAN address detection (`getLanAddresses`, `isLoopbackHost`, `isWildcardHost`), Vite config lookup (`findUserViteConfig`) | Process lifecycle, dev server bootstrap and CLI output → `@ubean/cli` / `@ubean/builder` |
|
|
41
|
+
|
|
42
|
+
**Runtime dependency whitelist (main entry): `@standard-schema/spec`, `hono` (type-only), `pathe`, `scule`** — no `tslog`, no `node:*`. The `/logger` subpath exists precisely so that tslog never enters the browser-safe main barrel; `/node` exists for the same reason with `node:net` / `node:os` / `node:fs`.
|
|
43
|
+
|
|
44
|
+
`@ubean/shared` declares **no peer dependencies** — nothing degrades when a peer is absent, because there is none.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm add @ubean/shared
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Transitively installed by every other `@ubean/*` package; add it directly when you only need the shared types or utilities.
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { UbeanError, createError, errorToResponse, defineEnv, isNodeRuntime, matchGlob } from '@ubean/shared';
|
|
58
|
+
|
|
59
|
+
// Typed error with a status code
|
|
60
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
61
|
+
|
|
62
|
+
// Turn it into a JSON Response (works for unknown errors too)
|
|
63
|
+
try {
|
|
64
|
+
await handler();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return errorToResponse(err);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// '**' matches any depth, '*' matches a single segment
|
|
70
|
+
matchGlob('/blog/a/b', '/blog/**'); // true
|
|
71
|
+
isNodeRuntime(); // false on Cloudflare Workers
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Environment Variables
|
|
75
|
+
|
|
76
|
+
`defineEnv(config)` validates a constructor-based schema and returns `{ env, validate }`. `server` keys are never exposed to the client; `public` keys are exposed via `import.meta.env`.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { defineEnv } from '@ubean/shared';
|
|
80
|
+
|
|
81
|
+
export const { env, validate } = defineEnv({
|
|
82
|
+
server: {
|
|
83
|
+
DATABASE_URL: { type: String, required: true },
|
|
84
|
+
PORT: { type: Number, default: 9527 }
|
|
85
|
+
},
|
|
86
|
+
public: {
|
|
87
|
+
APP_NAME: { type: String, default: 'ubean-app' }
|
|
88
|
+
},
|
|
89
|
+
mode: 'warn' // 'warn' logs; 'throw' rethrows on validation failure
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
env.PORT; // number
|
|
93
|
+
validate(); // { success, errors, data }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| API | Description |
|
|
97
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
98
|
+
| `defineEnv(config)` | Validate a schema and return `{ env, validate }` (`DefineEnvResult<S>`) |
|
|
99
|
+
| `setRuntimeEnv(env)` / `useRuntimeEnv(key, def)` | Merge / read runtime environment values (worker bindings, injected secrets) |
|
|
100
|
+
| `isNodeRuntime()` | True on Node / Bun / Deno (checked via `process.versions.node`, not `typeof process`) |
|
|
101
|
+
| `EnvSchema` / `InferEnvOutput<S>` | Schema map type and its inferred output type |
|
|
102
|
+
| `EnvValidationError` | `{ key, message, value }` for each failed entry |
|
|
103
|
+
|
|
104
|
+
`isNodeRuntime()` exists to gate Node-only capabilities behind a **dynamic** `import()`: bundlers keep statically imported `node:*` modules in the artifact, and workerd fails at module instantiation (`No such module "node:fs/promises"`) before any branch is evaluated.
|
|
105
|
+
|
|
106
|
+
## Errors
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { UbeanError, createError, isUbeanError, errorToResponse } from '@ubean/shared';
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
throw createError({ statusCode: 400, statusMessage: 'Bad Request', data: { field: 'email' } });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (isUbeanError(err)) {
|
|
115
|
+
err.statusCode; // 400
|
|
116
|
+
}
|
|
117
|
+
return errorToResponse(err); // JSON: { error, statusCode, data }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `UbeanError extends Error` carries `statusCode` / `statusMessage` / `data`; the default message comes from a status-code table (400/401/403/404/405/408/409/422/429/500/501/502/503), falling back to `'Error'`.
|
|
122
|
+
- `isUbeanError(err)` accepts both real instances and errors crossing a module boundary (checks `err.name === 'UbeanError'` plus a numeric `statusCode`).
|
|
123
|
+
- `errorToResponse(err)` renders a `UbeanError` with its own status code; any other error becomes `500` with `{ error: 'Internal Server Error', message }`.
|
|
124
|
+
|
|
125
|
+
## Route Rules & Shared Types
|
|
126
|
+
|
|
127
|
+
These types are the shared vocabulary of the routing, caching, and prerender layers. They live here to break cycles — `@ubean/routes` and `@ubean/config` both need them, and neither may depend on the other.
|
|
128
|
+
|
|
129
|
+
| Type | Shape / meaning |
|
|
130
|
+
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
131
|
+
| `BaseRouteMeta` | `{ requiresAuth?: boolean; [key: string]: unknown }` |
|
|
132
|
+
| `RateLimitMeta` / `CacheMeta` | `{ maxRequests, windowSeconds }` / `{ ttl, swr? }` |
|
|
133
|
+
| `RouteMeta extends BaseRouteMeta` | Adds `rateLimit` and `cache` |
|
|
134
|
+
| `IsrRule` | `{ ttl: number; swr?: boolean }` — the object form of a `routeRules.isr` entry |
|
|
135
|
+
| `RouteRule` | Per-route `redirect` / `rewrite` / `proxy` / `headers` / `cache` / `ssr` / `prerender` / `isr` / `ppr` |
|
|
136
|
+
| `PageHead` | Head metadata consumed by `@ubean/pages` and `@ubean/seo` |
|
|
137
|
+
| `Span`, `SpanOptions`, `SpanContext`, `SpanAttributes`, `SpanEvent`, `SpanEndOptions`, `SpanStatus` | Tracing primitives consumed by `@ubean/server` observability |
|
|
138
|
+
| `UbeanVariables` / `UbeanBindings` / `UbeanEnv` | Hono context variables, platform bindings, and the composed `UbeanEnv` |
|
|
139
|
+
| `UbeanContext` / `UbeanHandler` / `UbeanMiddleware` / `ComposedHandler` / `Input` / `GenericSchema` | Handler- and middleware-level types for `@ubean/routes` |
|
|
140
|
+
| `UbeanMiddlewareStep` | Union of middleware step names (`handle`, `requestId`, `securityHeaders`, `csrf`, `cacheStore`, `i18n`, `routeRules`, …) |
|
|
141
|
+
|
|
142
|
+
## Server Actions Shared Types
|
|
143
|
+
|
|
144
|
+
Server Actions are defined in `@ubean/routes`, but their wire types are shared so that the client runtime can consume them without importing server code.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { ACTION_BRAND, ActionError, fail, isServerAction, isActionFailure } from '@ubean/shared';
|
|
148
|
+
|
|
149
|
+
fail(400, { email: 'Invalid email' }); // { status: 400, errors: { email: '…' } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| API | Description |
|
|
153
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
154
|
+
| `ACTION_BRAND` | Well-known symbol (`Symbol.for('ubean.action')`) marking a `ServerAction` |
|
|
155
|
+
| `ServerAction<TInput, TOutput>` | Action descriptor; `ActionHandler` is its implementation signature |
|
|
156
|
+
| `ActionContext` / `ActionResult<T>` | Handler context and success envelope |
|
|
157
|
+
| `ActionFailure<T>` | `{ status, errors }` field-level failure envelope |
|
|
158
|
+
| `fail(status, errors)` | Build an `ActionFailure` |
|
|
159
|
+
| `ActionError` | User-readable error class with `code` / `status`, thrown from a handler |
|
|
160
|
+
| `isServerAction(value)` / `isActionFailure(value)` | Type guards |
|
|
161
|
+
| `ActionSchema<TOutput>` | Standard-Schema / `safeParse` input contract |
|
|
162
|
+
|
|
163
|
+
## Glob Matching
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { matchGlob, matchAnyGlob } from '@ubean/shared';
|
|
167
|
+
|
|
168
|
+
matchGlob('/blog', '/blog/**'); // true ('**' also matches the bare prefix)
|
|
169
|
+
matchGlob('/blog/a', '/blog/*'); // true ('*' is a single segment)
|
|
170
|
+
matchGlob('/blog/a/b', '/blog/*'); // false
|
|
171
|
+
matchAnyGlob('/admin/x', ['/blog/**', '/admin/**']); // true
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`matchGlob(route, pattern)` supports `**` (recursive), `*` (single segment), and literals; `pattern === '**'` or `'/**'` matches everything. It is the matcher behind SSR `exclude`, prerender `include`/`exclude`, and route-rule patterns.
|
|
175
|
+
|
|
176
|
+
## Path & String Utilities
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { normalizePath, getDirname, getBasename, getExtension, getStem, pathToTitle, capitalize } from '@ubean/shared';
|
|
180
|
+
|
|
181
|
+
normalizePath('//a\\b/c/'); // '/a/b/c'
|
|
182
|
+
getExtension('Post.MD'); // 'md'
|
|
183
|
+
getStem('Post.md'); // 'Post'
|
|
184
|
+
pathToTitle('blog/getting-started.md'); // 'Getting Started'
|
|
185
|
+
pathToTitle('blog/index.md'); // 'Blog' (index resolves to the parent directory name)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`pathToTitle` delegates to `scule`'s `kebabCase` plus `capitalize` — the title-case rule used by `@ubean/content` when a document has no explicit `title`.
|
|
189
|
+
|
|
190
|
+
## Logging
|
|
191
|
+
|
|
192
|
+
`/logger` is the only public logging entry. It is a **separate subpath on purpose**: re-exporting it from the main entry would pull tslog into the browser-safe barrel.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { logger, getLogger, createUbeanLogger, setMinLevel, setDebugLogging } from '@ubean/shared/logger';
|
|
196
|
+
|
|
197
|
+
logger.info('booted');
|
|
198
|
+
const http = getLogger('http'); // child logger named `ubean:http`
|
|
199
|
+
|
|
200
|
+
setMinLevel('debug'); // global level switch (affects existing and future instances)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| API | Description |
|
|
204
|
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
205
|
+
| `logger` | Default global tslog instance named `ubean` |
|
|
206
|
+
| `createUbeanLogger(options)` | Create a named instance; `options` are tslog v5 settings (`type` / `pretty` / `json` / `mask` / `stack` / `meta` / `minLevel`) |
|
|
207
|
+
| `getLogger(scope?, options?)` | Scope child logger; inherits settings and `minLevel` from the parent; no scope → global `logger` |
|
|
208
|
+
| `setMinLevel(level)` | Global level: `silly` / `trace` / `debug` / `info` / `warn` / `error` / `fatal` |
|
|
209
|
+
| `setDebugLogging(enabled)` | Convenience toggle for the debug level |
|
|
210
|
+
| `UbeanLogger` / `UbeanLoggerOptions` / `LogLevelName` | Logger instance type, settings, and level name union |
|
|
211
|
+
|
|
212
|
+
The Hono request logger lives behind `/logger/hono`, again to keep Hono middleware out of the shared barrel:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createRequestLoggerMiddleware } from '@ubean/shared/logger/hono';
|
|
216
|
+
|
|
217
|
+
app.use('*', createRequestLoggerMiddleware({ slowThreshold: 1000, exclude: ['/_health', '/api/**'] }));
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`RequestLoggerOptions`: `logger` (default `getLogger('http')`), `exclude` (glob strings, `RegExp`, or a `(path) => boolean` predicate), `slowThreshold` (ms, default `1000`), `logQuery` (log the full URL including the query string, default `false`). Errors captured by Hono's `onError` are detected through `c.error` and logged with their status.
|
|
221
|
+
|
|
222
|
+
## Node Utilities
|
|
223
|
+
|
|
224
|
+
`/node` bundles everything that imports `node:net`, `node:os`, or `node:fs`. It never enters the main entry.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { findAvailablePort, waitForPort, isPortReachable, sleep } from '@ubean/shared/node';
|
|
228
|
+
import { getLanAddresses, isLoopbackHost, isWildcardHost } from '@ubean/shared/node';
|
|
229
|
+
import { findUserViteConfig } from '@ubean/shared/node';
|
|
230
|
+
|
|
231
|
+
const port = await findAvailablePort(3000, { strictPort: false }); // walks 3001, 3002… like Vite
|
|
232
|
+
await waitForPort(port, { timeout: 5000 });
|
|
233
|
+
getLanAddresses(); // ['192.168.1.20', …] — IPv4 only, interface-name sorted
|
|
234
|
+
findUserViteConfig(process.cwd()); // '/…/vite.config.ts' | null
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Group | API |
|
|
238
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| Port probing | `findAvailablePort(port, options)`, `waitForPort(port, options)`, `isPortReachable(port, host)`, `sleep(ms)`; options types `FindPortOptions`, `WaitForPortOptions` |
|
|
240
|
+
| Network addresses | `isLoopbackHost(host)`, `isWildcardHost(host)`, `getLanAddresses()` |
|
|
241
|
+
| Vite config | `findUserViteConfig(cwd)` — scans `vite.config.ts` / `.mts` / `.js` / `.mjs`, returns the first hit or `null` |
|
|
242
|
+
|
|
243
|
+
## Type Safety
|
|
244
|
+
|
|
245
|
+
- The package is written in strict TypeScript and ships `dist/*.d.ts` for every subpath; `EnvSchema` + `InferEnvOutput<S>` make `defineEnv({...}).env` fully inferred — `PORT` is `number`, not `string`.
|
|
246
|
+
- `GenericSchema<O = unknown>` accepts Standard Schema objects; `validate()` is the async/sync-agnostic entry point, while only schemas with a sync `safeParse` work as direct schema entries.
|
|
247
|
+
- Error narrowing is type-safe through `isUbeanError` and `isActionFailure`, so `statusCode` / `errors` are only reachable after the guard.
|
|
248
|
+
- No global `declare module` augmentation is performed — the types are plain exports.
|
|
249
|
+
|
|
250
|
+
## Architecture Notes
|
|
251
|
+
|
|
252
|
+
- **Dependency direction.** `@ubean/shared` is the root leaf: it imports **no** `@ubean/*` package. Everything else may import it. Types that previously caused cycles (`BaseRouteMeta` from `@ubean/routing`, `Span*` from `@ubean/server`, `PageHead` from `@ubean/pages`) were deliberately pushed down here rather than duplicated.
|
|
253
|
+
- **Subpath split is a browser-safety boundary, not a convenience.** `tslog` lives behind `/logger`; `node:net` / `node:os` / `node:fs` behind `/node`; Hono middleware behind `/logger/hono`. The main barrel (`src/index.ts`) only re-exports `types` / `error` / `env` / `path` / `glob` / `string`, so client bundles never pick up a logger or a Node built-in.
|
|
254
|
+
- **No `globalThis` registry of its own.** The data and cache registries of sibling packages need cross-instance sharing, but `@ubean/shared` itself holds only `_runtimeEnv` — a module-level merge target set explicitly by `setRuntimeEnv`, which is also available through the app runtime.
|
|
255
|
+
- **No side effects on import.** Every module is a pure collection of exports; importing `@ubean/shared` installs nothing globally and binds no listeners.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# @ubean/shared
|
|
2
|
+
|
|
3
|
+
**[English](./README.md) | 中文**
|
|
4
|
+
|
|
5
|
+
ubean 的共享协议类型、错误、环境变量处理与通用工具。
|
|
6
|
+
|
|
7
|
+
`@ubean/shared` 是 monorepo 的基础 leaf 包:其他所有 `@ubean/*` 包都可以依赖它,而它**不依赖**任何 `@ubean/*` 包。主入口保持浏览器安全 —— Node 专用工具放在 `/node` 子路径,基于 tslog 的 logger 放在 `/logger`。
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineEnv, createError, matchGlob } from '@ubean/shared';
|
|
11
|
+
import { logger } from '@ubean/shared/logger';
|
|
12
|
+
import { findAvailablePort } from '@ubean/shared/node';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 目录
|
|
16
|
+
|
|
17
|
+
- [包定位](#包定位)
|
|
18
|
+
- [安装](#安装)
|
|
19
|
+
- [快速开始](#快速开始)
|
|
20
|
+
- [环境变量](#环境变量)
|
|
21
|
+
- [错误](#错误)
|
|
22
|
+
- [路由规则与共享类型](#路由规则与共享类型)
|
|
23
|
+
- [Server Actions 共享类型](#server-actions-共享类型)
|
|
24
|
+
- [Glob 匹配](#glob-匹配)
|
|
25
|
+
- [路径与字符串工具](#路径与字符串工具)
|
|
26
|
+
- [日志](#日志)
|
|
27
|
+
- [Node 工具](#node-工具)
|
|
28
|
+
- [类型安全](#类型安全)
|
|
29
|
+
- [架构说明](#架构说明)
|
|
30
|
+
|
|
31
|
+
## 包定位
|
|
32
|
+
|
|
33
|
+
| 本包负责 | 本包不负责 —— 对应归属 |
|
|
34
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
35
|
+
| 跨包共享的协议**类型**:`PageHead`、`RouteMeta` / `BaseRouteMeta` / `RateLimitMeta` / `CacheMeta`、`RouteRule` / `IsrRule`、`Span*`、`UbeanEnv` / `UbeanContext` / `UbeanHandler` / `UbeanMiddleware` / `UbeanVariables` / `UbeanBindings` / `UbeanMiddlewareStep`、`GenericSchema` / `Input` | 页面数据协议的运行时 → `@ubean/pages`;handler 运行时 → `@ubean/routes`;SEO 元数据构造 → `@ubean/seo` |
|
|
36
|
+
| 零依赖的通用**工具**:`normalizePath`、`getDirname`、`getBasename`、`getExtension`、`getStem`、`pathToTitle`、`capitalize`、`matchGlob`、`matchAnyGlob` | 路由扫描 → `@ubean/scan`;预渲染链接发现 → `@ubean/builder` |
|
|
37
|
+
| **错误原语**:`UbeanError`、`createError`、`isUbeanError`、`errorToResponse` | Hono `onError` 接线与 `handleError` hook → `@ubean/app` |
|
|
38
|
+
| **环境变量**辅助:`defineEnv`、`setRuntimeEnv`、`useRuntimeEnv`、`isNodeRuntime` 以及 `EnvSchema` / `InferEnvOutput` / `EnvValidationError` 类型 | `ubean.config.ts` 加载与模块解析 → `@ubean/config` |
|
|
39
|
+
| **日志**门面(`/logger`、`/logger/hono`):`createUbeanLogger` / `logger` / `getLogger` / `setMinLevel` / `setDebugLogging` 与 Hono 请求日志中间件,封装 tslog@5 | 服务端请求生命周期、缓存与 tracing → `@ubean/server`;DevTools 日志 UI → `@ubean/devtools` |
|
|
40
|
+
| **Node 专用**工具(`/node`):端口探测(`findAvailablePort`、`waitForPort`、`isPortReachable`、`sleep`)、局域网地址探测(`getLanAddresses`、`isLoopbackHost`、`isWildcardHost`)、Vite 配置查找(`findUserViteConfig`) | 进程生命周期、dev server 自举与 CLI 输出 → `@ubean/cli` / `@ubean/builder` |
|
|
41
|
+
|
|
42
|
+
**主入口的运行时依赖白名单:`@standard-schema/spec`、`hono`(仅类型)、`pathe`、`scule`** —— 不含 `tslog`,不含 `node:*`。`/logger` 子路径的存在意义就是让 tslog 永不进入浏览器安全的主 barrel;`/node` 出于同样理由隔离 `node:net` / `node:os` / `node:fs`。
|
|
43
|
+
|
|
44
|
+
`@ubean/shared` **没有任何 peer 依赖** —— 没有 peer,也就不存在「缺少 peer 时能力降级」的情况。
|
|
45
|
+
|
|
46
|
+
## 安装
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm add @ubean/shared
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
其他每个 `@ubean/*` 包都会传递安装它;只需要共享类型或工具时可以直接安装。
|
|
53
|
+
|
|
54
|
+
## 快速开始
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { UbeanError, createError, errorToResponse, defineEnv, isNodeRuntime, matchGlob } from '@ubean/shared';
|
|
58
|
+
|
|
59
|
+
// 带状态码的类型化错误
|
|
60
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
61
|
+
|
|
62
|
+
// 转成 JSON Response(未知错误同样适用)
|
|
63
|
+
try {
|
|
64
|
+
await handler();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return errorToResponse(err);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// '**' 匹配任意层级,'*' 只匹配单段
|
|
70
|
+
matchGlob('/blog/a/b', '/blog/**'); // true
|
|
71
|
+
isNodeRuntime(); // Cloudflare Workers 上为 false
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 环境变量
|
|
75
|
+
|
|
76
|
+
`defineEnv(config)` 校验基于构造函数的 schema,返回 `{ env, validate }`。`server` 键永不暴露给客户端;`public` 键通过 `import.meta.env` 暴露。
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { defineEnv } from '@ubean/shared';
|
|
80
|
+
|
|
81
|
+
export const { env, validate } = defineEnv({
|
|
82
|
+
server: {
|
|
83
|
+
DATABASE_URL: { type: String, required: true },
|
|
84
|
+
PORT: { type: Number, default: 9527 }
|
|
85
|
+
},
|
|
86
|
+
public: {
|
|
87
|
+
APP_NAME: { type: String, default: 'ubean-app' }
|
|
88
|
+
},
|
|
89
|
+
mode: 'warn' // 'warn' 记录日志;'throw' 在校验失败时抛出
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
env.PORT; // number
|
|
93
|
+
validate(); // { success, errors, data }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| API | 说明 |
|
|
97
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
98
|
+
| `defineEnv(config)` | 校验 schema 并返回 `{ env, validate }`(`DefineEnvResult<S>`) |
|
|
99
|
+
| `setRuntimeEnv(env)` / `useRuntimeEnv(key, def)` | 合并 / 读取运行态环境变量(worker bindings、注入的密钥) |
|
|
100
|
+
| `isNodeRuntime()` | Node / Bun / Deno 上为 true(用 `process.versions.node` 判定,而非 `typeof process`) |
|
|
101
|
+
| `EnvSchema` / `InferEnvOutput<S>` | schema 映射类型及其推导出的输出类型 |
|
|
102
|
+
| `EnvValidationError` | 每个失败项的 `{ key, message, value }` |
|
|
103
|
+
|
|
104
|
+
`isNodeRuntime()` 的用途是把 Node 专用能力挡在**动态** `import()` 之后:打包器会把静态 import 的 `node:*` 留在产物里,workerd 在模块实例化阶段就失败(`No such module "node:fs/promises"`),根本轮不到分支判断。
|
|
105
|
+
|
|
106
|
+
## 错误
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { UbeanError, createError, isUbeanError, errorToResponse } from '@ubean/shared';
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
throw createError({ statusCode: 400, statusMessage: 'Bad Request', data: { field: 'email' } });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (isUbeanError(err)) {
|
|
115
|
+
err.statusCode; // 400
|
|
116
|
+
}
|
|
117
|
+
return errorToResponse(err); // JSON: { error, statusCode, data }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `UbeanError extends Error` 携带 `statusCode` / `statusMessage` / `data`;默认 message 来自状态码表(400/401/403/404/405/408/409/422/429/500/501/502/503),未命中回退到 `'Error'`。
|
|
122
|
+
- `isUbeanError(err)` 同时接受真实实例与跨模块边界传来的错误(检查 `err.name === 'UbeanError'` 且 `statusCode` 为数字)。
|
|
123
|
+
- `errorToResponse(err)` 用 `UbeanError` 自身的状态码渲染;其他错误一律变成 `500` + `{ error: 'Internal Server Error', message }`。
|
|
124
|
+
|
|
125
|
+
## 路由规则与共享类型
|
|
126
|
+
|
|
127
|
+
这些类型是路由、缓存与预渲染层的共享词汇。它们放在这里是为了打破循环依赖 —— `@ubean/routes` 与 `@ubean/config` 都需要它们,而两者不能互相依赖。
|
|
128
|
+
|
|
129
|
+
| 类型 | 形状 / 含义 |
|
|
130
|
+
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `BaseRouteMeta` | `{ requiresAuth?: boolean; [key: string]: unknown }` |
|
|
132
|
+
| `RateLimitMeta` / `CacheMeta` | `{ maxRequests, windowSeconds }` / `{ ttl, swr? }` |
|
|
133
|
+
| `RouteMeta extends BaseRouteMeta` | 增加 `rateLimit` 与 `cache` |
|
|
134
|
+
| `IsrRule` | `{ ttl: number; swr?: boolean }` —— `routeRules.isr` 的对象形式 |
|
|
135
|
+
| `RouteRule` | 单路由的 `redirect` / `rewrite` / `proxy` / `headers` / `cache` / `ssr` / `prerender` / `isr` / `ppr` |
|
|
136
|
+
| `PageHead` | 被 `@ubean/pages` 与 `@ubean/seo` 消费的 head 元数据 |
|
|
137
|
+
| `Span`、`SpanOptions`、`SpanContext`、`SpanAttributes`、`SpanEvent`、`SpanEndOptions`、`SpanStatus` | 被 `@ubean/server` 可观测性消费的 tracing 原语 |
|
|
138
|
+
| `UbeanVariables` / `UbeanBindings` / `UbeanEnv` | Hono context variables、平台 bindings 与组合出的 `UbeanEnv` |
|
|
139
|
+
| `UbeanContext` / `UbeanHandler` / `UbeanMiddleware` / `ComposedHandler` / `Input` / `GenericSchema` | `@ubean/routes` 的 handler 与 middleware 层类型 |
|
|
140
|
+
| `UbeanMiddlewareStep` | 中间件步骤名的联合类型(`handle`、`requestId`、`securityHeaders`、`csrf`、`cacheStore`、`i18n`、`routeRules` 等) |
|
|
141
|
+
|
|
142
|
+
## Server Actions 共享类型
|
|
143
|
+
|
|
144
|
+
Server Actions 定义在 `@ubean/routes`,但它们的协议类型放在这里共享,这样客户端运行时无需引入服务端代码即可消费。
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { ACTION_BRAND, ActionError, fail, isServerAction, isActionFailure } from '@ubean/shared';
|
|
148
|
+
|
|
149
|
+
fail(400, { email: 'Invalid email' }); // { status: 400, errors: { email: '…' } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| API | 说明 |
|
|
153
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
154
|
+
| `ACTION_BRAND` | 标记 `ServerAction` 的 well-known symbol(`Symbol.for('ubean.action')`) |
|
|
155
|
+
| `ServerAction<TInput, TOutput>` | action 描述符;`ActionHandler` 是其实现签名 |
|
|
156
|
+
| `ActionContext` / `ActionResult<T>` | handler 上下文与成功信封 |
|
|
157
|
+
| `ActionFailure<T>` | `{ status, errors }` 字段级失败信封 |
|
|
158
|
+
| `fail(status, errors)` | 构造 `ActionFailure` |
|
|
159
|
+
| `ActionError` | 可在 handler 中抛出的用户可读错误类,含 `code` / `status` |
|
|
160
|
+
| `isServerAction(value)` / `isActionFailure(value)` | 类型守卫 |
|
|
161
|
+
| `ActionSchema<TOutput>` | Standard Schema / `safeParse` 输入契约 |
|
|
162
|
+
|
|
163
|
+
## Glob 匹配
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { matchGlob, matchAnyGlob } from '@ubean/shared';
|
|
167
|
+
|
|
168
|
+
matchGlob('/blog', '/blog/**'); // true ('**' 也匹配裸前缀)
|
|
169
|
+
matchGlob('/blog/a', '/blog/*'); // true ('*' 是单段)
|
|
170
|
+
matchGlob('/blog/a/b', '/blog/*'); // false
|
|
171
|
+
matchAnyGlob('/admin/x', ['/blog/**', '/admin/**']); // true
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`matchGlob(route, pattern)` 支持 `**`(递归)、`*`(单段)与字面量;`pattern === '**'` 或 `'/**'` 匹配一切。SSR `exclude`、预渲染 `include`/`exclude` 与路由规则 pattern 都走这个匹配器。
|
|
175
|
+
|
|
176
|
+
## 路径与字符串工具
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { normalizePath, getDirname, getBasename, getExtension, getStem, pathToTitle, capitalize } from '@ubean/shared';
|
|
180
|
+
|
|
181
|
+
normalizePath('//a\\b/c/'); // '/a/b/c'
|
|
182
|
+
getExtension('Post.MD'); // 'md'
|
|
183
|
+
getStem('Post.md'); // 'Post'
|
|
184
|
+
pathToTitle('blog/getting-started.md'); // 'Getting Started'
|
|
185
|
+
pathToTitle('blog/index.md'); // 'Blog' (index 解析为父目录名)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`pathToTitle` 委托 `scule` 的 `kebabCase` 加 `capitalize` —— 这就是 `@ubean/content` 在文档没有显式 `title` 时使用的标题化规则。
|
|
189
|
+
|
|
190
|
+
## 日志
|
|
191
|
+
|
|
192
|
+
`/logger` 是唯一的公开日志入口。它**刻意做成独立子路径**:从主入口 re-export 会把 tslog 拖进浏览器安全的 barrel。
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { logger, getLogger, createUbeanLogger, setMinLevel, setDebugLogging } from '@ubean/shared/logger';
|
|
196
|
+
|
|
197
|
+
logger.info('booted');
|
|
198
|
+
const http = getLogger('http'); // 名为 `ubean:http` 的子 logger
|
|
199
|
+
|
|
200
|
+
setMinLevel('debug'); // 全局级别切换(对已创建与后续创建的实例都生效)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| API | 说明 |
|
|
204
|
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
205
|
+
| `logger` | 名为 `ubean` 的默认全局 tslog 实例 |
|
|
206
|
+
| `createUbeanLogger(options)` | 创建命名实例;`options` 为 tslog v5 settings(`type` / `pretty` / `json` / `mask` / `stack` / `meta` / `minLevel`) |
|
|
207
|
+
| `getLogger(scope?, options?)` | scope 子 logger;继承父实例的 settings 与 `minLevel`;不传 scope 返回全局 `logger` |
|
|
208
|
+
| `setMinLevel(level)` | 全局级别:`silly` / `trace` / `debug` / `info` / `warn` / `error` / `fatal` |
|
|
209
|
+
| `setDebugLogging(enabled)` | debug 级别的便捷开关 |
|
|
210
|
+
| `UbeanLogger` / `UbeanLoggerOptions` / `LogLevelName` | logger 实例类型、settings 与级别名联合类型 |
|
|
211
|
+
|
|
212
|
+
Hono 请求日志放在 `/logger/hono`,同样是为了让 Hono 中间件不进入 shared barrel:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createRequestLoggerMiddleware } from '@ubean/shared/logger/hono';
|
|
216
|
+
|
|
217
|
+
app.use('*', createRequestLoggerMiddleware({ slowThreshold: 1000, exclude: ['/_health', '/api/**'] }));
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`RequestLoggerOptions`:`logger`(默认 `getLogger('http')`)、`exclude`(glob 字符串、`RegExp` 或 `(path) => boolean` 谓词)、`slowThreshold`(毫秒,默认 `1000`)、`logQuery`(记录含 query string 的完整 URL,默认 `false`)。Hono `onError` 捕获的错误通过 `c.error` 识别,并连同状态码一起记录。
|
|
221
|
+
|
|
222
|
+
## Node 工具
|
|
223
|
+
|
|
224
|
+
`/node` 汇集所有 import `node:net`、`node:os`、`node:fs` 的实现,永不进入主入口。
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { findAvailablePort, waitForPort, isPortReachable, sleep } from '@ubean/shared/node';
|
|
228
|
+
import { getLanAddresses, isLoopbackHost, isWildcardHost } from '@ubean/shared/node';
|
|
229
|
+
import { findUserViteConfig } from '@ubean/shared/node';
|
|
230
|
+
|
|
231
|
+
const port = await findAvailablePort(3000, { strictPort: false }); // 像 Vite 一样依次探 3001、3002…
|
|
232
|
+
await waitForPort(port, { timeout: 5000 });
|
|
233
|
+
getLanAddresses(); // ['192.168.1.20', …] —— 仅 IPv4,按网卡名排序
|
|
234
|
+
findUserViteConfig(process.cwd()); // '/…/vite.config.ts' | null
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| 分组 | API |
|
|
238
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
239
|
+
| 端口探测 | `findAvailablePort(port, options)`、`waitForPort(port, options)`、`isPortReachable(port, host)`、`sleep(ms)`;选项类型 `FindPortOptions`、`WaitForPortOptions` |
|
|
240
|
+
| 网络地址 | `isLoopbackHost(host)`、`isWildcardHost(host)`、`getLanAddresses()` |
|
|
241
|
+
| Vite 配置 | `findUserViteConfig(cwd)` —— 依次查找 `vite.config.ts` / `.mts` / `.js` / `.mjs`,返回首个命中或 `null` |
|
|
242
|
+
|
|
243
|
+
## 类型安全
|
|
244
|
+
|
|
245
|
+
- 本包用严格 TypeScript 编写,每个子路径都产出 `dist/*.d.ts`;`EnvSchema` + `InferEnvOutput<S>` 让 `defineEnv({...}).env` 完全推导 —— `PORT` 是 `number` 而非 `string`。
|
|
246
|
+
- `GenericSchema<O = unknown>` 接受 Standard Schema 对象;`validate()` 是同步/异步无关的入口,而只有带同步 `safeParse` 的 schema 能作为直接 schema 条目使用。
|
|
247
|
+
- 错误收窄通过 `isUbeanError` 与 `isActionFailure` 保证类型安全,`statusCode` / `errors` 只有过了守卫才能访问。
|
|
248
|
+
- 不做任何全局 `declare module` 增强 —— 这些类型都是普通导出。
|
|
249
|
+
|
|
250
|
+
## 架构说明
|
|
251
|
+
|
|
252
|
+
- **依赖方向。** `@ubean/shared` 是根 leaf:**不** import 任何 `@ubean/*` 包,其余包都可以 import 它。历史上造成环依赖的类型(来自 `@ubean/routing` 的 `BaseRouteMeta`、来自 `@ubean/server` 的 `Span*`、来自 `@ubean/pages` 的 `PageHead`)都是有意下沉到这里的,而不是各自复制一份。
|
|
253
|
+
- **子路径划分是浏览器安全边界,不是便利性分组。** `tslog` 在 `/logger` 之后,`node:net` / `node:os` / `node:fs` 在 `/node` 之后,Hono 中间件在 `/logger/hono` 之后。主 barrel(`src/index.ts`)只 re-export `types` / `error` / `env` / `path` / `glob` / `string`,因此客户端产物永远不会捎上 logger 或 Node 内建模块。
|
|
254
|
+
- **刻意不依赖 `globalThis`。** 兄弟包的数据与缓存注册表需要跨实例共享,但 `@ubean/shared` 自身只持有 `_runtimeEnv` —— 一个由 `setRuntimeEnv` 显式写入的模块级合并目标,它同样可以通过 app runtime 访问。
|
|
255
|
+
- **import 无副作用。** 每个模块都是纯导出集合;import `@ubean/shared` 不会全局安装任何东西,也不会绑定任何监听器。
|
package/dist/logger/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createUbeanLogger, getLogger, logger, setDebugLogging, setMinLevel };
|
|
1
|
+
export { t as createUbeanLogger, n as getLogger, r as logger, i as setDebugLogging, a as setMinLevel } from "../logger-NMOHjTCB.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/shared",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Shared protocol types, errors, env and universal utilities for ubean",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^26.6.5",
|
|
39
39
|
"typescript": "npm:typescript-native-bridge@latest",
|
|
40
|
-
"vite-plus": "1.
|
|
40
|
+
"vite-plus": "1.3.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "vp pack",
|