@forinda/kickjs-otel 3.1.0 → 3.1.2
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/dist/index.d.mts +35 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +38 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
package/dist/index.d.mts
CHANGED
|
@@ -22,6 +22,34 @@ interface OtelAdapterOptions {
|
|
|
22
22
|
* @example ['/health', '/_debug/*', '/favicon.ico']
|
|
23
23
|
*/
|
|
24
24
|
ignoreRoutes?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Span-attribute keys to mask before export. Mirrors pino's
|
|
27
|
+
* `redact.paths` contract so one list can drive both log and span
|
|
28
|
+
* redaction:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { sensitiveKeys } from './config/redaction'
|
|
32
|
+
*
|
|
33
|
+
* pino({ redact: { paths: sensitiveKeys } })
|
|
34
|
+
* new OtelAdapter({ sensitiveKeys })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* String entries do case-insensitive exact match on the attribute
|
|
38
|
+
* key (not value); `RegExp` entries are matched against the key.
|
|
39
|
+
* Matching attributes have their value replaced with `'[REDACTED]'`.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* sensitiveKeys: ['password', 'token', /^x-api-key/i, /authorization/i]
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
sensitiveKeys?: (string | RegExp)[];
|
|
47
|
+
/**
|
|
48
|
+
* Custom redactor — takes precedence over `sensitiveKeys` when set.
|
|
49
|
+
* Return the replacement value (`'[REDACTED]'` by convention) or the
|
|
50
|
+
* original value to let it through.
|
|
51
|
+
*/
|
|
52
|
+
redactAttribute?: (key: string, value: unknown) => unknown;
|
|
25
53
|
}
|
|
26
54
|
//#endregion
|
|
27
55
|
//#region src/otel.adapter.d.ts
|
|
@@ -58,11 +86,18 @@ declare class OtelAdapter implements AppAdapter {
|
|
|
58
86
|
private meter;
|
|
59
87
|
private requestCounter;
|
|
60
88
|
private requestDuration;
|
|
89
|
+
private readonly redact;
|
|
61
90
|
constructor(options?: OtelAdapterOptions);
|
|
62
91
|
beforeStart({}: AdapterContext): void;
|
|
63
92
|
middleware(): AdapterMiddleware[];
|
|
64
93
|
private onFinish;
|
|
65
94
|
private shouldIgnore;
|
|
95
|
+
/**
|
|
96
|
+
* Run the configured redactor over an attribute bag. Exposed so
|
|
97
|
+
* downstream code that adds attributes directly on a span
|
|
98
|
+
* (`span.setAttributes(...)`) can share the same redaction contract.
|
|
99
|
+
*/
|
|
100
|
+
applyRedaction<T extends Record<string, unknown>>(attrs: T): Record<string, unknown>;
|
|
66
101
|
shutdown(): Promise<void>;
|
|
67
102
|
}
|
|
68
103
|
//#endregion
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/otel.adapter.ts"],"mappings":";;;;UAAiB,kBAAA;;EAEf,WAAA;EAFe;EAKf,cAAA;;EAGA,OAAA;EANA;EASA,OAAA;EAHA;;;;EASA,gBAAA,IAAoB,GAAA,UAAa,MAAA;EAOjC;;;;;EAAA,YAAA;AAAA;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/otel.adapter.ts"],"mappings":";;;;UAAiB,kBAAA;;EAEf,WAAA;EAFe;EAKf,cAAA;;EAGA,OAAA;EANA;EASA,OAAA;EAHA;;;;EASA,gBAAA,IAAoB,GAAA,UAAa,MAAA;EAOjC;;;;;EAAA,YAAA;EA8B8C;;;;;ACjBhD;;;;;;;;;;;;;;;;EDUE,aAAA,aAA0B,MAAA;ECHlB;;;;;EDUR,eAAA,IAAmB,GAAA,UAAa,KAAA;AAAA;;;;AAtDlC;;;;;;;;;;;;;;;;;;;;;;ACqCA;;;cAAa,WAAA,YAAuB,UAAA;EAClC,IAAA;EAAA,QACQ,OAAA;EAAA,QAIA,MAAA;EAAA,QACA,KAAA;EAAA,QACA,cAAA;EAAA,QACA,eAAA;EAAA,iBACS,MAAA;cAEL,OAAA,GAAS,kBAAA;EAWrB,WAAA,CAAA,IAAgB,cAAA;EA+BhB,UAAA,CAAA,GAAc,iBAAA;EAAA,QA2CN,QAAA;EAAA,QAkCA,YAAA;EA7HA;;;;;EA4IR,cAAA,WAAyB,MAAA,kBAAA,CAAyB,KAAA,EAAO,CAAA,GAAI,MAAA;EAQvD,QAAA,CAAA,GAAY,OAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-otel v3.1.
|
|
2
|
+
* @forinda/kickjs-otel v3.1.2
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -48,6 +48,7 @@ var OtelAdapter = class {
|
|
|
48
48
|
meter = null;
|
|
49
49
|
requestCounter = null;
|
|
50
50
|
requestDuration = null;
|
|
51
|
+
redact;
|
|
51
52
|
constructor(options = {}) {
|
|
52
53
|
this.options = {
|
|
53
54
|
serviceName: options.serviceName ?? "kickjs-app",
|
|
@@ -56,6 +57,7 @@ var OtelAdapter = class {
|
|
|
56
57
|
metrics: options.metrics ?? true,
|
|
57
58
|
...options
|
|
58
59
|
};
|
|
60
|
+
this.redact = buildRedactor(this.options.sensitiveKeys, this.options.redactAttribute);
|
|
59
61
|
}
|
|
60
62
|
beforeStart({}) {
|
|
61
63
|
try {
|
|
@@ -85,14 +87,14 @@ var OtelAdapter = class {
|
|
|
85
87
|
let span = null;
|
|
86
88
|
if (this.tracer) {
|
|
87
89
|
const otelApi = __require("@opentelemetry/api");
|
|
88
|
-
span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, { attributes: {
|
|
90
|
+
span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, { attributes: this.applyRedaction({
|
|
89
91
|
"http.method": req.method,
|
|
90
92
|
"http.url": req.originalUrl,
|
|
91
93
|
"http.target": req.path,
|
|
92
94
|
"http.user_agent": req.get("user-agent") ?? "",
|
|
93
95
|
"net.host.name": req.hostname,
|
|
94
96
|
...this.options.customAttributes?.(req) ?? {}
|
|
95
|
-
} });
|
|
97
|
+
}) });
|
|
96
98
|
const ctx = otelApi.trace.setSpan(otelApi.context.active(), span);
|
|
97
99
|
otelApi.context.with(ctx, () => {
|
|
98
100
|
this.onFinish(req, res, startTime, span);
|
|
@@ -116,10 +118,10 @@ var OtelAdapter = class {
|
|
|
116
118
|
"http.status_code": res.statusCode
|
|
117
119
|
};
|
|
118
120
|
if (span) {
|
|
119
|
-
span.setAttributes({
|
|
121
|
+
span.setAttributes(this.applyRedaction({
|
|
120
122
|
"http.status_code": res.statusCode,
|
|
121
123
|
"http.route": route
|
|
122
|
-
});
|
|
124
|
+
}));
|
|
123
125
|
if (res.statusCode >= 400) span.setStatus({
|
|
124
126
|
code: 2,
|
|
125
127
|
message: `HTTP ${res.statusCode}`
|
|
@@ -137,10 +139,41 @@ var OtelAdapter = class {
|
|
|
137
139
|
return path === pattern;
|
|
138
140
|
});
|
|
139
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Run the configured redactor over an attribute bag. Exposed so
|
|
144
|
+
* downstream code that adds attributes directly on a span
|
|
145
|
+
* (`span.setAttributes(...)`) can share the same redaction contract.
|
|
146
|
+
*/
|
|
147
|
+
applyRedaction(attrs) {
|
|
148
|
+
const out = {};
|
|
149
|
+
for (const key of Object.keys(attrs)) out[key] = this.redact(key, attrs[key]);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
140
152
|
async shutdown() {
|
|
141
153
|
log.info("OTel adapter shutdown");
|
|
142
154
|
}
|
|
143
155
|
};
|
|
156
|
+
/**
|
|
157
|
+
* Build an attribute redactor from `sensitiveKeys` + optional
|
|
158
|
+
* `redactAttribute` override. String keys match case-insensitively
|
|
159
|
+
* against the attribute name; `RegExp` entries are matched verbatim.
|
|
160
|
+
* A custom `redactAttribute` runs after the key-based mask so users
|
|
161
|
+
* can inspect values too.
|
|
162
|
+
*/
|
|
163
|
+
function buildRedactor(sensitiveKeys, custom) {
|
|
164
|
+
if (custom) return custom;
|
|
165
|
+
if (!sensitiveKeys || sensitiveKeys.length === 0) return (_k, v) => v;
|
|
166
|
+
const lowered = /* @__PURE__ */ new Set();
|
|
167
|
+
const patterns = [];
|
|
168
|
+
for (const entry of sensitiveKeys) if (typeof entry === "string") lowered.add(entry.toLowerCase());
|
|
169
|
+
else patterns.push(entry);
|
|
170
|
+
return (key, value) => {
|
|
171
|
+
const lower = key.toLowerCase();
|
|
172
|
+
if (lowered.has(lower)) return "[REDACTED]";
|
|
173
|
+
for (const p of patterns) if (p.test(key)) return "[REDACTED]";
|
|
174
|
+
return value;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
144
177
|
//#endregion
|
|
145
178
|
export { OtelAdapter };
|
|
146
179
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/otel.adapter.ts"],"sourcesContent":["import {\n Logger,\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n} from '@forinda/kickjs'\nimport type { Request, Response, NextFunction } from 'express'\nimport type { OtelAdapterOptions } from './types'\n\nconst log = Logger.for('OtelAdapter')\n\n/**\n * OpenTelemetry adapter for KickJS — automatic tracing and metrics.\n *\n * Creates spans for each HTTP request with route, method, status code,\n * and duration. Optionally records request count and latency histograms.\n *\n * Works with any OTel-compatible backend: Jaeger, Grafana Tempo, Datadog,\n * Honeycomb, etc. Configure exporters via the OTel SDK before bootstrapping.\n *\n * @example\n * ```ts\n * import { OtelAdapter } from '@forinda/kickjs-otel'\n *\n * // Set up OTel SDK (e.g., with Jaeger exporter) before bootstrap\n * bootstrap({\n * modules,\n * adapters: [\n * new OtelAdapter({\n * serviceName: 'my-api',\n * serviceVersion: '1.0.0',\n * ignoreRoutes: ['/health', '/_debug/*'],\n * }),\n * ],\n * })\n * ```\n */\nexport class OtelAdapter implements AppAdapter {\n name = 'OtelAdapter'\n private options: Required<\n Pick<OtelAdapterOptions, 'serviceName' | 'serviceVersion' | 'tracing' | 'metrics'>\n > &\n OtelAdapterOptions\n private tracer: any = null\n private meter: any = null\n private requestCounter: any = null\n private requestDuration: any = null\n\n constructor(options: OtelAdapterOptions = {}) {\n this.options = {\n serviceName: options.serviceName ?? 'kickjs-app',\n serviceVersion: options.serviceVersion ?? '0.0.0',\n tracing: options.tracing ?? true,\n metrics: options.metrics ?? true,\n ...options,\n }\n }\n\n beforeStart({}: AdapterContext): void {\n try {\n // Dynamically import OTel API — it's a peer dependency\n const otelApi = require('@opentelemetry/api')\n\n if (this.options.tracing) {\n this.tracer = otelApi.trace.getTracer(this.options.serviceName, this.options.serviceVersion)\n log.info(`Tracing enabled for ${this.options.serviceName}`)\n }\n\n if (this.options.metrics) {\n this.meter = otelApi.metrics.getMeter(this.options.serviceName, this.options.serviceVersion)\n\n this.requestCounter = this.meter.createCounter('http.server.request.count', {\n description: 'Total number of HTTP requests',\n })\n\n this.requestDuration = this.meter.createHistogram('http.server.request.duration', {\n description: 'HTTP request duration in milliseconds',\n unit: 'ms',\n })\n\n log.info('Metrics enabled — http.server.request.count, http.server.request.duration')\n }\n } catch {\n log.warn(\n 'OpenTelemetry API not found. Install @opentelemetry/api to enable tracing and metrics.',\n )\n }\n }\n\n middleware(): AdapterMiddleware[] {\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n // Skip ignored routes\n if (this.shouldIgnore(req.path)) {\n return next()\n }\n\n const startTime = performance.now()\n\n // Start a span if tracing is enabled\n let span: any = null\n if (this.tracer) {\n const otelApi = require('@opentelemetry/api')\n span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, {\n attributes: {\n 'http.method': req.method,\n 'http.url': req.originalUrl,\n 'http.target': req.path,\n 'http.user_agent': req.get('user-agent') ?? '',\n 'net.host.name': req.hostname,\n ...(this.options.customAttributes?.(req) ?? {}),\n },\n })\n\n // Set span on context so downstream code can add attributes\n const ctx = otelApi.trace.setSpan(otelApi.context.active(), span)\n otelApi.context.with(ctx, () => {\n this.onFinish(req, res, startTime, span)\n next()\n })\n return\n }\n\n this.onFinish(req, res, startTime, null)\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n private onFinish(req: Request, res: Response, startTime: number, span: any): void {\n res.on('finish', () => {\n const duration = performance.now() - startTime\n const route = (req as any).route?.path ?? req.path\n const attributes = {\n 'http.method': req.method,\n 'http.route': route,\n 'http.status_code': res.statusCode,\n }\n\n // End span\n if (span) {\n span.setAttributes({\n 'http.status_code': res.statusCode,\n 'http.route': route,\n })\n if (res.statusCode >= 400) {\n span.setStatus({ code: 2, message: `HTTP ${res.statusCode}` })\n }\n span.end()\n }\n\n // Record metrics\n if (this.requestCounter) {\n this.requestCounter.add(1, attributes)\n }\n if (this.requestDuration) {\n this.requestDuration.record(duration, attributes)\n }\n })\n }\n\n private shouldIgnore(path: string): boolean {\n if (!this.options.ignoreRoutes) return false\n return this.options.ignoreRoutes.some((pattern) => {\n if (pattern.endsWith('*')) {\n return path.startsWith(pattern.slice(0, -1))\n }\n return path === pattern\n })\n }\n\n async shutdown(): Promise<void> {\n log.info('OTel adapter shutdown')\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/otel.adapter.ts"],"sourcesContent":["import {\n Logger,\n type AppAdapter,\n type AdapterContext,\n type AdapterMiddleware,\n} from '@forinda/kickjs'\nimport type { Request, Response, NextFunction } from 'express'\nimport type { OtelAdapterOptions } from './types'\n\nconst log = Logger.for('OtelAdapter')\n\n/**\n * OpenTelemetry adapter for KickJS — automatic tracing and metrics.\n *\n * Creates spans for each HTTP request with route, method, status code,\n * and duration. Optionally records request count and latency histograms.\n *\n * Works with any OTel-compatible backend: Jaeger, Grafana Tempo, Datadog,\n * Honeycomb, etc. Configure exporters via the OTel SDK before bootstrapping.\n *\n * @example\n * ```ts\n * import { OtelAdapter } from '@forinda/kickjs-otel'\n *\n * // Set up OTel SDK (e.g., with Jaeger exporter) before bootstrap\n * bootstrap({\n * modules,\n * adapters: [\n * new OtelAdapter({\n * serviceName: 'my-api',\n * serviceVersion: '1.0.0',\n * ignoreRoutes: ['/health', '/_debug/*'],\n * }),\n * ],\n * })\n * ```\n */\nexport class OtelAdapter implements AppAdapter {\n name = 'OtelAdapter'\n private options: Required<\n Pick<OtelAdapterOptions, 'serviceName' | 'serviceVersion' | 'tracing' | 'metrics'>\n > &\n OtelAdapterOptions\n private tracer: any = null\n private meter: any = null\n private requestCounter: any = null\n private requestDuration: any = null\n private readonly redact: (key: string, value: unknown) => unknown\n\n constructor(options: OtelAdapterOptions = {}) {\n this.options = {\n serviceName: options.serviceName ?? 'kickjs-app',\n serviceVersion: options.serviceVersion ?? '0.0.0',\n tracing: options.tracing ?? true,\n metrics: options.metrics ?? true,\n ...options,\n }\n this.redact = buildRedactor(this.options.sensitiveKeys, this.options.redactAttribute)\n }\n\n beforeStart({}: AdapterContext): void {\n try {\n // Dynamically import OTel API — it's a peer dependency\n const otelApi = require('@opentelemetry/api')\n\n if (this.options.tracing) {\n this.tracer = otelApi.trace.getTracer(this.options.serviceName, this.options.serviceVersion)\n log.info(`Tracing enabled for ${this.options.serviceName}`)\n }\n\n if (this.options.metrics) {\n this.meter = otelApi.metrics.getMeter(this.options.serviceName, this.options.serviceVersion)\n\n this.requestCounter = this.meter.createCounter('http.server.request.count', {\n description: 'Total number of HTTP requests',\n })\n\n this.requestDuration = this.meter.createHistogram('http.server.request.duration', {\n description: 'HTTP request duration in milliseconds',\n unit: 'ms',\n })\n\n log.info('Metrics enabled — http.server.request.count, http.server.request.duration')\n }\n } catch {\n log.warn(\n 'OpenTelemetry API not found. Install @opentelemetry/api to enable tracing and metrics.',\n )\n }\n }\n\n middleware(): AdapterMiddleware[] {\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n // Skip ignored routes\n if (this.shouldIgnore(req.path)) {\n return next()\n }\n\n const startTime = performance.now()\n\n // Start a span if tracing is enabled\n let span: any = null\n if (this.tracer) {\n const otelApi = require('@opentelemetry/api')\n span = this.tracer.startSpan(`${req.method} ${req.route?.path ?? req.path}`, {\n attributes: this.applyRedaction({\n 'http.method': req.method,\n 'http.url': req.originalUrl,\n 'http.target': req.path,\n 'http.user_agent': req.get('user-agent') ?? '',\n 'net.host.name': req.hostname,\n ...(this.options.customAttributes?.(req) ?? {}),\n }),\n })\n\n // Set span on context so downstream code can add attributes\n const ctx = otelApi.trace.setSpan(otelApi.context.active(), span)\n otelApi.context.with(ctx, () => {\n this.onFinish(req, res, startTime, span)\n next()\n })\n return\n }\n\n this.onFinish(req, res, startTime, null)\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n private onFinish(req: Request, res: Response, startTime: number, span: any): void {\n res.on('finish', () => {\n const duration = performance.now() - startTime\n const route = (req as any).route?.path ?? req.path\n const attributes = {\n 'http.method': req.method,\n 'http.route': route,\n 'http.status_code': res.statusCode,\n }\n\n // End span\n if (span) {\n span.setAttributes(\n this.applyRedaction({\n 'http.status_code': res.statusCode,\n 'http.route': route,\n }),\n )\n if (res.statusCode >= 400) {\n span.setStatus({ code: 2, message: `HTTP ${res.statusCode}` })\n }\n span.end()\n }\n\n // Record metrics\n if (this.requestCounter) {\n this.requestCounter.add(1, attributes)\n }\n if (this.requestDuration) {\n this.requestDuration.record(duration, attributes)\n }\n })\n }\n\n private shouldIgnore(path: string): boolean {\n if (!this.options.ignoreRoutes) return false\n return this.options.ignoreRoutes.some((pattern) => {\n if (pattern.endsWith('*')) {\n return path.startsWith(pattern.slice(0, -1))\n }\n return path === pattern\n })\n }\n\n /**\n * Run the configured redactor over an attribute bag. Exposed so\n * downstream code that adds attributes directly on a span\n * (`span.setAttributes(...)`) can share the same redaction contract.\n */\n applyRedaction<T extends Record<string, unknown>>(attrs: T): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(attrs)) {\n out[key] = this.redact(key, attrs[key])\n }\n return out\n }\n\n async shutdown(): Promise<void> {\n log.info('OTel adapter shutdown')\n }\n}\n\n/**\n * Build an attribute redactor from `sensitiveKeys` + optional\n * `redactAttribute` override. String keys match case-insensitively\n * against the attribute name; `RegExp` entries are matched verbatim.\n * A custom `redactAttribute` runs after the key-based mask so users\n * can inspect values too.\n */\nfunction buildRedactor(\n sensitiveKeys: (string | RegExp)[] | undefined,\n custom: ((key: string, value: unknown) => unknown) | undefined,\n): (key: string, value: unknown) => unknown {\n if (custom) return custom\n if (!sensitiveKeys || sensitiveKeys.length === 0) return (_k, v) => v\n\n const lowered = new Set<string>()\n const patterns: RegExp[] = []\n for (const entry of sensitiveKeys) {\n if (typeof entry === 'string') lowered.add(entry.toLowerCase())\n else patterns.push(entry)\n }\n\n return (key: string, value: unknown) => {\n const lower = key.toLowerCase()\n if (lowered.has(lower)) return '[REDACTED]'\n for (const p of patterns) {\n if (p.test(key)) return '[REDACTED]'\n }\n return value\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-otel",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "OpenTelemetry adapter for KickJS — automatic tracing, metrics, and export to any OTel backend",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -101,7 +101,9 @@
|
|
|
101
101
|
"@opentelemetry/semantic-conventions": "^1.4.0",
|
|
102
102
|
"@types/node": "^25.0.0",
|
|
103
103
|
"typescript": "^5.9.2",
|
|
104
|
-
"
|
|
104
|
+
"unplugin-swc": "^1.5.9",
|
|
105
|
+
"vitest": "^4.1.2",
|
|
106
|
+
"@forinda/kickjs": "3.1.2"
|
|
105
107
|
},
|
|
106
108
|
"publishConfig": {
|
|
107
109
|
"access": "public"
|
|
@@ -123,6 +125,7 @@
|
|
|
123
125
|
"scripts": {
|
|
124
126
|
"build": "wireit",
|
|
125
127
|
"dev": "tsdown --watch",
|
|
128
|
+
"test": "vitest run",
|
|
126
129
|
"typecheck": "tsc --noEmit",
|
|
127
130
|
"clean": "rm -rf dist .wireit",
|
|
128
131
|
"lint": "tsc --noEmit"
|