nuxt-error-tracker 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @ray_of_goodness_/nuxt-error-tracker
2
+
3
+ Lightweight frontend error tracker for **Nuxt 3 / 4**. Captures unhandled
4
+ errors, failed requests, and a breadcrumb trail of user actions, then ships them
5
+ to your Observer ingest endpoint. Client-only, batched, and built to **never
6
+ break the host app** — if it isn't configured, it does nothing.
7
+
8
+ ## Features
9
+
10
+ - Captures Vue errors, `window.onerror`, unhandled promise rejections
11
+ - Optional `fetch` instrumentation — auto-reports network failures and 5xx
12
+ - Optional `console.error` capture
13
+ - **Breadcrumbs**: ring buffer of clicks, route changes, fetches and console
14
+ errors, snapshotted onto each error
15
+ - Batched + debounced sending; force-flush on page hide via `sendBeacon`
16
+ - Per-session dedup so one recurring error can't flood your quota
17
+ - Safe by default: missing keys → plugin is a no-op, never throws
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm i @ray_of_goodness_/nuxt-error-tracker
23
+ # or: bun add @ray_of_goodness_/nuxt-error-tracker
24
+ ```
25
+
26
+ ## Setup
27
+
28
+ Add the module and point it at your ingest endpoint with your project's public
29
+ key:
30
+
31
+ ```ts
32
+ // nuxt.config.ts
33
+ export default defineNuxtConfig({
34
+ modules: ['@ray_of_goodness_/nuxt-error-tracker'],
35
+ errorTracker: {
36
+ endpoint: 'https://<your-observer-host>/v1/ingest',
37
+ publicKey: 'pk_xxxxxxxxxxxxxxxxxxxxxxxx',
38
+ },
39
+ })
40
+ ```
41
+
42
+ > The public key identifies your project on the ingest side. It's a **public**
43
+ > value — safe to ship in client bundles. Origin allowlisting and rate limiting
44
+ > are enforced server-side.
45
+
46
+ ### No keys? No problem
47
+
48
+ If `endpoint` or `publicKey` is empty (or `enabled: false`), the plugin is never
49
+ registered and your app runs untouched. This makes it safe to leave the module
50
+ installed while wiring keys per-environment — dev builds without a key simply
51
+ don't track.
52
+
53
+ ## Configuration via environment variables
54
+
55
+ Every option can be driven from the environment instead of hardcoding it, using
56
+ Nuxt's `runtimeConfig` public-env convention:
57
+
58
+ ```ini
59
+ NUXT_PUBLIC_ERROR_TRACKER_ENDPOINT=https://<your-observer-host>/v1/ingest
60
+ NUXT_PUBLIC_ERROR_TRACKER_PUBLIC_KEY=pk_xxxxxxxxxxxxxxxxxxxxxxxx
61
+ NUXT_PUBLIC_ERROR_TRACKER_APP_VERSION=1.4.2
62
+ ```
63
+
64
+ Wire them in `nuxt.config.ts`:
65
+
66
+ ```ts
67
+ export default defineNuxtConfig({
68
+ modules: ['@ray_of_goodness_/nuxt-error-tracker'],
69
+ runtimeConfig: {
70
+ public: {
71
+ errorTracker: {
72
+ endpoint: process.env.NUXT_PUBLIC_ERROR_TRACKER_ENDPOINT,
73
+ publicKey: process.env.NUXT_PUBLIC_ERROR_TRACKER_PUBLIC_KEY,
74
+ appVersion: process.env.NUXT_PUBLIC_ERROR_TRACKER_APP_VERSION,
75
+ },
76
+ },
77
+ },
78
+ })
79
+ ```
80
+
81
+ ## Options
82
+
83
+ | Option | Type | Default | Description |
84
+ | ----------------- | --------- | ----------- | ------------------------------------------------------------------ |
85
+ | `endpoint` | `string` | `''` | Ingest URL (`.../v1/ingest`). Required. |
86
+ | `publicKey` | `string` | `''` | Your project's public key. Required. |
87
+ | `appVersion` | `string` | `'unknown'` | Tag every error with a release/version. |
88
+ | `enabled` | `boolean` | `true` | Master switch. `false` = fully disabled. |
89
+ | `captureFetch` | `boolean` | `true` | Wrap `fetch` to breadcrumb requests and report failures/5xx. |
90
+ | `captureConsole` | `boolean` | `true` | Patch `console.error` to capture logged-but-not-thrown errors. |
91
+ | `flushDelay` | `number` | `2000` | Debounce (ms) before a batch is sent. |
92
+ | `maxPerSession` | `number` | `5` | Max occurrences of the *same* error captured per session. |
93
+ | `maxBatch` | `number` | `20` | Max errors per request (server also hard-caps at 20). |
94
+ | `maxBreadcrumbs` | `number` | `30` | Breadcrumbs kept in the ring buffer / sent per error (max 50). |
95
+
96
+ ## Runtime helpers
97
+
98
+ The plugin injects two helpers for manual use:
99
+
100
+ ```ts
101
+ const { $trackError, $addBreadcrumb } = useNuxtApp()
102
+
103
+ // Report a handled error yourself
104
+ try {
105
+ await risky()
106
+ } catch (e) {
107
+ $trackError(e)
108
+ }
109
+
110
+ // Add your own breadcrumb to the trail
111
+ $addBreadcrumb({
112
+ type: 'custom',
113
+ category: 'checkout',
114
+ message: 'user clicked pay',
115
+ level: 'info',
116
+ })
117
+ ```
118
+
119
+ ## What gets sent
120
+
121
+ Per error: `message`, `stack`, `source` (`vue` | `window` | `promise` |
122
+ `fetch` | `console`), `url`, `route`, timestamp, and the breadcrumb trail. Once
123
+ per batch: `device` info (user agent, language, screen/viewport, connection),
124
+ `userId` (read from a `user_id` cookie if present), and `appVersion`. Field
125
+ lengths are truncated client-side to match server limits.
126
+
127
+ ### Privacy
128
+
129
+ Click breadcrumbs record the element (tag / id / class / short text) — **never
130
+ input values**. Nothing is captured until an error occurs; breadcrumbs live only
131
+ in memory and ship attached to an error.
132
+
133
+ ## How it sends
134
+
135
+ Payloads are encoded as **protobuf** and POSTed as `application/x-protobuf`. On
136
+ page-hide the queue is force-flushed via `navigator.sendBeacon`, falling back to
137
+ `fetch(..., { keepalive: true })`. Binary framing keeps the network panel tidy —
138
+ it is **not** encryption; the schema is public.
139
+
140
+ ## License
141
+
142
+ MIT
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,24 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ endpoint: string;
5
+ publicKey: string;
6
+ appVersion?: string;
7
+ enabled?: boolean;
8
+ /** Wrap window.fetch to capture failed requests (network errors + 5xx). Default true. */
9
+ captureFetch?: boolean;
10
+ /** Patch console.error to capture logged-but-not-thrown errors. Default true. */
11
+ captureConsole?: boolean;
12
+ /** Debounce before a batch is flushed, in ms. Default 2000. */
13
+ flushDelay?: number;
14
+ /** Max occurrences of the same error captured per session. Default 5. */
15
+ maxPerSession?: number;
16
+ /** Max errors per flushed batch. Default 20 (server also caps at 20). */
17
+ maxBatch?: number;
18
+ /** Max breadcrumbs kept in the ring buffer / sent per error. Default 30. */
19
+ maxBreadcrumbs?: number;
20
+ }
21
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
22
+
23
+ export { _default as default };
24
+ export type { ModuleOptions };
@@ -0,0 +1,24 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ endpoint: string;
5
+ publicKey: string;
6
+ appVersion?: string;
7
+ enabled?: boolean;
8
+ /** Wrap window.fetch to capture failed requests (network errors + 5xx). Default true. */
9
+ captureFetch?: boolean;
10
+ /** Patch console.error to capture logged-but-not-thrown errors. Default true. */
11
+ captureConsole?: boolean;
12
+ /** Debounce before a batch is flushed, in ms. Default 2000. */
13
+ flushDelay?: number;
14
+ /** Max occurrences of the same error captured per session. Default 5. */
15
+ maxPerSession?: number;
16
+ /** Max errors per flushed batch. Default 20 (server also caps at 20). */
17
+ maxBatch?: number;
18
+ /** Max breadcrumbs kept in the ring buffer / sent per error. Default 30. */
19
+ maxBreadcrumbs?: number;
20
+ }
21
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
22
+
23
+ export { _default as default };
24
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@ray_of_goodness_/nuxt-error-tracker",
3
+ "configKey": "errorTracker",
4
+ "version": "0.1.3",
5
+ "builder": {
6
+ "@nuxt/module-builder": "0.8.4",
7
+ "unbuild": "2.0.0"
8
+ }
9
+ }
@@ -0,0 +1,55 @@
1
+ import { defineNuxtModule, extendViteConfig, createResolver, addPlugin } from '@nuxt/kit';
2
+
3
+ function posInt(value, fallback) {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
6
+ }
7
+ const module = defineNuxtModule({
8
+ meta: {
9
+ name: "@ray_of_goodness_/nuxt-error-tracker",
10
+ configKey: "errorTracker"
11
+ },
12
+ defaults: {
13
+ endpoint: "",
14
+ publicKey: "",
15
+ appVersion: "unknown",
16
+ enabled: true,
17
+ captureFetch: true,
18
+ captureConsole: true,
19
+ flushDelay: 2e3,
20
+ maxPerSession: 5,
21
+ maxBatch: 20,
22
+ maxBreadcrumbs: 30
23
+ },
24
+ setup(options, nuxt) {
25
+ if (!options.enabled || !options.endpoint || !options.publicKey)
26
+ return;
27
+ nuxt.options.runtimeConfig.public.errorTracker = {
28
+ endpoint: options.endpoint,
29
+ publicKey: options.publicKey,
30
+ appVersion: options.appVersion ?? "unknown",
31
+ captureFetch: options.captureFetch !== false,
32
+ captureConsole: options.captureConsole !== false,
33
+ flushDelay: posInt(options.flushDelay, 2e3),
34
+ maxPerSession: posInt(options.maxPerSession, 5),
35
+ // Server hard-caps batches at 20 — never emit larger, even if misconfigured.
36
+ maxBatch: Math.min(posInt(options.maxBatch, 20), 20),
37
+ // Server DTO caps at 50 breadcrumbs — stay at or below.
38
+ maxBreadcrumbs: Math.min(posInt(options.maxBreadcrumbs, 30), 50)
39
+ };
40
+ extendViteConfig((config) => {
41
+ config.optimizeDeps ||= {};
42
+ config.optimizeDeps.include ||= [];
43
+ if (!config.optimizeDeps.include.includes("protobufjs")) {
44
+ config.optimizeDeps.include.push("protobufjs");
45
+ }
46
+ });
47
+ const resolver = createResolver(import.meta.url);
48
+ addPlugin({
49
+ src: resolver.resolve("./runtime/plugin.client"),
50
+ mode: "client"
51
+ });
52
+ }
53
+ });
54
+
55
+ export { module as default };
@@ -0,0 +1,27 @@
1
+ export interface Crumb {
2
+ type?: string;
3
+ category?: string;
4
+ message?: string;
5
+ level?: string;
6
+ timestamp?: string;
7
+ data?: Record<string, unknown>;
8
+ }
9
+ export interface FlushError {
10
+ message: string;
11
+ stack: string;
12
+ source: string;
13
+ url: string;
14
+ route: string;
15
+ occurredAt: string;
16
+ breadcrumbs?: Crumb[];
17
+ }
18
+ export interface FlushBody {
19
+ meta: {
20
+ device?: Record<string, unknown>;
21
+ userId?: string | null;
22
+ appVersion?: string;
23
+ };
24
+ errors: FlushError[];
25
+ }
26
+ /** Encode a flush body to protobuf bytes matching the shared Batch schema. */
27
+ export declare function encodeBatch(body: FlushBody): Uint8Array;
@@ -0,0 +1,70 @@
1
+ import protobuf from "protobufjs";
2
+ const INGEST_PROTO = `
3
+ syntax = "proto3";
4
+ message Breadcrumb {
5
+ string type = 1;
6
+ string category = 2;
7
+ string message = 3;
8
+ string level = 4;
9
+ string timestamp = 5;
10
+ string data = 6;
11
+ }
12
+ message Error {
13
+ string message = 1;
14
+ string stack = 2;
15
+ string source = 3;
16
+ string url = 4;
17
+ string route = 5;
18
+ string userId = 6;
19
+ string appVersion = 7;
20
+ string device = 8;
21
+ string occurredAt = 9;
22
+ repeated Breadcrumb breadcrumbs = 10;
23
+ }
24
+ message Meta {
25
+ string device = 1;
26
+ string userId = 2;
27
+ string appVersion = 3;
28
+ }
29
+ message Batch {
30
+ Meta meta = 1;
31
+ repeated Error errors = 2;
32
+ }
33
+ `;
34
+ const Batch = protobuf.parse(INGEST_PROTO).root.lookupType("Batch");
35
+ function jsonStr(v) {
36
+ if (v === void 0 || v === null) return "";
37
+ try {
38
+ return JSON.stringify(v);
39
+ } catch {
40
+ return "";
41
+ }
42
+ }
43
+ export function encodeBatch(body) {
44
+ const message = {
45
+ meta: {
46
+ device: jsonStr(body.meta.device),
47
+ userId: body.meta.userId ?? "",
48
+ appVersion: body.meta.appVersion ?? ""
49
+ },
50
+ errors: body.errors.map((e) => ({
51
+ message: e.message,
52
+ stack: e.stack,
53
+ source: e.source,
54
+ url: e.url,
55
+ route: e.route,
56
+ appVersion: "",
57
+ device: "",
58
+ occurredAt: e.occurredAt,
59
+ breadcrumbs: (e.breadcrumbs ?? []).map((c) => ({
60
+ type: c.type ?? "",
61
+ category: c.category ?? "",
62
+ message: c.message ?? "",
63
+ level: c.level ?? "",
64
+ timestamp: c.timestamp ?? "",
65
+ data: jsonStr(c.data)
66
+ }))
67
+ }))
68
+ };
69
+ return Batch.encode(Batch.fromObject(message)).finish();
70
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,204 @@
1
+ import {
2
+ defineNuxtPlugin,
3
+ useCookie,
4
+ useRoute,
5
+ useRouter,
6
+ useRuntimeConfig
7
+ } from "#imports";
8
+ import { encodeBatch } from "./ingest.codec.js";
9
+ export default defineNuxtPlugin((nuxtApp) => {
10
+ const config = useRuntimeConfig().public.errorTracker;
11
+ if (!config || !config.endpoint || !config.publicKey) return;
12
+ const route = useRoute();
13
+ const FLUSH_DELAY = config.flushDelay ?? 2e3;
14
+ const MAX_PER_SESSION = config.maxPerSession ?? 5;
15
+ const MAX_BATCH = config.maxBatch ?? 20;
16
+ const MAX_CRUMBS = config.maxBreadcrumbs ?? 30;
17
+ const queue = [];
18
+ const sessionCounts = /* @__PURE__ */ new Map();
19
+ let flushTimer = null;
20
+ const crumbs = [];
21
+ function addCrumb(c) {
22
+ crumbs.push({ ...c, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
23
+ if (crumbs.length > MAX_CRUMBS) crumbs.shift();
24
+ }
25
+ function deviceInfo() {
26
+ return {
27
+ userAgent: navigator.userAgent,
28
+ language: navigator.language,
29
+ platform: navigator.platform,
30
+ screen: `${window.screen.width}x${window.screen.height}`,
31
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
32
+ online: navigator.onLine,
33
+ memory: navigator.deviceMemory ?? null,
34
+ connection: navigator.connection?.effectiveType ?? null
35
+ };
36
+ }
37
+ function resolveUserId() {
38
+ return useCookie("user_id").value ?? null;
39
+ }
40
+ function sessionKey(message, stack) {
41
+ return `${message.slice(0, 120)}::${(stack.split("\n")[1] ?? "").trim()}`;
42
+ }
43
+ function flush(useBeacon = false) {
44
+ if (!queue.length) return;
45
+ const batch = queue.splice(0, MAX_BATCH);
46
+ const body = {
47
+ meta: {
48
+ device: deviceInfo(),
49
+ userId: resolveUserId(),
50
+ appVersion: config.appVersion
51
+ },
52
+ errors: batch
53
+ };
54
+ const bytes = encodeBatch(body);
55
+ if (useBeacon && navigator.sendBeacon) {
56
+ const url = `${config.endpoint}?key=${encodeURIComponent(config.publicKey)}`;
57
+ const sent = navigator.sendBeacon(
58
+ url,
59
+ new Blob([bytes], { type: "application/x-protobuf" })
60
+ );
61
+ if (sent) return;
62
+ }
63
+ fetch(config.endpoint, {
64
+ method: "POST",
65
+ headers: {
66
+ "Content-Type": "application/x-protobuf",
67
+ "X-Public-Key": config.publicKey
68
+ },
69
+ body: bytes,
70
+ keepalive: true
71
+ }).catch(() => {
72
+ queue.unshift(...batch);
73
+ });
74
+ }
75
+ function scheduleFlush() {
76
+ if (flushTimer) return;
77
+ flushTimer = setTimeout(() => {
78
+ flushTimer = null;
79
+ flush();
80
+ }, FLUSH_DELAY);
81
+ }
82
+ function capture(err, source) {
83
+ try {
84
+ const error = err instanceof Error ? err : new Error(String(err));
85
+ if (error.message === "Script error.") return;
86
+ const key = sessionKey(error.message, error.stack ?? "");
87
+ const count = sessionCounts.get(key) ?? 0;
88
+ if (count >= MAX_PER_SESSION) return;
89
+ sessionCounts.set(key, count + 1);
90
+ queue.push({
91
+ message: error.message.slice(0, 1e3),
92
+ stack: (error.stack ?? "").slice(0, 8e3),
93
+ source,
94
+ url: window.location.href.slice(0, 2e3),
95
+ route: route.fullPath.slice(0, 500),
96
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
97
+ // Snapshot the trail leading up to this error (copy, not reference).
98
+ breadcrumbs: crumbs.slice(-MAX_CRUMBS)
99
+ });
100
+ scheduleFlush();
101
+ } catch {
102
+ }
103
+ }
104
+ nuxtApp.hook("vue:error", (err) => capture(err, "vue"));
105
+ window.addEventListener(
106
+ "error",
107
+ (event) => capture(event.error ?? event.message, "window")
108
+ );
109
+ window.addEventListener(
110
+ "unhandledrejection",
111
+ (event) => capture(event.reason, "promise")
112
+ );
113
+ document.addEventListener(
114
+ "click",
115
+ (event) => {
116
+ const el = event.target;
117
+ if (!el || !el.tagName) return;
118
+ const tag = el.tagName.toLowerCase();
119
+ const id = el.id ? `#${el.id}` : "";
120
+ const cls = typeof el.className === "string" && el.className ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
121
+ const text = (el.textContent ?? "").trim().slice(0, 40);
122
+ addCrumb({
123
+ type: "click",
124
+ category: "ui.click",
125
+ message: `${tag}${id}${cls}${text ? ` "${text}"` : ""}`
126
+ });
127
+ },
128
+ { capture: true, passive: true }
129
+ );
130
+ useRouter().afterEach((to, from) => {
131
+ addCrumb({
132
+ type: "navigation",
133
+ category: "navigation",
134
+ message: `${from.fullPath} \u2192 ${to.fullPath}`
135
+ });
136
+ });
137
+ if (config.captureFetch && typeof window.fetch === "function") {
138
+ const originalFetch = window.fetch.bind(window);
139
+ window.fetch = async (input, init) => {
140
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
141
+ const reqUrl = input instanceof Request ? input.url : String(input);
142
+ const isOwn = reqUrl.startsWith(config.endpoint);
143
+ try {
144
+ const res = await originalFetch(input, init);
145
+ if (!isOwn) {
146
+ addCrumb({
147
+ type: "fetch",
148
+ category: "http",
149
+ level: res.status >= 400 ? "error" : "info",
150
+ message: `${method} ${reqUrl} \u2192 ${res.status}`
151
+ });
152
+ if (res.status >= 500) {
153
+ capture(
154
+ new Error(`HTTP ${res.status} ${method} ${reqUrl}`),
155
+ "fetch"
156
+ );
157
+ }
158
+ }
159
+ return res;
160
+ } catch (err) {
161
+ if (!isOwn) {
162
+ const reason = err instanceof Error ? err.message : String(err);
163
+ addCrumb({
164
+ type: "fetch",
165
+ category: "http",
166
+ level: "error",
167
+ message: `${method} ${reqUrl} \u2192 failed: ${reason}`
168
+ });
169
+ capture(
170
+ new Error(`Fetch failed ${method} ${reqUrl}: ${reason}`),
171
+ "fetch"
172
+ );
173
+ }
174
+ throw err;
175
+ }
176
+ };
177
+ }
178
+ if (config.captureConsole) {
179
+ const originalError = console.error.bind(console);
180
+ console.error = (...args) => {
181
+ const errArg = args.find((a) => a instanceof Error);
182
+ const message = errArg?.message ?? args.map((a) => typeof a === "string" ? a : String(a)).join(" ").slice(0, 1e3);
183
+ if (message) {
184
+ addCrumb({
185
+ type: "console",
186
+ category: "console",
187
+ level: "error",
188
+ message
189
+ });
190
+ capture(errArg ?? new Error(message), "console");
191
+ }
192
+ originalError(...args);
193
+ };
194
+ }
195
+ document.addEventListener("visibilitychange", () => {
196
+ if (document.visibilityState === "hidden") flush(true);
197
+ });
198
+ return {
199
+ provide: {
200
+ trackError: (err) => capture(err, "window"),
201
+ addBreadcrumb: (c) => addCrumb(c)
202
+ }
203
+ };
204
+ });
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.js'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.js'
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module'
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "nuxt-error-tracker",
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/types.d.ts",
8
+ "import": "./dist/module.mjs"
9
+ }
10
+ },
11
+ "main": "./dist/module.mjs",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "scripts": {
19
+ "build": "nuxt-module-build build",
20
+ "prepublishOnly": "nuxt-module-build build"
21
+ },
22
+ "dependencies": {
23
+ "protobufjs": "^8.6.6"
24
+ },
25
+ "peerDependencies": {
26
+ "@nuxt/kit": "^3.14.0 || ^4.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@nuxt/kit": "^3.14.0",
30
+ "@nuxt/module-builder": "^0.8.0",
31
+ "nuxt": "^3.14.0"
32
+ }
33
+ }