@realuptime/errors 0.4.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/types.ts ADDED
@@ -0,0 +1,293 @@
1
+ /**
2
+ * The Errors wire contract (docs/errors-plan.md, "The SDKs: two
3
+ * languages, one contract").
4
+ *
5
+ * DUPLICATED, not imported. Same convention as apps/agent/types.ts and for
6
+ * the same reason: this package ships into customer bundles and processes,
7
+ * so its dependency graph must stay auditable at a glance -- zero imports
8
+ * beyond the platform. The cost (a server-side rename does not break this
9
+ * build) is paid by wire-contract.test.ts, which pins every field name, and
10
+ * by the shared scrub vectors (scrub-vectors.json), which pin the scrub
11
+ * semantics across this SDK, the Python SDK, and the server's second net.
12
+ * The server-side mirror of these limits lives in
13
+ * packages/db/error-ingest.ts.
14
+ *
15
+ * ## v2 (REA-182): identity, tags, custom context, device metadata
16
+ *
17
+ * STRICTLY ADDITIVE, and that word is load-bearing rather than decorative.
18
+ * Every v1 field keeps its name, its type and its meaning; every v2 field is
19
+ * OPTIONAL and absent from a v1 payload. A v1 batch produced by an SDK that
20
+ * predates this file still ingests byte-for-byte unchanged, which
21
+ * route.test.ts pins with a frozen golden v1 body.
22
+ *
23
+ * **There is deliberately no `version` field on the wire, and that is a
24
+ * decision, not an omission.** A version discriminant would have to be
25
+ * REQUIRED to be worth reading, and a required field is by construction not
26
+ * additive: every already-deployed SDK in a customer's process omits it, so
27
+ * the server would have to treat "absent" as v1 anyway -- which is exactly
28
+ * what optional-field presence already tells it, with no field to keep in
29
+ * sync. The `sdk` string ("realuptime-errors-js/0.3.0") remains the honest
30
+ * record of what produced a payload. A future change that is NOT additive
31
+ * (a removed field, a changed meaning) gets a versioned PATH, the way the
32
+ * fingerprint algorithm gets a version rather than mutating history.
33
+ */
34
+
35
+ /** One stack frame, post-scrub. `inApp` is the SDK's best-effort guess
36
+ * (Node: not under node_modules and not an internal frame); the server
37
+ * trusts it for display and for fingerprinting. */
38
+ export interface WireFrame {
39
+ file: string;
40
+ function: string | null;
41
+ line: number | null;
42
+ inApp: boolean;
43
+ /** 1-based column when the runtime provided one (Phase 3). Source-map
44
+ * symbolication needs it; a frame without one renders raw server-side
45
+ * with a stated reason, never silently pretty. */
46
+ column?: number | null;
47
+ /** Source context, captured client-side where the SDK can read source
48
+ * (the Python SDK); pattern-scrubbed before serialization. */
49
+ contextLine?: string | null;
50
+ preContext?: string[];
51
+ postContext?: string[];
52
+ /** v2 (REA-182): this frame's local variables, stringified.
53
+ *
54
+ * OFF BY DEFAULT in both SDKs and hard-capped when on
55
+ * (MAX_LOCAL_VARS_PER_FRAME entries, MAX_LOCAL_VAR_LENGTH characters
56
+ * each), because locals are the single leakiest thing an error tracker
57
+ * can capture: the variable holding the password is, definitionally, in
58
+ * scope at the frame that failed to use it. Values pass the pattern scrub
59
+ * like every other captured string, but the pattern scrub is a net, not a
60
+ * guarantee, which is why the switch defaults to off rather than to
61
+ * "trust the regexes".
62
+ *
63
+ * Captured by the Python SDK only, the same asymmetry `contextLine`
64
+ * already has: a JS stack is a STRING, and the runtime hands the SDK no
65
+ * access to the frames behind it. The field is in the contract on both
66
+ * sides so the scrub, the storage and the UI are one implementation. */
67
+ vars?: Record<string, string> | null;
68
+ }
69
+
70
+ /** v2 (REA-182): who this happened to.
71
+ *
72
+ * `id` is the field the product is built around: an opaque account or user
73
+ * identifier is what makes "how many users hit this" answerable, and it is
74
+ * the customer's own key space, not a person's contact details. It rides
75
+ * the wire as-is (pattern-scrubbed like any other string).
76
+ *
77
+ * `email` and `username` are PERSONAL DATA and are `[scrubbed]` by default,
78
+ * client-side, before serialization. A customer who genuinely needs them
79
+ * opts each one back in BY NAME in SDK config (`allowFields: ["user.email"]`)
80
+ * -- the same per-field, in-code, never-a-global-switch shape the removed
81
+ * headers use, and for the same reason: a one-switch opt-out is how a
82
+ * default dies. There is no server-side way to turn this on, deliberately:
83
+ * a default that a dashboard toggle can disable is a default that leaks
84
+ * before anyone notices. */
85
+ export interface WireUser {
86
+ id?: string | null;
87
+ email?: string | null;
88
+ username?: string | null;
89
+ }
90
+
91
+ /** v2 (REA-182): where it ran. Runtime and platform facts the SDK reads out
92
+ * of its own process, never out of the request.
93
+ *
94
+ * Deliberately NOT here: hostname, local IP, container id, or anything else
95
+ * that identifies a machine rather than a runtime. Those are operational
96
+ * identifiers with real re-identification value and no debugging value that
97
+ * `release` + `environment` does not already carry. */
98
+ export interface WireDevice {
99
+ /** "node", "python", "browser". */
100
+ runtime?: string;
101
+ /** e.g. "22.4.0", "3.12.1". */
102
+ runtimeVersion?: string;
103
+ /** e.g. "linux", "darwin", "win32". */
104
+ platform?: string;
105
+ /** e.g. "x64", "arm64". */
106
+ arch?: string;
107
+ }
108
+
109
+ /** One breadcrumb (Phase 3): a scrubbed trail entry recorded before the
110
+ * event. Newest last on the wire. */
111
+ export interface WireBreadcrumb {
112
+ /** ISO 8601, the customer clock; display-only. */
113
+ timestamp: string | null;
114
+ category: string | null;
115
+ message: string;
116
+ data: Record<string, string> | null;
117
+ }
118
+
119
+ /** Minimal request context: method, path, status by default. Headers appear
120
+ * only for fields the integrator captured deliberately, and the scrub-by-
121
+ * default rules in scrub.ts have already run over everything here before it
122
+ * is serialized. */
123
+ export interface WireRequest {
124
+ method?: string;
125
+ path?: string;
126
+ status?: number;
127
+ headers?: Record<string, string>;
128
+ /** v2 (REA-182): the matched ROUTE TEMPLATE, e.g. "/orders/:id", as
129
+ * distinct from the concrete path "/orders/4242". This is the one extra
130
+ * thing a framework adapter genuinely knows and cannot be guessed from
131
+ * the path, it is low-cardinality (it groups), and it carries no
132
+ * user-supplied bytes at all. Adapters forward it and nothing else new:
133
+ * never headers, never cookies, never bodies. Pattern-scrubbed anyway,
134
+ * because a route template is still a string a customer wrote. */
135
+ route?: string;
136
+ }
137
+
138
+ export interface WireEvent {
139
+ /** ISO 8601, the customer process's clock. The server bounds it and never
140
+ * uses it for quota. */
141
+ occurredAt: string;
142
+ message: string;
143
+ exceptionType: string | null;
144
+ release: string | null;
145
+ environment: string | null;
146
+ frames: WireFrame[] | null;
147
+ request: WireRequest | null;
148
+ /** Custom grouping override (`fingerprint: [...]`), the escape hatch for
149
+ * the cases automatic grouping gets wrong. Null means "group for me". */
150
+ fingerprint: string[] | null;
151
+ /** The breadcrumb trail preceding this event, newest last, at most
152
+ * MAX_BREADCRUMBS_PER_EVENT (Phase 3). Null when none were recorded. */
153
+ breadcrumbs: WireBreadcrumb[] | null;
154
+ /** How many older breadcrumbs the bounded client-side ring had evicted
155
+ * when this event was built: the count of what the trail is NOT showing,
156
+ * reported so the server can render "showing the last N; M older
157
+ * dropped" instead of a quietly truncated trail. */
158
+ breadcrumbsDropped: number;
159
+
160
+ // ── v2 (REA-182), all optional and all absent from a v1 payload ───────
161
+
162
+ /** Who this happened to. See WireUser: id rides as-is, email and username
163
+ * are scrubbed unless opted in per field. */
164
+ user?: WireUser | null;
165
+ /** Short, low-cardinality labels the customer sets (`setTag`), the things
166
+ * an issues list can filter on: "tenant", "plan", "feature-flag". Values
167
+ * pass the pattern scrub. At most MAX_TAGS_PER_EVENT. */
168
+ tags?: Record<string, string> | null;
169
+ /** Free-form custom context the customer sets (`setContext`): the
170
+ * application state that makes an issue diagnosable and that no schema of
171
+ * ours could have anticipated. Values pass the pattern scrub. At most
172
+ * MAX_CONTEXT_ENTRIES. */
173
+ context?: Record<string, string> | null;
174
+ /** Runtime/platform metadata the SDK reads from its own process. */
175
+ device?: WireDevice | null;
176
+ /** REA-257: a client-minted UUID for this event, present ONLY when the
177
+ * integration opted into user feedback (`init({ feedback: {...} })`). It
178
+ * is what a later `POST /api/errors/v1/feedback/<key>` names to attach a
179
+ * user's report to this exact occurrence. An integration that never
180
+ * enables feedback keeps emitting the id-less payload it always did. */
181
+ eventId?: string;
182
+ }
183
+
184
+ export interface WireBatch {
185
+ /** e.g. "realuptime-errors-js/0.1.0" */
186
+ sdk: string;
187
+ /** Events this SDK dropped client-side (bounded buffer overflow) since the
188
+ * last successful delivery. Relayed so even client-side drops reach the
189
+ * product's visible drop counters -- never omitted, never reset without
190
+ * being reported. */
191
+ droppedClient: number;
192
+ events: WireEvent[];
193
+ }
194
+
195
+ /** Success body. `accepted` + `droppedOverQuota` always sum to the batch's
196
+ * event count; `overQuota: true` with `resetsAt` tells the SDK to stop
197
+ * sending until the window resets rather than burning the customer's cycles
198
+ * on refusals. */
199
+ export interface WireIngestResult {
200
+ accepted: number;
201
+ droppedOverQuota: number;
202
+ overQuota: boolean;
203
+ quota: { used: number; limit: number; resetsAt: string };
204
+ }
205
+
206
+ /** Machine-readable refusal reasons the SDK understands. Anything else is
207
+ * treated as transient.
208
+ *
209
+ * `oversize_context` (v2, RU-4009) is a 400 like `malformed`, and both SDKs
210
+ * already handle every 400 the same way -- drop the batch, log once, never
211
+ * retry -- so it needs no new client branch. It is a SEPARATE reason string
212
+ * because "your event carried 500 tags" and "your event was not valid JSON"
213
+ * are different bugs in the customer's integration, and telling them apart
214
+ * costs one string. */
215
+ export type WireRefusalReason =
216
+ | "over_quota"
217
+ | "rate_limited"
218
+ | "key_revoked"
219
+ | "malformed"
220
+ | "oversize_context";
221
+
222
+ // Mirrored by MAX_ERROR_EVENTS_PER_BATCH etc. in packages/db/error-ingest.ts
223
+ // and pinned by wire-contract.test.ts on both sides.
224
+ export const MAX_EVENTS_PER_BATCH = 50;
225
+ export const MAX_MESSAGE_LENGTH = 4000;
226
+ export const MAX_FRAMES_PER_EVENT = 50;
227
+ export const MAX_STRING_LENGTH = 512;
228
+ export const MAX_BREADCRUMBS_PER_EVENT = 20;
229
+ export const MAX_BREADCRUMB_DATA_ENTRIES = 10;
230
+
231
+ // ── v2 (REA-182) caps ───────────────────────────────────────────────────
232
+ //
233
+ // Two tiers, deliberately:
234
+ //
235
+ // SOFT caps (the constants below) are what an honest integration lives
236
+ // inside. Going past one is not an error: the server keeps the first N
237
+ // entries, COUNTS what it dropped, and stores the count so the issue
238
+ // detail can say "3 context entries were dropped (size cap)". Truncating
239
+ // silently is the exact lie this product was founded against, so the
240
+ // count is first-class data rather than a log line.
241
+ //
242
+ // The HARD bound (HARD_MAX_MAP_ENTRIES) is where a payload stops being an
243
+ // over-eager integration and starts being a bug or an attack; that one
244
+ // REFUSES the batch with RU-4009 rather than quietly keeping 20 of 5,000.
245
+
246
+ /** Tags per event kept; extras are dropped and counted. */
247
+ export const MAX_TAGS_PER_EVENT = 20;
248
+ /** Custom-context entries per event kept; extras are dropped and counted. */
249
+ export const MAX_CONTEXT_ENTRIES = 20;
250
+ /** Tag and context KEY length. Shorter than MAX_STRING_LENGTH because a key
251
+ * is a label, not a payload; a 512-character key is a value in disguise. */
252
+ export const MAX_CONTEXT_KEY_LENGTH = 64;
253
+ /** Local variables kept per frame when the (off-by-default) capture is on. */
254
+ export const MAX_LOCAL_VARS_PER_FRAME = 20;
255
+ /** Per-value cap for a local variable, tighter than MAX_STRING_LENGTH: a
256
+ * local is captured for its shape, not its contents. */
257
+ export const MAX_LOCAL_VAR_LENGTH = 256;
258
+ /** Combined serialized budget for user + tags + context + device on ONE
259
+ * event, enforced server-side inside the ingest transaction. Entries past
260
+ * it are dropped and counted, same honesty rule as the soft caps. */
261
+ export const MAX_CONTEXT_BYTES = 4096;
262
+ /** The refusal boundary: more entries than this in any one map is a bug in
263
+ * the caller, not an over-eager integration. */
264
+ export const HARD_MAX_MAP_ENTRIES = 200;
265
+
266
+ // ── REA-257: user feedback (the report dialog) ──────────────────────────
267
+ //
268
+ // Opt-in, customer-configured, and deliberately a SEPARATE wire message from
269
+ // the event batch: a user's own words about what they were doing are
270
+ // personal data the customer chose to collect, so they travel only when the
271
+ // customer turned the feature on, and they are scrubbed (the same pattern
272
+ // rules) before they leave the page. Mirrored by packages/db/error-feedback.ts
273
+ // and pinned by wire-contract.test.ts.
274
+
275
+ /** `POST /api/errors/v1/feedback/<key>` body. */
276
+ export interface WireFeedback {
277
+ /** The client-minted event id the report attaches to (WireEvent.eventId). */
278
+ eventId: string;
279
+ /** What the user typed. Scrubbed client-side before serialization. */
280
+ comments: string;
281
+ /** Optional, only sent when the dialog showed the field and the user
282
+ * filled it. Scrubbed by the same pattern rules. */
283
+ name?: string;
284
+ email?: string;
285
+ /** e.g. "realuptime-errors-js/0.4.0" */
286
+ sdk: string;
287
+ }
288
+
289
+ export const MAX_FEEDBACK_COMMENTS_LENGTH = 4000;
290
+ export const MAX_FEEDBACK_NAME_LENGTH = 200;
291
+ export const MAX_FEEDBACK_EMAIL_LENGTH = 254;
292
+ /** Body size ceiling the server enforces on the feedback endpoint. */
293
+ export const MAX_FEEDBACK_BODY_BYTES = 16_384;
package/vite.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { resolve } from "node:path";
2
+ import {
3
+ collectSourcemapFiles,
4
+ formatProgressLine,
5
+ preflightSourcemapFiles,
6
+ resolveSourcemapUploadUrl,
7
+ type SourcemapFile,
8
+ uploadSourcemaps,
9
+ type UploadSummary,
10
+ } from "./sourcemaps.ts";
11
+
12
+ /**
13
+ * Vite plugin (REA-254): uploads the `.map` files a production build wrote
14
+ * to its outDir, on `closeBundle`, to the project's source-map endpoint.
15
+ *
16
+ * // vite.config.ts
17
+ * import { realuptimeSourcemaps } from "@realuptime/errors/vite";
18
+ * export default defineConfig({
19
+ * build: { sourcemap: true },
20
+ * plugins: [
21
+ * realuptimeSourcemaps({
22
+ * release: process.env.RELEASE ?? process.env.GITHUB_SHA!,
23
+ * dsn: process.env.REALUPTIME_ERRORS_DSN,
24
+ * }),
25
+ * ],
26
+ * });
27
+ *
28
+ * It does not import "vite": the hook shapes are matched structurally, so
29
+ * this stays a zero-dependency file in a zero-dependency package (the same
30
+ * rule the framework adapters follow). It never changes the build output:
31
+ * no map stripping, no injected code, no release constant, nothing. It reads
32
+ * the maps Vite already wrote and posts them.
33
+ *
34
+ * Failure policy: an upload failure is logged loudly with the server's own
35
+ * message and the build still succeeds, because a deploy pipeline that
36
+ * cannot reach realuptime should not also fail to ship; set
37
+ * `failOnError: true` to make the build fail instead. Either way the
38
+ * failure is one clear line, never silence.
39
+ */
40
+
41
+ export interface SourcemapPluginOptions {
42
+ /** The release string the app reports at runtime (`init({ release })`).
43
+ * The maps are matched to events by this exact string. */
44
+ release: string;
45
+ /** The project DSN; `REALUPTIME_ERRORS_DSN` is read when omitted. */
46
+ dsn?: string;
47
+ /** A bare ingest key (`REALUPTIME_ERRORS_INGEST_KEY` when omitted), used
48
+ * with `origin` when no DSN is set. */
49
+ ingestKey?: string;
50
+ origin?: string;
51
+ /** Prefix artifact names with the public path the bundle is served from
52
+ * (e.g. `~/assets` or `static/js`), when the outDir layout does not match
53
+ * the URL path. Matching is by path suffix, so it is rarely needed. */
54
+ urlPrefix?: string;
55
+ /** Override the directory to collect from; defaults to the resolved
56
+ * build outDir. */
57
+ dir?: string;
58
+ /** Skip the upload without failing (e.g. local builds): when false, the
59
+ * plugin logs one line and does nothing. Default: upload whenever a
60
+ * credential is present. */
61
+ enabled?: boolean;
62
+ failOnError?: boolean;
63
+ /** Test seams. */
64
+ fetchImpl?: typeof fetch;
65
+ log?: (line: string) => void;
66
+ env?: Record<string, string | undefined>;
67
+ }
68
+
69
+ /** The slice of Vite's ResolvedConfig this plugin reads. */
70
+ interface ResolvedConfigLike {
71
+ root?: string;
72
+ build?: { outDir?: string };
73
+ }
74
+
75
+ export interface SourcemapPlugin {
76
+ name: string;
77
+ apply: "build";
78
+ enforce: "post";
79
+ configResolved(config: ResolvedConfigLike): void;
80
+ closeBundle(): Promise<void>;
81
+ }
82
+
83
+ /** The shared run both plugins call: collect, pre-flight, upload, report.
84
+ * Returns the summary on success, null when skipped or failed (already
85
+ * logged); throws only when `failOnError` is set. */
86
+ export async function runSourcemapUpload(
87
+ options: SourcemapPluginOptions,
88
+ dir: string,
89
+ label: string,
90
+ ): Promise<UploadSummary | null> {
91
+ const log = options.log ?? ((line: string) => console.error(line));
92
+ const env = options.env ?? process.env;
93
+ const prefix = `[realuptime-errors ${label}]`;
94
+ const fail = (message: string): null => {
95
+ if (options.failOnError) throw new Error(`${prefix} ${message}`);
96
+ log(`${prefix} ${message}`);
97
+ return null;
98
+ };
99
+ if (options.enabled === false) {
100
+ log(`${prefix} disabled; no source maps uploaded.`);
101
+ return null;
102
+ }
103
+ const dsn = options.dsn ?? env.REALUPTIME_ERRORS_DSN;
104
+ const ingestKey = options.ingestKey ?? env.REALUPTIME_ERRORS_INGEST_KEY;
105
+ if (!dsn && !ingestKey) {
106
+ log(`${prefix} no REALUPTIME_ERRORS_DSN or ingest key set; no source maps uploaded for ${options.release}.`);
107
+ return null;
108
+ }
109
+ if (!options.release) return fail("release is required; no source maps uploaded.");
110
+ let url: string;
111
+ try {
112
+ url = resolveSourcemapUploadUrl({ dsn, ingestKey, origin: options.origin });
113
+ } catch (cause) {
114
+ return fail(cause instanceof Error ? cause.message : String(cause));
115
+ }
116
+ const { files, duplicates, missing } = collectSourcemapFiles([dir], { urlPrefix: options.urlPrefix });
117
+ if (missing.length > 0) return fail(`${dir} does not exist; no source maps uploaded.`);
118
+ const problem = preflightSourcemapFiles(files);
119
+ if (problem) return fail(problem);
120
+ for (const name of duplicates) log(`${prefix} duplicate artifact name ${name}; the first occurrence was kept.`);
121
+ try {
122
+ const summary = await uploadSourcemaps({
123
+ url,
124
+ release: options.release,
125
+ files,
126
+ fetchImpl: options.fetchImpl,
127
+ onProgress: (progress) => log(`${prefix} ${formatProgressLine(progress)}`),
128
+ });
129
+ log(`${prefix} uploaded ${summary.uploaded} source map${summary.uploaded === 1 ? "" : "s"} for release ${summary.release} (stored ${summary.stored}, replaced ${summary.replaced}).`);
130
+ return summary;
131
+ } catch (cause) {
132
+ return fail(cause instanceof Error ? cause.message : String(cause));
133
+ }
134
+ }
135
+
136
+ export function realuptimeSourcemaps(options: SourcemapPluginOptions): SourcemapPlugin {
137
+ let outDir = options.dir ?? "dist";
138
+ return {
139
+ name: "realuptime-errors-sourcemaps",
140
+ apply: "build",
141
+ enforce: "post",
142
+ configResolved(config) {
143
+ if (options.dir) {
144
+ outDir = resolve(config.root ?? process.cwd(), options.dir);
145
+ return;
146
+ }
147
+ outDir = resolve(config.root ?? process.cwd(), config.build?.outDir ?? "dist");
148
+ },
149
+ async closeBundle() {
150
+ await runSourcemapUpload(options, outDir, "vite");
151
+ },
152
+ };
153
+ }
154
+
155
+ export type { SourcemapFile, UploadSummary };
package/webpack.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { resolve } from "node:path";
2
+ import { runSourcemapUpload, type SourcemapPluginOptions } from "./vite.ts";
3
+
4
+ /**
5
+ * webpack plugin (REA-254), same core as the Vite plugin: after webpack
6
+ * emits its assets, the `.map` files in `output.path` are uploaded to the
7
+ * project's source-map endpoint for the given release.
8
+ *
9
+ * // webpack.config.js
10
+ * const { RealuptimeSourcemapsPlugin } = require("@realuptime/errors/webpack");
11
+ * module.exports = {
12
+ * devtool: "source-map",
13
+ * plugins: [new RealuptimeSourcemapsPlugin({ release: process.env.GITHUB_SHA })],
14
+ * };
15
+ *
16
+ * Works with webpack 4 and 5 and with Rspack: it only uses
17
+ * `compiler.hooks.afterEmit.tapPromise` and `compiler.options.output.path`,
18
+ * both matched structurally (no "webpack" import, zero dependencies). It
19
+ * never changes the emitted assets. Failure policy is the Vite plugin's:
20
+ * loud log, build still succeeds, unless `failOnError: true`.
21
+ */
22
+
23
+ interface CompilerLike {
24
+ options?: { output?: { path?: string }; context?: string };
25
+ hooks: {
26
+ afterEmit: { tapPromise(name: string, fn: () => Promise<void>): void };
27
+ };
28
+ }
29
+
30
+ export class RealuptimeSourcemapsPlugin {
31
+ private readonly options: SourcemapPluginOptions;
32
+
33
+ constructor(options: SourcemapPluginOptions) {
34
+ this.options = options;
35
+ }
36
+
37
+ apply(compiler: CompilerLike): void {
38
+ compiler.hooks.afterEmit.tapPromise("RealuptimeSourcemapsPlugin", async () => {
39
+ const context = compiler.options?.context ?? process.cwd();
40
+ const dir = this.options.dir ? resolve(context, this.options.dir) : (compiler.options?.output?.path ?? resolve(context, "dist"));
41
+ await runSourcemapUpload(this.options, dir, "webpack");
42
+ });
43
+ }
44
+ }
45
+
46
+ export type { SourcemapPluginOptions };