cursedbelt-core 1.0.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/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- package/src/testing/index.ts +11 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical server error class + error/validation envelope for the whole
|
|
3
|
+
* monorepo — cursedbelt apps AND cursedshell plugins.
|
|
4
|
+
*
|
|
5
|
+
* Every API failure — thrown deep in a handler, surfaced by middleware, or raised
|
|
6
|
+
* by a plugin service — is shaped into ONE wire contract, the **rich** envelope
|
|
7
|
+
* defined once in cwip (`cwip/error`):
|
|
8
|
+
*
|
|
9
|
+
* { "error": { "name", "message", "code?", "status?", "category?", "context?",
|
|
10
|
+
* "isOperational", "timestamp" }, "correlationId?": "…" }
|
|
11
|
+
*
|
|
12
|
+
* Clients parse exactly this shape, so there is no per-route error drift — and no
|
|
13
|
+
* drift between a cwip-based server (which already emits it via `cwip/server`'s
|
|
14
|
+
* `errorHandler`) and a cursedshell one. The extra fields are additive:
|
|
15
|
+
* a client that only reads `message`/`code`/`context` keeps working unchanged.
|
|
16
|
+
*
|
|
17
|
+
* `ApiError` is the ergonomic throwable: `throw new ApiError(404, 'not found')`.
|
|
18
|
+
* It is a thin subclass of cwip's `AppError` with the status-first constructor
|
|
19
|
+
* shape every plugin already uses, so a single `getSummary()`/`toErrorEnvelope`
|
|
20
|
+
* produces the rich body. The `errorEnvelope` Hono `onError` handler
|
|
21
|
+
* (cursedbelt-server) turns it (and any other thrown value) into the envelope +
|
|
22
|
+
* the right HTTP status; {@link ApiError.fromEnvelope} rebuilds one on the CLIENT
|
|
23
|
+
* from a parsed error body.
|
|
24
|
+
*
|
|
25
|
+
* This module is **framework-free** (no Hono, no DOM, no server deps — it imports
|
|
26
|
+
* only cwip's framework-free `cwip/error` and a type-only `zod` for the validation
|
|
27
|
+
* builder), so it is safe to import from browser bundles and from cursedshell's
|
|
28
|
+
* plugin-pure layer via the `cursedbelt-core/errors` subpath. Server code reaches the
|
|
29
|
+
* same symbols through `cursedbelt-server` (which re-exports this module).
|
|
30
|
+
*/
|
|
31
|
+
import { AppError, type AppErrorSummary, type ErrorEnvelope, isAppError, type ToErrorEnvelopeOptions, toErrorEnvelope } from 'cwip/error';
|
|
32
|
+
import type { ZodError } from 'zod';
|
|
33
|
+
export { AppError, type AppErrorSummary, type ErrorEnvelope, isAppError, type ToErrorEnvelopeOptions, toErrorEnvelope };
|
|
34
|
+
export interface ApiErrorOptions {
|
|
35
|
+
/** Stable machine code carried into the envelope. */
|
|
36
|
+
code?: string;
|
|
37
|
+
/** Structured detail carried into the envelope (e.g. `{ issues }`). */
|
|
38
|
+
context?: Record<string, unknown>;
|
|
39
|
+
/** Underlying cause, preserved for server-side logging (never serialized). */
|
|
40
|
+
cause?: unknown;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A throwable HTTP error carrying its status + canonical-envelope fields. A thin,
|
|
44
|
+
* status-first subclass of cwip's {@link AppError} — `new ApiError(404, 'nope',
|
|
45
|
+
* { code, context })` is the ergonomic form every handler/middleware/plugin
|
|
46
|
+
* service uses; the inherited `getSummary()` yields the rich `AppErrorSummary` so
|
|
47
|
+
* `toErrorEnvelope` maps it to the response with no extra work. On the client,
|
|
48
|
+
* {@link ApiError.fromEnvelope} rebuilds it from a parsed error body so callers
|
|
49
|
+
* can branch on `.status`/`.code`/`.context`.
|
|
50
|
+
*/
|
|
51
|
+
export declare class ApiError extends AppError {
|
|
52
|
+
/**
|
|
53
|
+
* Always set — narrows the base's optional `status` (an `ApiError` is defined
|
|
54
|
+
* by its HTTP status). The value is assigned by `AppError`'s constructor from
|
|
55
|
+
* the `status` we pass through; `declare` just re-types it without re-emitting
|
|
56
|
+
* the field (so the base's assignment isn't shadowed under class-field semantics).
|
|
57
|
+
*/
|
|
58
|
+
readonly status: number;
|
|
59
|
+
constructor(status: number, message: string, options?: ApiErrorOptions);
|
|
60
|
+
/**
|
|
61
|
+
* Rebuild an `ApiError` from a parsed error envelope's `error` object — the
|
|
62
|
+
* client half of the contract (e.g. `createServiceClient`'s response parser).
|
|
63
|
+
* Reads only `message`/`code`/`context`, so it works against both the rich
|
|
64
|
+
* envelope and any legacy minimal body. Missing/absent envelope falls back to a
|
|
65
|
+
* status-derived message.
|
|
66
|
+
*/
|
|
67
|
+
static fromEnvelope(status: number, envelope?: Partial<AppErrorSummary>): ApiError;
|
|
68
|
+
}
|
|
69
|
+
/** Type guard for {@link ApiError} (the status-first server throwable). */
|
|
70
|
+
export declare const isApiError: (value: unknown) => value is ApiError;
|
|
71
|
+
/**
|
|
72
|
+
* One field-level validation error on the wire. `path` is the dotted Zod path to
|
|
73
|
+
* the offending field (`''` for a top-level/whole-body issue) — it mirrors
|
|
74
|
+
* `z.ZodIssue.path`, so server code maps 1:1 and clients read a stable name.
|
|
75
|
+
*/
|
|
76
|
+
export interface ValidationIssue {
|
|
77
|
+
/** Dotted path to the offending field (`''` for a top-level issue). */
|
|
78
|
+
path: string;
|
|
79
|
+
/** Human-readable, zod-authored message. */
|
|
80
|
+
message: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The canonical machine code carried in a validation envelope's `error.code`, so a
|
|
84
|
+
* client can branch on it without string-matching the (localizable) message.
|
|
85
|
+
*/
|
|
86
|
+
export declare const VALIDATION_ERROR_CODE = "validation";
|
|
87
|
+
/** The HTTP status every validation failure maps to (both emitters respond 400). */
|
|
88
|
+
export declare const VALIDATION_STATUS = 400;
|
|
89
|
+
/** Map a {@link https://zod.dev | zod} error's issues to the wire {@link ValidationIssue}s. */
|
|
90
|
+
export declare function toValidationIssues(error: ZodError): ValidationIssue[];
|
|
91
|
+
/**
|
|
92
|
+
* Build the canonical validation envelope from a zod error (or pre-mapped issues).
|
|
93
|
+
* It constructs an {@link ApiError} (status {@link VALIDATION_STATUS}, code
|
|
94
|
+
* {@link VALIDATION_ERROR_CODE}, `context.issues`) and runs it through the shared
|
|
95
|
+
* {@link toErrorEnvelope}, so a validation failure is shaped identically to every
|
|
96
|
+
* other error on the wire — `{ error: { …, code: 'validation', status: 400,
|
|
97
|
+
* context: { issues } } }`. This is the single builder — both cursedbelt's
|
|
98
|
+
* `zValidator` middleware and cursedshell's `buildServiceRouter` call it, so a
|
|
99
|
+
* client parses validation failures identically no matter which layer emitted them.
|
|
100
|
+
*/
|
|
101
|
+
export declare function validationEnvelope(errorOrIssues: ZodError | ValidationIssue[], message?: string): ErrorEnvelope;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical server error class + error/validation envelope for the whole
|
|
3
|
+
* monorepo — cursedbelt apps AND cursedshell plugins.
|
|
4
|
+
*
|
|
5
|
+
* Every API failure — thrown deep in a handler, surfaced by middleware, or raised
|
|
6
|
+
* by a plugin service — is shaped into ONE wire contract, the **rich** envelope
|
|
7
|
+
* defined once in cwip (`cwip/error`):
|
|
8
|
+
*
|
|
9
|
+
* { "error": { "name", "message", "code?", "status?", "category?", "context?",
|
|
10
|
+
* "isOperational", "timestamp" }, "correlationId?": "…" }
|
|
11
|
+
*
|
|
12
|
+
* Clients parse exactly this shape, so there is no per-route error drift — and no
|
|
13
|
+
* drift between a cwip-based server (which already emits it via `cwip/server`'s
|
|
14
|
+
* `errorHandler`) and a cursedshell one. The extra fields are additive:
|
|
15
|
+
* a client that only reads `message`/`code`/`context` keeps working unchanged.
|
|
16
|
+
*
|
|
17
|
+
* `ApiError` is the ergonomic throwable: `throw new ApiError(404, 'not found')`.
|
|
18
|
+
* It is a thin subclass of cwip's `AppError` with the status-first constructor
|
|
19
|
+
* shape every plugin already uses, so a single `getSummary()`/`toErrorEnvelope`
|
|
20
|
+
* produces the rich body. The `errorEnvelope` Hono `onError` handler
|
|
21
|
+
* (cursedbelt-server) turns it (and any other thrown value) into the envelope +
|
|
22
|
+
* the right HTTP status; {@link ApiError.fromEnvelope} rebuilds one on the CLIENT
|
|
23
|
+
* from a parsed error body.
|
|
24
|
+
*
|
|
25
|
+
* This module is **framework-free** (no Hono, no DOM, no server deps — it imports
|
|
26
|
+
* only cwip's framework-free `cwip/error` and a type-only `zod` for the validation
|
|
27
|
+
* builder), so it is safe to import from browser bundles and from cursedshell's
|
|
28
|
+
* plugin-pure layer via the `cursedbelt-core/errors` subpath. Server code reaches the
|
|
29
|
+
* same symbols through `cursedbelt-server` (which re-exports this module).
|
|
30
|
+
*/
|
|
31
|
+
import { AppError, isAppError, toErrorEnvelope, } from 'cwip/error';
|
|
32
|
+
// ── Re-export the ONE canonical envelope + formatter (defined in cwip) ────────
|
|
33
|
+
// So `cursedbelt-core/errors` and `cwip/error` speak the exact same wire contract.
|
|
34
|
+
export { AppError, isAppError, toErrorEnvelope };
|
|
35
|
+
/**
|
|
36
|
+
* A throwable HTTP error carrying its status + canonical-envelope fields. A thin,
|
|
37
|
+
* status-first subclass of cwip's {@link AppError} — `new ApiError(404, 'nope',
|
|
38
|
+
* { code, context })` is the ergonomic form every handler/middleware/plugin
|
|
39
|
+
* service uses; the inherited `getSummary()` yields the rich `AppErrorSummary` so
|
|
40
|
+
* `toErrorEnvelope` maps it to the response with no extra work. On the client,
|
|
41
|
+
* {@link ApiError.fromEnvelope} rebuilds it from a parsed error body so callers
|
|
42
|
+
* can branch on `.status`/`.code`/`.context`.
|
|
43
|
+
*/
|
|
44
|
+
export class ApiError extends AppError {
|
|
45
|
+
constructor(status, message, options = {}) {
|
|
46
|
+
super(message, {
|
|
47
|
+
status,
|
|
48
|
+
code: options.code,
|
|
49
|
+
context: options.context,
|
|
50
|
+
cause: options.cause,
|
|
51
|
+
});
|
|
52
|
+
// `AppError` sets `this.name = new.target.name` → 'ApiError' already.
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Rebuild an `ApiError` from a parsed error envelope's `error` object — the
|
|
56
|
+
* client half of the contract (e.g. `createServiceClient`'s response parser).
|
|
57
|
+
* Reads only `message`/`code`/`context`, so it works against both the rich
|
|
58
|
+
* envelope and any legacy minimal body. Missing/absent envelope falls back to a
|
|
59
|
+
* status-derived message.
|
|
60
|
+
*/
|
|
61
|
+
static fromEnvelope(status, envelope) {
|
|
62
|
+
return new ApiError(status, envelope?.message ?? `Request failed with status ${status}`, {
|
|
63
|
+
code: envelope?.code,
|
|
64
|
+
context: envelope?.context,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Type guard for {@link ApiError} (the status-first server throwable). */
|
|
69
|
+
export const isApiError = (value) => value instanceof ApiError;
|
|
70
|
+
/**
|
|
71
|
+
* The canonical machine code carried in a validation envelope's `error.code`, so a
|
|
72
|
+
* client can branch on it without string-matching the (localizable) message.
|
|
73
|
+
*/
|
|
74
|
+
export const VALIDATION_ERROR_CODE = 'validation';
|
|
75
|
+
/** The HTTP status every validation failure maps to (both emitters respond 400). */
|
|
76
|
+
export const VALIDATION_STATUS = 400;
|
|
77
|
+
/** The default client-facing message when validation fails. */
|
|
78
|
+
const DEFAULT_VALIDATION_MESSAGE = 'Validation failed';
|
|
79
|
+
/** Map a {@link https://zod.dev | zod} error's issues to the wire {@link ValidationIssue}s. */
|
|
80
|
+
export function toValidationIssues(error) {
|
|
81
|
+
return error.issues.map((issue) => ({
|
|
82
|
+
path: issue.path.map((segment) => String(segment)).join('.'),
|
|
83
|
+
message: issue.message,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the canonical validation envelope from a zod error (or pre-mapped issues).
|
|
88
|
+
* It constructs an {@link ApiError} (status {@link VALIDATION_STATUS}, code
|
|
89
|
+
* {@link VALIDATION_ERROR_CODE}, `context.issues`) and runs it through the shared
|
|
90
|
+
* {@link toErrorEnvelope}, so a validation failure is shaped identically to every
|
|
91
|
+
* other error on the wire — `{ error: { …, code: 'validation', status: 400,
|
|
92
|
+
* context: { issues } } }`. This is the single builder — both cursedbelt's
|
|
93
|
+
* `zValidator` middleware and cursedshell's `buildServiceRouter` call it, so a
|
|
94
|
+
* client parses validation failures identically no matter which layer emitted them.
|
|
95
|
+
*/
|
|
96
|
+
export function validationEnvelope(errorOrIssues, message = DEFAULT_VALIDATION_MESSAGE) {
|
|
97
|
+
const issues = Array.isArray(errorOrIssues) ? errorOrIssues : toValidationIssues(errorOrIssues);
|
|
98
|
+
return toErrorEnvelope(new ApiError(VALIDATION_STATUS, message, { code: VALIDATION_ERROR_CODE, context: { issues } }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Uint8Array` → a body the DOM types accept, in ONE place.
|
|
3
|
+
*
|
|
4
|
+
* ## The problem this exists to stop being solved five different ways
|
|
5
|
+
*
|
|
6
|
+
* TypeScript 5.7 made `Uint8Array` generic in its buffer: `Uint8Array<ArrayBufferLike>`.
|
|
7
|
+
* `BodyInit` accepts `ArrayBufferView<ArrayBuffer>` — an ArrayBuffer view, NOT a
|
|
8
|
+
* possibly-shared one — so `new Response(bytes)` and `fetch(url, { body: bytes })`
|
|
9
|
+
* stopped compiling for every plain `new Uint8Array([…])` in the repo. Nothing
|
|
10
|
+
* about the runtime changed; the types got more precise about `SharedArrayBuffer`.
|
|
11
|
+
*
|
|
12
|
+
* Two fixes suggest themselves and one of them is a bug:
|
|
13
|
+
*
|
|
14
|
+
* * `bytes.buffer as ArrayBuffer` — **wrong in general.** `.buffer` is the
|
|
15
|
+
* WHOLE underlying buffer, so for any view that does not span it (`subarray`,
|
|
16
|
+
* a slice of a decoder's output, a chunk read off a stream) this silently
|
|
17
|
+
* sends more bytes than the caller holds. It happens to be right for a
|
|
18
|
+
* literal `new Uint8Array([1,2,3,4])` and that is exactly what makes it a
|
|
19
|
+
* trap: it works everywhere it is first tried.
|
|
20
|
+
* * `bytes as Uint8Array<ArrayBuffer>` — correct, and what this does. It
|
|
21
|
+
* asserts the one thing the caller actually knows (this buffer is not
|
|
22
|
+
* shared), changes no lengths, and keeps the view's own offset.
|
|
23
|
+
*
|
|
24
|
+
* Test-only by intent: production code that needs to send bytes should hold a
|
|
25
|
+
* correctly-typed buffer rather than assert one.
|
|
26
|
+
*/
|
|
27
|
+
/** A `Uint8Array` as a `BodyInit`, preserving byte offset and length. */
|
|
28
|
+
export declare const asBody: (bytes: Uint8Array) => Uint8Array<ArrayBuffer>;
|
|
29
|
+
/** `new Response(bytes)` with the assertion applied — the common case. */
|
|
30
|
+
export declare const bytesResponse: (bytes: Uint8Array, init?: ResponseInit) => Response;
|
|
31
|
+
/** A readable stream of `bytes` — what the media catalogue's ingest paths take. */
|
|
32
|
+
export declare const bytesStream: (bytes: Uint8Array | string) => ReadableStream<Uint8Array>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Uint8Array` → a body the DOM types accept, in ONE place.
|
|
3
|
+
*
|
|
4
|
+
* ## The problem this exists to stop being solved five different ways
|
|
5
|
+
*
|
|
6
|
+
* TypeScript 5.7 made `Uint8Array` generic in its buffer: `Uint8Array<ArrayBufferLike>`.
|
|
7
|
+
* `BodyInit` accepts `ArrayBufferView<ArrayBuffer>` — an ArrayBuffer view, NOT a
|
|
8
|
+
* possibly-shared one — so `new Response(bytes)` and `fetch(url, { body: bytes })`
|
|
9
|
+
* stopped compiling for every plain `new Uint8Array([…])` in the repo. Nothing
|
|
10
|
+
* about the runtime changed; the types got more precise about `SharedArrayBuffer`.
|
|
11
|
+
*
|
|
12
|
+
* Two fixes suggest themselves and one of them is a bug:
|
|
13
|
+
*
|
|
14
|
+
* * `bytes.buffer as ArrayBuffer` — **wrong in general.** `.buffer` is the
|
|
15
|
+
* WHOLE underlying buffer, so for any view that does not span it (`subarray`,
|
|
16
|
+
* a slice of a decoder's output, a chunk read off a stream) this silently
|
|
17
|
+
* sends more bytes than the caller holds. It happens to be right for a
|
|
18
|
+
* literal `new Uint8Array([1,2,3,4])` and that is exactly what makes it a
|
|
19
|
+
* trap: it works everywhere it is first tried.
|
|
20
|
+
* * `bytes as Uint8Array<ArrayBuffer>` — correct, and what this does. It
|
|
21
|
+
* asserts the one thing the caller actually knows (this buffer is not
|
|
22
|
+
* shared), changes no lengths, and keeps the view's own offset.
|
|
23
|
+
*
|
|
24
|
+
* Test-only by intent: production code that needs to send bytes should hold a
|
|
25
|
+
* correctly-typed buffer rather than assert one.
|
|
26
|
+
*/
|
|
27
|
+
/** A `Uint8Array` as a `BodyInit`, preserving byte offset and length. */
|
|
28
|
+
export const asBody = (bytes) => bytes;
|
|
29
|
+
/** `new Response(bytes)` with the assertion applied — the common case. */
|
|
30
|
+
export const bytesResponse = (bytes, init) => new Response(asBody(bytes), init);
|
|
31
|
+
/** A readable stream of `bytes` — what the media catalogue's ingest paths take. */
|
|
32
|
+
export const bytesStream = (bytes) => new Response(typeof bytes === 'string' ? asBody(new TextEncoder().encode(bytes)) : asBody(bytes))
|
|
33
|
+
.body;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make a happy-dom node CHEAP to serialize, for every `bun test` process in this
|
|
3
|
+
* repo. Imported by `packages/cursedbelt/happydom.ts` and
|
|
4
|
+
* `packages/cursedshell/happydom.ts` — the two test preloads — so it is the ONE
|
|
5
|
+
* place this behavior is defined.
|
|
6
|
+
*
|
|
7
|
+
* ## The bug this exists to not have
|
|
8
|
+
*
|
|
9
|
+
* `expect(received)` builds its failure message EAGERLY, before anything decides
|
|
10
|
+
* whether that message will ever be read. When `received` is a happy-dom node,
|
|
11
|
+
* Bun's inspector walks the node's own object graph — and a happy-dom node holds
|
|
12
|
+
* `[Symbol(parentNode)]`, so the walk goes UPWARD through the parent chain into
|
|
13
|
+
* `HTMLBodyElement` → `HTMLHtmlElement` → `HTMLDocument` → the window, and back
|
|
14
|
+
* down through every sibling subtree it meets on the way.
|
|
15
|
+
*
|
|
16
|
+
* Measured on this repo's own studio spec (2026-07-26):
|
|
17
|
+
*
|
|
18
|
+
* | received value | cost of ONE failing `expect` |
|
|
19
|
+
* |---------------------------------------|------------------------------|
|
|
20
|
+
* | `true` | 0.1 ms |
|
|
21
|
+
* | a plain object | 0.0 ms |
|
|
22
|
+
* | a detached empty `<div>` | 9.8 ms |
|
|
23
|
+
* | an ATTACHED empty `<div>` | 29.8 ms |
|
|
24
|
+
* | an attached `<div>` with 50 children | **1,576 ms** |
|
|
25
|
+
*
|
|
26
|
+
* That cost is paid on assertions that FAIL, which sounds rare until you notice
|
|
27
|
+
* that `waitFor(() => expect(el).toBeNull())` fails on purpose on every poll
|
|
28
|
+
* until the condition comes true. The studio spec's `settled()` helper spent
|
|
29
|
+
* ~790 ms per mount building error strings that `waitFor` then threw away — 12
|
|
30
|
+
* mounts, ~9.5 s of a 10.5 s file, and on the CI runner the same two assertions
|
|
31
|
+
* blew through Bun's 5,000 ms per-test timeout and turned `main` red.
|
|
32
|
+
*
|
|
33
|
+
* ## The fix
|
|
34
|
+
*
|
|
35
|
+
* Install `nodejs.util.inspect.custom` on happy-dom's `Node.prototype` (the
|
|
36
|
+
* common ancestor of every element, text node, fragment and document), returning
|
|
37
|
+
* TRUNCATED `outerHTML`. Bun's inspector honors that hook and stops walking.
|
|
38
|
+
* Same measurement, after: **1,576 ms → 0.1 ms.**
|
|
39
|
+
*
|
|
40
|
+
* The message gets BETTER as well as faster. What it replaces is not an HTML
|
|
41
|
+
* dump; it is a `[Symbol(listeners)] / [Symbol(parentNode)]` object graph that
|
|
42
|
+
* scrolls for thousands of lines and never shows the markup. `outerHTML` is what
|
|
43
|
+
* someone reading a failure actually wants.
|
|
44
|
+
*
|
|
45
|
+
* ## What this deliberately does NOT do
|
|
46
|
+
*
|
|
47
|
+
* It does not touch `prettyDOM`, `screen.debug()` or any Testing Library output
|
|
48
|
+
* — those have their own serializer and are called only when someone asks for
|
|
49
|
+
* them, so they were never the cost. Only the implicit `expect`/`console.log`
|
|
50
|
+
* path changes.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Install the cheap inspector. Idempotent, and a no-op when happy-dom has not
|
|
54
|
+
* registered its globals (a pure-logic preload has no `Node`).
|
|
55
|
+
*
|
|
56
|
+
* Call AFTER `GlobalRegistrator.register()` — before it, `globalThis.Node` is
|
|
57
|
+
* whatever Bun ships and patching it would miss the class the specs actually get.
|
|
58
|
+
*/
|
|
59
|
+
export declare function installCheapDomInspect(): void;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make a happy-dom node CHEAP to serialize, for every `bun test` process in this
|
|
3
|
+
* repo. Imported by `packages/cursedbelt/happydom.ts` and
|
|
4
|
+
* `packages/cursedshell/happydom.ts` — the two test preloads — so it is the ONE
|
|
5
|
+
* place this behavior is defined.
|
|
6
|
+
*
|
|
7
|
+
* ## The bug this exists to not have
|
|
8
|
+
*
|
|
9
|
+
* `expect(received)` builds its failure message EAGERLY, before anything decides
|
|
10
|
+
* whether that message will ever be read. When `received` is a happy-dom node,
|
|
11
|
+
* Bun's inspector walks the node's own object graph — and a happy-dom node holds
|
|
12
|
+
* `[Symbol(parentNode)]`, so the walk goes UPWARD through the parent chain into
|
|
13
|
+
* `HTMLBodyElement` → `HTMLHtmlElement` → `HTMLDocument` → the window, and back
|
|
14
|
+
* down through every sibling subtree it meets on the way.
|
|
15
|
+
*
|
|
16
|
+
* Measured on this repo's own studio spec (2026-07-26):
|
|
17
|
+
*
|
|
18
|
+
* | received value | cost of ONE failing `expect` |
|
|
19
|
+
* |---------------------------------------|------------------------------|
|
|
20
|
+
* | `true` | 0.1 ms |
|
|
21
|
+
* | a plain object | 0.0 ms |
|
|
22
|
+
* | a detached empty `<div>` | 9.8 ms |
|
|
23
|
+
* | an ATTACHED empty `<div>` | 29.8 ms |
|
|
24
|
+
* | an attached `<div>` with 50 children | **1,576 ms** |
|
|
25
|
+
*
|
|
26
|
+
* That cost is paid on assertions that FAIL, which sounds rare until you notice
|
|
27
|
+
* that `waitFor(() => expect(el).toBeNull())` fails on purpose on every poll
|
|
28
|
+
* until the condition comes true. The studio spec's `settled()` helper spent
|
|
29
|
+
* ~790 ms per mount building error strings that `waitFor` then threw away — 12
|
|
30
|
+
* mounts, ~9.5 s of a 10.5 s file, and on the CI runner the same two assertions
|
|
31
|
+
* blew through Bun's 5,000 ms per-test timeout and turned `main` red.
|
|
32
|
+
*
|
|
33
|
+
* ## The fix
|
|
34
|
+
*
|
|
35
|
+
* Install `nodejs.util.inspect.custom` on happy-dom's `Node.prototype` (the
|
|
36
|
+
* common ancestor of every element, text node, fragment and document), returning
|
|
37
|
+
* TRUNCATED `outerHTML`. Bun's inspector honors that hook and stops walking.
|
|
38
|
+
* Same measurement, after: **1,576 ms → 0.1 ms.**
|
|
39
|
+
*
|
|
40
|
+
* The message gets BETTER as well as faster. What it replaces is not an HTML
|
|
41
|
+
* dump; it is a `[Symbol(listeners)] / [Symbol(parentNode)]` object graph that
|
|
42
|
+
* scrolls for thousands of lines and never shows the markup. `outerHTML` is what
|
|
43
|
+
* someone reading a failure actually wants.
|
|
44
|
+
*
|
|
45
|
+
* ## What this deliberately does NOT do
|
|
46
|
+
*
|
|
47
|
+
* It does not touch `prettyDOM`, `screen.debug()` or any Testing Library output
|
|
48
|
+
* — those have their own serializer and are called only when someone asks for
|
|
49
|
+
* them, so they were never the cost. Only the implicit `expect`/`console.log`
|
|
50
|
+
* path changes.
|
|
51
|
+
*/
|
|
52
|
+
/** Longest markup we will put in a failure message before eliding the middle. */
|
|
53
|
+
const MAX_MARKUP = 800;
|
|
54
|
+
/** Node.nodeType constants, spelled out so this file needs no DOM lib types. */
|
|
55
|
+
const ELEMENT_NODE = 1;
|
|
56
|
+
const TEXT_NODE = 3;
|
|
57
|
+
const COMMENT_NODE = 8;
|
|
58
|
+
const DOCUMENT_NODE = 9;
|
|
59
|
+
const DOCUMENT_FRAGMENT_NODE = 11;
|
|
60
|
+
const INSPECT = Symbol.for('nodejs.util.inspect.custom');
|
|
61
|
+
/** Keep the head and the tail: the opening tag says what it is, the closing tag
|
|
62
|
+
* says where it ended, and the middle of a 40 KB body is never the useful part. */
|
|
63
|
+
function elide(markup) {
|
|
64
|
+
if (markup.length <= MAX_MARKUP)
|
|
65
|
+
return markup;
|
|
66
|
+
const head = markup.slice(0, MAX_MARKUP - 120);
|
|
67
|
+
const tail = markup.slice(-100);
|
|
68
|
+
return `${head}\n … ${markup.length - MAX_MARKUP + 20} more chars …\n${tail}`;
|
|
69
|
+
}
|
|
70
|
+
function describe(node) {
|
|
71
|
+
switch (node.nodeType) {
|
|
72
|
+
case ELEMENT_NODE:
|
|
73
|
+
return elide(node.outerHTML ?? `<${(node.nodeName ?? 'element').toLowerCase()} />`);
|
|
74
|
+
case TEXT_NODE:
|
|
75
|
+
return `#text ${JSON.stringify(node.data ?? '')}`;
|
|
76
|
+
case COMMENT_NODE:
|
|
77
|
+
return `<!--${node.data ?? ''}-->`;
|
|
78
|
+
case DOCUMENT_NODE:
|
|
79
|
+
return '#document';
|
|
80
|
+
case DOCUMENT_FRAGMENT_NODE:
|
|
81
|
+
return elide(`#document-fragment ${node.innerHTML ?? `(${node.childNodes?.length ?? 0} children)`}`);
|
|
82
|
+
default:
|
|
83
|
+
return `${node.nodeName ?? 'Node'}(nodeType ${node.nodeType})`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Install the cheap inspector. Idempotent, and a no-op when happy-dom has not
|
|
88
|
+
* registered its globals (a pure-logic preload has no `Node`).
|
|
89
|
+
*
|
|
90
|
+
* Call AFTER `GlobalRegistrator.register()` — before it, `globalThis.Node` is
|
|
91
|
+
* whatever Bun ships and patching it would miss the class the specs actually get.
|
|
92
|
+
*/
|
|
93
|
+
export function installCheapDomInspect() {
|
|
94
|
+
const NodeCtor = globalThis.Node;
|
|
95
|
+
const proto = NodeCtor?.prototype;
|
|
96
|
+
if (!proto)
|
|
97
|
+
return;
|
|
98
|
+
if (Object.hasOwn(proto, INSPECT))
|
|
99
|
+
return; // already installed
|
|
100
|
+
Object.defineProperty(proto, INSPECT, {
|
|
101
|
+
value() {
|
|
102
|
+
// An inspector that throws turns a readable assertion failure into an
|
|
103
|
+
// unreadable one, so every branch here is best-effort.
|
|
104
|
+
try {
|
|
105
|
+
return describe(this);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return '[unserializable DOM node]';
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
configurable: true,
|
|
112
|
+
enumerable: false,
|
|
113
|
+
writable: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `fetch` stand-in that actually satisfies `typeof fetch`.
|
|
3
|
+
*
|
|
4
|
+
* `typeof fetch` is not just a call signature: the DOM lib declares `preconnect`
|
|
5
|
+
* on it, so a bare `async (input) => new Response(…)` is NOT assignable and
|
|
6
|
+
* `as typeof fetch` is refused outright ("neither type sufficiently overlaps").
|
|
7
|
+
* The reflexive workaround is `as unknown as typeof fetch`, and that is the
|
|
8
|
+
* pattern worth stopping: a double assertion silences THIS mismatch and every
|
|
9
|
+
* future one, including a genuine argument-shape error in the fake.
|
|
10
|
+
*
|
|
11
|
+
* This attaches the missing member instead. The result is a real value of the
|
|
12
|
+
* type, so the fake's own `(input, init)` signature stays checked — which is the
|
|
13
|
+
* whole reason to type a fake at all.
|
|
14
|
+
*/
|
|
15
|
+
/** The part of `fetch` a test actually implements. */
|
|
16
|
+
export type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a fake so it is a complete `typeof fetch`.
|
|
19
|
+
*
|
|
20
|
+
* `preconnect` is a no-op: it is a resource HINT with no observable result, so a
|
|
21
|
+
* test that triggered one and got nothing has lost nothing.
|
|
22
|
+
*/
|
|
23
|
+
export declare const fakeFetch: (impl: FetchImpl) => typeof fetch;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `fetch` stand-in that actually satisfies `typeof fetch`.
|
|
3
|
+
*
|
|
4
|
+
* `typeof fetch` is not just a call signature: the DOM lib declares `preconnect`
|
|
5
|
+
* on it, so a bare `async (input) => new Response(…)` is NOT assignable and
|
|
6
|
+
* `as typeof fetch` is refused outright ("neither type sufficiently overlaps").
|
|
7
|
+
* The reflexive workaround is `as unknown as typeof fetch`, and that is the
|
|
8
|
+
* pattern worth stopping: a double assertion silences THIS mismatch and every
|
|
9
|
+
* future one, including a genuine argument-shape error in the fake.
|
|
10
|
+
*
|
|
11
|
+
* This attaches the missing member instead. The result is a real value of the
|
|
12
|
+
* type, so the fake's own `(input, init)` signature stays checked — which is the
|
|
13
|
+
* whole reason to type a fake at all.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Wrap a fake so it is a complete `typeof fetch`.
|
|
17
|
+
*
|
|
18
|
+
* `preconnect` is a no-op: it is a resource HINT with no observable result, so a
|
|
19
|
+
* test that triggered one and got nothing has lost nothing.
|
|
20
|
+
*/
|
|
21
|
+
export const fakeFetch = (impl) => Object.assign(impl, { preconnect: () => undefined });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// cursedbelt-core/testing — test-only helpers shared across every `bun test` process
|
|
2
|
+
// in the fleet. Its own subpath so nothing test-related is reachable from the
|
|
3
|
+
// runtime entrypoints.
|
|
4
|
+
//
|
|
5
|
+
// Exposed on 2026-08-02 so consumers (e.g. cursed-satellites' happydom preload)
|
|
6
|
+
// can `import { installCheapDomInspect } from "cursedbelt-core/testing"` instead of
|
|
7
|
+
// keeping a ~60-line hand-port of domInspect.ts — the file already lived here,
|
|
8
|
+
// it just had no export.
|
|
9
|
+
export { installCheapDomInspect } from "./domInspect";
|
|
10
|
+
export { asBody, bytesResponse, bytesStream } from "./bytes";
|
|
11
|
+
export { fakeFetch } from "./fakeFetch";
|