@alfredmouelle/create-stack 0.1.1 → 0.2.0
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 +41 -18
- package/_stack/apps/next-base/.turbo/turbo-typecheck.log +1 -0
- package/_stack/apps/next-base/.vscode/settings.json +35 -0
- package/_stack/apps/next-base/.zed/settings.json +45 -0
- package/_stack/apps/next-base/src/components/ui/spinner.tsx +1 -1
- package/_stack/apps/next-base/src/emails/components/components.tsx +2 -2
- package/_stack/apps/next-base/src/emails/components/context.tsx +1 -1
- package/_stack/apps/next-base/src/emails/components/theme.ts +6 -7
- package/_stack/apps/next-base/src/env.ts +2 -3
- package/_stack/apps/next-base/src/lib/date.ts +1 -1
- package/_stack/apps/next-base/src/server/auth/guards.ts +1 -1
- package/_stack/apps/next-base/src/server/better-auth/config.ts +1 -1
- package/_stack/apps/next-base/src/server/better-auth/server.ts +2 -2
- package/_stack/apps/next-base/src/server/db/schemas/index.ts +1 -1
- package/_stack/apps/next-base/src/server/db/seed.ts +2 -2
- package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +3 -3
- package/_stack/apps/next-base/src/server/email/core/address.ts +3 -3
- package/_stack/apps/next-base/src/server/email/core/port.ts +13 -20
- package/_stack/apps/next-base/src/server/email/core/render.ts +2 -2
- package/_stack/apps/next-base/src/server/email/factory.ts +7 -9
- package/_stack/apps/next-base/src/server/email/index.ts +1 -1
- package/_stack/apps/next-base/src/trpc/react.tsx +2 -2
- package/_stack/apps/next-base/src/trpc/server.ts +1 -1
- package/_stack/apps/tanstack-base/.turbo/turbo-typecheck.log +1 -0
- package/_stack/apps/tanstack-base/.vscode/settings.json +35 -0
- package/_stack/apps/tanstack-base/.zed/settings.json +45 -0
- package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +1 -1
- package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +1 -1
- package/_stack/apps/tanstack-base/src/emails/components/components.tsx +2 -2
- package/_stack/apps/tanstack-base/src/emails/components/context.tsx +1 -1
- package/_stack/apps/tanstack-base/src/emails/components/theme.ts +6 -7
- package/_stack/apps/tanstack-base/src/env.ts +2 -6
- package/_stack/apps/tanstack-base/src/lib/date.ts +1 -1
- package/_stack/apps/tanstack-base/src/routes/__root.tsx +1 -1
- package/_stack/apps/tanstack-base/src/routes/_authed.tsx +1 -4
- package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +1 -1
- package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +1 -2
- package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +1 -1
- package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +6 -7
- package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +1 -1
- package/_stack/apps/tanstack-base/src/server/db/seed.ts +2 -2
- package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +3 -3
- package/_stack/apps/tanstack-base/src/server/email/core/address.ts +3 -3
- package/_stack/apps/tanstack-base/src/server/email/core/port.ts +12 -22
- package/_stack/apps/tanstack-base/src/server/email/core/render.ts +1 -1
- package/_stack/apps/tanstack-base/src/server/email/factory.ts +7 -8
- package/_stack/apps/tanstack-base/src/server/email/index.ts +1 -1
- package/_stack/packages/analytics/package.json +26 -0
- package/_stack/packages/analytics/src/adapters/noop/index.ts +12 -0
- package/_stack/packages/analytics/src/adapters/plausible/config.ts +10 -0
- package/_stack/packages/analytics/src/adapters/plausible/index.ts +94 -0
- package/_stack/packages/analytics/src/adapters/posthog/config.ts +7 -0
- package/_stack/packages/analytics/src/adapters/posthog/index.ts +50 -0
- package/_stack/packages/analytics/src/core/port.ts +30 -0
- package/_stack/packages/analytics/src/index.ts +17 -0
- package/_stack/packages/cache/package.json +25 -0
- package/_stack/packages/cache/src/adapters/memory/index.ts +51 -0
- package/_stack/packages/cache/src/adapters/redis/config.ts +8 -0
- package/_stack/packages/cache/src/adapters/redis/index.ts +73 -0
- package/_stack/packages/cache/src/core/port.ts +29 -0
- package/_stack/packages/cache/src/core/wrap.ts +20 -0
- package/_stack/packages/cache/src/index.ts +12 -0
- package/_stack/packages/error-tracking/package.json +25 -0
- package/_stack/packages/error-tracking/src/adapters/console/index.ts +43 -0
- package/_stack/packages/error-tracking/src/adapters/sentry/config.ts +8 -0
- package/_stack/packages/error-tracking/src/adapters/sentry/index.ts +72 -0
- package/_stack/packages/error-tracking/src/core/port.ts +39 -0
- package/_stack/packages/error-tracking/src/index.ts +14 -0
- package/_stack/packages/http/package.json +20 -0
- package/_stack/packages/http/src/api.ts +373 -0
- package/_stack/packages/http/src/index.ts +14 -0
- package/_stack/packages/http/src/responses.ts +25 -0
- package/_stack/packages/http/src/types.ts +9 -0
- package/_stack/packages/jobs/package.json +27 -0
- package/_stack/packages/jobs/src/adapters/inngest/config.ts +8 -0
- package/_stack/packages/jobs/src/adapters/inngest/index.ts +93 -0
- package/_stack/packages/jobs/src/adapters/memory/index.ts +31 -0
- package/_stack/packages/jobs/src/adapters/trigger/config.ts +8 -0
- package/_stack/packages/jobs/src/adapters/trigger/index.ts +85 -0
- package/_stack/packages/jobs/src/core/port.ts +37 -0
- package/_stack/packages/jobs/src/index.ts +23 -0
- package/_stack/packages/logger/package.json +25 -0
- package/_stack/packages/logger/src/adapters/console/config.ts +7 -0
- package/_stack/packages/logger/src/adapters/console/index.ts +69 -0
- package/_stack/packages/logger/src/adapters/pino/index.ts +54 -0
- package/_stack/packages/logger/src/core/port.ts +21 -0
- package/_stack/packages/logger/src/index.ts +12 -0
- package/_stack/packages/mailer/src/adapters/brevo/index.ts +3 -3
- package/_stack/packages/mailer/src/adapters/resend/index.ts +3 -3
- package/_stack/packages/mailer/src/adapters/ses/config.ts +3 -3
- package/_stack/packages/mailer/src/adapters/ses/index.ts +4 -5
- package/_stack/packages/storage/package.json +27 -0
- package/_stack/packages/storage/src/adapters/gcs/config.ts +8 -0
- package/_stack/packages/storage/src/adapters/gcs/index.ts +111 -0
- package/_stack/packages/storage/src/adapters/local/config.ts +8 -0
- package/_stack/packages/storage/src/adapters/local/index.ts +78 -0
- package/_stack/packages/storage/src/adapters/r2/config.ts +8 -0
- package/_stack/packages/storage/src/adapters/r2/index.ts +39 -0
- package/_stack/packages/storage/src/adapters/s3/config.ts +11 -0
- package/_stack/packages/storage/src/adapters/s3/index.ts +143 -0
- package/_stack/packages/storage/src/core/port.ts +41 -0
- package/_stack/packages/storage/src/index.ts +21 -0
- package/index.mjs +89 -55
- package/lib/build.mjs +21 -11
- package/lib/capabilities.mjs +375 -0
- package/lib/env.mjs +26 -6
- package/lib/foundations.mjs +35 -0
- package/lib/identity.mjs +4 -5
- package/lib/mailer.mjs +9 -13
- package/lib/paths.mjs +15 -0
- package/lib/scaffold.mjs +12 -11
- package/lib/strip.mjs +9 -24
- package/lib/util.mjs +8 -9
- package/package.json +1 -1
- package/_stack/packages/mailer/capability.json +0 -28
- package/_stack/patterns/README.md +0 -58
- package/_stack/patterns/_baseline/env.ts +0 -31
- package/_stack/patterns/_baseline/tsconfig.json +0 -27
- package/_stack/patterns/better-auth/pattern.json +0 -73
- package/_stack/patterns/better-auth-next/pattern.json +0 -76
- package/_stack/patterns/data-table/pattern.json +0 -43
- package/_stack/patterns/drizzle/pattern.json +0 -61
- package/_stack/patterns/trpc/pattern.json +0 -61
- package/_stack/patterns/trpc-next/pattern.json +0 -64
- package/lib/manifests.mjs +0 -61
- /package/{_stack/patterns/_baseline → templates}/README-author.md +0 -0
- /package/{_stack/patterns/_baseline → templates}/biome.jsonc +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfredmouelle/cache",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"ioredis": "^5.11.1",
|
|
17
|
+
"valibot": "^1.4.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.10.2",
|
|
21
|
+
"tsdown": "^0.22.3",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"vitest": "^4.1.9"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CachePort } from '../../core/port.js'
|
|
2
|
+
import { wrapValue } from '../../core/wrap.js'
|
|
3
|
+
|
|
4
|
+
interface Entry {
|
|
5
|
+
value: unknown
|
|
6
|
+
/** Epoch ms expiry; `undefined` = no expiry. */
|
|
7
|
+
expiresAt?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MemoryAdapterOptions {
|
|
11
|
+
/** Seed store (mostly for tests). */
|
|
12
|
+
store?: Map<string, Entry>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** In-process `Map` cache with lazy per-key expiry; no deps. Dev/tests. */
|
|
16
|
+
export function memoryAdapter(options: MemoryAdapterOptions = {}): CachePort {
|
|
17
|
+
const store = options.store ?? new Map<string, Entry>()
|
|
18
|
+
|
|
19
|
+
function read(key: string): Entry | null {
|
|
20
|
+
const entry = store.get(key)
|
|
21
|
+
if (entry === undefined) return null
|
|
22
|
+
if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {
|
|
23
|
+
store.delete(key)
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
return entry
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const port: CachePort = {
|
|
30
|
+
name: 'memory',
|
|
31
|
+
async get<T>(key: string) {
|
|
32
|
+
const entry = read(key)
|
|
33
|
+
return entry === null ? null : (entry.value as T)
|
|
34
|
+
},
|
|
35
|
+
async set<T>(key: string, value: T, ttlSeconds?: number) {
|
|
36
|
+
const expiresAt = ttlSeconds === undefined ? undefined : Date.now() + ttlSeconds * 1000
|
|
37
|
+
store.set(key, { value, expiresAt })
|
|
38
|
+
},
|
|
39
|
+
async delete(key: string) {
|
|
40
|
+
store.delete(key)
|
|
41
|
+
},
|
|
42
|
+
async has(key: string) {
|
|
43
|
+
return read(key) !== null
|
|
44
|
+
},
|
|
45
|
+
wrap<T>(key: string, factory: () => Promise<T>, ttlSeconds?: number) {
|
|
46
|
+
return wrapValue(port, key, factory, ttlSeconds)
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return port
|
|
51
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Redis } from 'ioredis'
|
|
2
|
+
import * as v from 'valibot'
|
|
3
|
+
import { CacheError, type CachePort } from '../../core/port.js'
|
|
4
|
+
import { wrapValue } from '../../core/wrap.js'
|
|
5
|
+
import { RedisConfigSchema } from './config.js'
|
|
6
|
+
|
|
7
|
+
/** Minimal structural view of the Redis client (eases testing). */
|
|
8
|
+
export interface RedisLike {
|
|
9
|
+
get(key: string): Promise<string | null>
|
|
10
|
+
set(key: string, value: string, secondsToken?: 'EX', seconds?: number): Promise<unknown>
|
|
11
|
+
del(key: string): Promise<unknown>
|
|
12
|
+
exists(key: string): Promise<number>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RedisAdapterOptions {
|
|
16
|
+
/** Inject a custom/mock client; defaults to a real `Redis`. */
|
|
17
|
+
client?: RedisLike
|
|
18
|
+
/** Connection URL when no `client` is injected. */
|
|
19
|
+
url?: string
|
|
20
|
+
/** Prepended to every key. */
|
|
21
|
+
keyPrefix?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function redisAdapter(options: RedisAdapterOptions = {}): CachePort {
|
|
25
|
+
// Validate early: bad option fails at construction, not at use.
|
|
26
|
+
const config = v.parse(RedisConfigSchema, { url: options.url, keyPrefix: options.keyPrefix })
|
|
27
|
+
const defaultClient = () =>
|
|
28
|
+
(config.url ? new Redis(config.url) : new Redis()) as unknown as RedisLike
|
|
29
|
+
const client: RedisLike = options.client ?? defaultClient()
|
|
30
|
+
const prefix = config.keyPrefix ?? ''
|
|
31
|
+
const k = (key: string) => `${prefix}${key}`
|
|
32
|
+
|
|
33
|
+
const port: CachePort = {
|
|
34
|
+
name: 'redis',
|
|
35
|
+
async get<T>(key: string) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await client.get(k(key))
|
|
38
|
+
if (raw === null) return null
|
|
39
|
+
return JSON.parse(raw) as T
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
throw new CacheError('Failed to read from Redis', { adapter: 'redis', cause })
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async set<T>(key: string, value: T, ttlSeconds?: number) {
|
|
45
|
+
try {
|
|
46
|
+
const json = JSON.stringify(value)
|
|
47
|
+
if (ttlSeconds === undefined) await client.set(k(key), json)
|
|
48
|
+
else await client.set(k(key), json, 'EX', ttlSeconds)
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
throw new CacheError('Failed to write to Redis', { adapter: 'redis', cause })
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async delete(key: string) {
|
|
54
|
+
try {
|
|
55
|
+
await client.del(k(key))
|
|
56
|
+
} catch (cause) {
|
|
57
|
+
throw new CacheError('Failed to delete from Redis', { adapter: 'redis', cause })
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
async has(key: string) {
|
|
61
|
+
try {
|
|
62
|
+
return (await client.exists(k(key))) > 0
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
throw new CacheError('Failed to query Redis', { adapter: 'redis', cause })
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
wrap<T>(key: string, factory: () => Promise<T>, ttlSeconds?: number) {
|
|
68
|
+
return wrapValue(port, key, factory, ttlSeconds)
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return port
|
|
73
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-facing port; swap adapters at the composition root, never this interface.
|
|
3
|
+
* Values are JSON-serialized for remote stores, so stored values must be JSON-serializable.
|
|
4
|
+
*/
|
|
5
|
+
export interface CachePort {
|
|
6
|
+
/** Backing adapter name (`redis`, `memory`, …). */
|
|
7
|
+
readonly name: string
|
|
8
|
+
/** Read a value, or `null` if absent/expired. */
|
|
9
|
+
get<T>(key: string): Promise<T | null>
|
|
10
|
+
/** Store a value, optionally expiring after `ttlSeconds`. */
|
|
11
|
+
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>
|
|
12
|
+
/** Remove a key (no-op if absent). */
|
|
13
|
+
delete(key: string): Promise<void>
|
|
14
|
+
/** Whether a non-expired value exists. */
|
|
15
|
+
has(key: string): Promise<boolean>
|
|
16
|
+
/** Read-through: cached value, else `factory()` stored with `ttlSeconds`. */
|
|
17
|
+
wrap<T>(key: string, factory: () => Promise<T>, ttlSeconds?: number): Promise<T>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Normalized adapter error; callers never catch backend types. */
|
|
21
|
+
export class CacheError extends Error {
|
|
22
|
+
readonly adapter: string
|
|
23
|
+
|
|
24
|
+
constructor(message: string, options: { adapter: string; cause?: unknown }) {
|
|
25
|
+
super(message, { cause: options.cause })
|
|
26
|
+
this.name = 'CacheError'
|
|
27
|
+
this.adapter = options.adapter
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Minimal read/write pair `wrap` needs; adapters pass their own `get`/`set`. */
|
|
2
|
+
export interface WrapStore {
|
|
3
|
+
get<T>(key: string): Promise<T | null>
|
|
4
|
+
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Read-through: return cached value, else call `factory()`, store with `ttlSeconds`, return. */
|
|
8
|
+
export async function wrapValue<T>(
|
|
9
|
+
store: WrapStore,
|
|
10
|
+
key: string,
|
|
11
|
+
factory: () => Promise<T>,
|
|
12
|
+
ttlSeconds?: number,
|
|
13
|
+
): Promise<T> {
|
|
14
|
+
const cached = await store.get<T>(key)
|
|
15
|
+
if (cached !== null) return cached
|
|
16
|
+
|
|
17
|
+
const value = await factory()
|
|
18
|
+
await store.set(key, value, ttlSeconds)
|
|
19
|
+
return value
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type MemoryAdapterOptions,
|
|
3
|
+
memoryAdapter,
|
|
4
|
+
} from './adapters/memory/index.js'
|
|
5
|
+
export { type RedisConfig, RedisConfigSchema } from './adapters/redis/config.js'
|
|
6
|
+
export {
|
|
7
|
+
type RedisAdapterOptions,
|
|
8
|
+
type RedisLike,
|
|
9
|
+
redisAdapter,
|
|
10
|
+
} from './adapters/redis/index.js'
|
|
11
|
+
export type { CachePort } from './core/port.js'
|
|
12
|
+
export { CacheError } from './core/port.js'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfredmouelle/error-tracking",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@sentry/node": "^10.59.0",
|
|
17
|
+
"valibot": "^1.4.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.10.2",
|
|
21
|
+
"tsdown": "^0.22.3",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"vitest": "^4.1.9"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Breadcrumb,
|
|
3
|
+
CaptureContext,
|
|
4
|
+
ErrorTrackingPort,
|
|
5
|
+
ErrorUser,
|
|
6
|
+
SeverityLevel,
|
|
7
|
+
} from '../../core/port.js'
|
|
8
|
+
|
|
9
|
+
export interface ConsoleAdapterOptions {
|
|
10
|
+
/** Max breadcrumbs kept in memory (oldest dropped). Default 20. */
|
|
11
|
+
maxBreadcrumbs?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Dev/test adapter logging to console; user + breadcrumb state kept in-memory (no transport). */
|
|
15
|
+
export function consoleAdapter(options: ConsoleAdapterOptions = {}): ErrorTrackingPort {
|
|
16
|
+
const maxBreadcrumbs = options.maxBreadcrumbs ?? 20
|
|
17
|
+
let user: ErrorUser | null = null
|
|
18
|
+
const breadcrumbs: Breadcrumb[] = []
|
|
19
|
+
|
|
20
|
+
function state() {
|
|
21
|
+
return { user, breadcrumbs }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: 'console',
|
|
26
|
+
captureException(error: unknown, context?: CaptureContext) {
|
|
27
|
+
console.error('[error-tracking] exception', error, { ...context, ...state() })
|
|
28
|
+
},
|
|
29
|
+
captureMessage(message: string, level: SeverityLevel = 'info') {
|
|
30
|
+
console.error('[error-tracking] message', { message, level, ...state() })
|
|
31
|
+
},
|
|
32
|
+
setUser(next: ErrorUser | null) {
|
|
33
|
+
user = next
|
|
34
|
+
},
|
|
35
|
+
addBreadcrumb(breadcrumb: Breadcrumb) {
|
|
36
|
+
breadcrumbs.push(breadcrumb)
|
|
37
|
+
if (breadcrumbs.length > maxBreadcrumbs) breadcrumbs.shift()
|
|
38
|
+
},
|
|
39
|
+
flush() {
|
|
40
|
+
return Promise.resolve(true)
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/node'
|
|
2
|
+
import * as v from 'valibot'
|
|
3
|
+
import type {
|
|
4
|
+
Breadcrumb,
|
|
5
|
+
CaptureContext,
|
|
6
|
+
ErrorTrackingPort,
|
|
7
|
+
ErrorUser,
|
|
8
|
+
SeverityLevel,
|
|
9
|
+
} from '../../core/port.js'
|
|
10
|
+
import { SentryConfigSchema } from './config.js'
|
|
11
|
+
|
|
12
|
+
/** Minimal structural view of the Sentry namespace (eases testing). */
|
|
13
|
+
export interface SentryCaptureContext {
|
|
14
|
+
tags?: Record<string, string>
|
|
15
|
+
extra?: Record<string, unknown>
|
|
16
|
+
level?: SeverityLevel
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SentryLike {
|
|
20
|
+
init(options: { dsn: string; environment?: string }): void
|
|
21
|
+
captureException(error: unknown, context?: SentryCaptureContext): void
|
|
22
|
+
captureMessage(message: string, level?: SeverityLevel): void
|
|
23
|
+
setUser(user: ErrorUser | null): void
|
|
24
|
+
addBreadcrumb(breadcrumb: Breadcrumb): void
|
|
25
|
+
flush(timeoutMs?: number): Promise<boolean>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SentryAdapterOptions {
|
|
29
|
+
dsn: string
|
|
30
|
+
environment?: string
|
|
31
|
+
/** Inject a custom/mock client; defaults to the real `@sentry/node`. */
|
|
32
|
+
client?: SentryLike
|
|
33
|
+
/** Skip the implicit `Sentry.init` (e.g. init happens elsewhere). */
|
|
34
|
+
init?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sentryAdapter(options: SentryAdapterOptions): ErrorTrackingPort {
|
|
38
|
+
// Validate early: missing DSN fails at construction, not at capture().
|
|
39
|
+
const config = v.parse(SentryConfigSchema, {
|
|
40
|
+
dsn: options.dsn,
|
|
41
|
+
environment: options.environment,
|
|
42
|
+
})
|
|
43
|
+
const client: SentryLike = options.client ?? (Sentry as unknown as SentryLike)
|
|
44
|
+
|
|
45
|
+
// Only init the real namespace; injected clients are assumed already wired.
|
|
46
|
+
if (options.init !== false && !options.client) {
|
|
47
|
+
client.init({ dsn: config.dsn, environment: config.environment })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name: 'sentry',
|
|
52
|
+
captureException(error: unknown, context?: CaptureContext) {
|
|
53
|
+
client.captureException(error, {
|
|
54
|
+
tags: context?.tags,
|
|
55
|
+
extra: context?.extra,
|
|
56
|
+
level: context?.level,
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
captureMessage(message: string, level?: SeverityLevel) {
|
|
60
|
+
client.captureMessage(message, level)
|
|
61
|
+
},
|
|
62
|
+
setUser(user: ErrorUser | null) {
|
|
63
|
+
client.setUser(user)
|
|
64
|
+
},
|
|
65
|
+
addBreadcrumb(breadcrumb: Breadcrumb) {
|
|
66
|
+
client.addBreadcrumb(breadcrumb)
|
|
67
|
+
},
|
|
68
|
+
flush(timeoutMs?: number) {
|
|
69
|
+
return client.flush(timeoutMs)
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Captured-event severity, matching common provider levels. */
|
|
2
|
+
export type SeverityLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug'
|
|
3
|
+
|
|
4
|
+
/** User an event is associated with. */
|
|
5
|
+
export interface ErrorUser {
|
|
6
|
+
id?: string
|
|
7
|
+
email?: string
|
|
8
|
+
username?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Trail of events leading up to an error, for debugging. */
|
|
12
|
+
export interface Breadcrumb {
|
|
13
|
+
message: string
|
|
14
|
+
category?: string
|
|
15
|
+
level?: SeverityLevel
|
|
16
|
+
data?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Extra context attached to a single capture. */
|
|
20
|
+
export interface CaptureContext {
|
|
21
|
+
tags?: Record<string, string>
|
|
22
|
+
extra?: Record<string, unknown>
|
|
23
|
+
level?: SeverityLevel
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** App-facing port; swap adapters at the composition root, never this interface. */
|
|
27
|
+
export interface ErrorTrackingPort {
|
|
28
|
+
readonly name: string
|
|
29
|
+
/** Report an error (or any thrown value) with optional context. */
|
|
30
|
+
captureException(error: unknown, context?: CaptureContext): void
|
|
31
|
+
/** Report a standalone message at severity (default `info`). */
|
|
32
|
+
captureMessage(message: string, level?: SeverityLevel): void
|
|
33
|
+
/** Associate subsequent events with a user; `null` clears. */
|
|
34
|
+
setUser(user: ErrorUser | null): void
|
|
35
|
+
/** Record a breadcrumb for subsequent events. */
|
|
36
|
+
addBreadcrumb(breadcrumb: Breadcrumb): void
|
|
37
|
+
/** Flush buffered events; resolves `true` if all sent in time. */
|
|
38
|
+
flush(timeoutMs?: number): Promise<boolean>
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { type ConsoleAdapterOptions, consoleAdapter } from './adapters/console/index.js'
|
|
2
|
+
export { type SentryConfig, SentryConfigSchema } from './adapters/sentry/config.js'
|
|
3
|
+
export {
|
|
4
|
+
type SentryAdapterOptions,
|
|
5
|
+
type SentryLike,
|
|
6
|
+
sentryAdapter,
|
|
7
|
+
} from './adapters/sentry/index.js'
|
|
8
|
+
export type {
|
|
9
|
+
Breadcrumb,
|
|
10
|
+
CaptureContext,
|
|
11
|
+
ErrorTrackingPort,
|
|
12
|
+
ErrorUser,
|
|
13
|
+
SeverityLevel,
|
|
14
|
+
} from './core/port.js'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfredmouelle/http",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"tsdown": "^0.22.3",
|
|
17
|
+
"typescript": "^5.9.3",
|
|
18
|
+
"vitest": "^4.1.9"
|
|
19
|
+
}
|
|
20
|
+
}
|