@kids-reporter/logger 0.0.1

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,64 @@
1
+ # @kids-reporter/logger
2
+
3
+ Shared structured logger and trace utilities for Kids Reporter services. Uses Google Cloud `X-Cloud-Trace-Context` to propagate a single `traceId` so that logs can be correlated in GCP Cloud Logging via `logging.googleapis.com/trace`.
4
+
5
+ ## Types
6
+
7
+ - **`LogSeverity`** — `'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'ALERT' | 'CRITICAL'`
8
+ - **`StructuredLogPayload`** — `{ severity: LogSeverity; message?: string } & Record<string, unknown>`
9
+ - **`TraceHeaderInput`** — `Headers | Record<string, string | undefined | unknown> | undefined`
10
+ - **`NormalizedTraceContext`** — `{ traceId: string; traceHeaders: { 'X-Cloud-Trace-Context': string } }`
11
+
12
+ ## API
13
+
14
+ ### `normalizeTraceContext(headersInput?, options?)`
15
+
16
+ Parses trace context from the `X-Cloud-Trace-Context` header (GCP format). Optionally generates a new trace ID when the header is missing.
17
+
18
+ - **`headersInput`** — Request headers (e.g. `Headers` or plain object).
19
+ - **`options.generateIfMissing`** — If `true` (default), creates a new trace ID when the header is missing; if `false`, returns `undefined` in that case.
20
+
21
+ **Returns:** `NormalizedTraceContext` or `undefined`.
22
+
23
+ ```ts
24
+ import { normalizeTraceContext } from '@kids-reporter/logger'
25
+
26
+ const ctx = normalizeTraceContext(request.headers)
27
+ // ctx.traceId, ctx.traceHeaders['X-Cloud-Trace-Context']
28
+ ```
29
+
30
+ ### `getTraceLogFields(headersInput?, options?)`
31
+
32
+ Builds an object of trace-related fields for structured logging. Includes `logging.googleapis.com/trace` when `projectId` is set for GCP log correlation.
33
+
34
+ - **`headersInput`** — Same as `normalizeTraceContext`.
35
+ - **`options.projectId`** — GCP project ID for the trace field; falls back to `GOOGLE_CLOUD_PROJECT` or `GCP_PROJECT` env vars.
36
+ - **`options.generateIfMissing`** — Same as `normalizeTraceContext` (default `false` here).
37
+
38
+ **Returns:** Object with `traceId` and optionally `logging.googleapis.com/trace`.
39
+
40
+ ```ts
41
+ import { getTraceLogFields } from '@kids-reporter/logger'
42
+
43
+ const fields = getTraceLogFields(req.headers, { projectId: 'my-gcp-project' })
44
+ console.log(JSON.stringify({ ...fields, message: 'Request processed' }))
45
+ ```
46
+
47
+ ### `getGcpTraceField({ projectId, traceId })`
48
+
49
+ Returns the GCP trace resource name: `projects/{projectId}/traces/{traceId}`. Returns `undefined` if `projectId` or `traceId` is missing.
50
+
51
+ ### `emitStructured(payload)`
52
+
53
+ Writes a structured log entry to the console as JSON. Uses `console.error` for ERROR/ALERT/CRITICAL, `console.warn` for WARNING, and `console.log` for others.
54
+
55
+ ```ts
56
+ import { emitStructured } from '@kids-reporter/logger'
57
+
58
+ emitStructured({
59
+ severity: 'INFO',
60
+ message: 'User signed in',
61
+ userId: 'usr_123',
62
+ ...getTraceLogFields(request.headers),
63
+ })
64
+ ```
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './utils.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './utils.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
@@ -0,0 +1,15 @@
1
+ export type LogSeverity = 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'ALERT' | 'CRITICAL';
2
+ export type StructuredLogPayload = {
3
+ severity: LogSeverity;
4
+ message?: string;
5
+ } & Record<string, unknown>;
6
+ /** Supports Express req.headers where values can be string | string[]. */
7
+ export type TraceHeaderInput = Headers | Record<string, string | string[] | undefined | unknown> | undefined;
8
+ /** Trace context for GCP Cloud Logging correlation. Only traceId is propagated. */
9
+ export type NormalizedTraceContext = {
10
+ traceId: string;
11
+ traceHeaders: {
12
+ 'X-Cloud-Trace-Context': string;
13
+ };
14
+ };
15
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GACnB,OAAO,GACP,MAAM,GACN,QAAQ,GACR,SAAS,GACT,OAAO,GACP,OAAO,GACP,UAAU,CAAA;AAEd,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,WAAW,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE3B,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GACxB,OAAO,GACP,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAAC,GACvD,SAAS,CAAA;AAEb,mFAAmF;AACnF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE;QACZ,uBAAuB,EAAE,MAAM,CAAA;KAChC,CAAA;CACF,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ import { NormalizedTraceContext, StructuredLogPayload, TraceHeaderInput } from './types.js';
2
+ export declare function normalizeTraceContext(headersInput: TraceHeaderInput | undefined, options: {
3
+ generateIfMissing: true;
4
+ }): NormalizedTraceContext;
5
+ export declare function normalizeTraceContext(headersInput?: TraceHeaderInput, options?: {
6
+ generateIfMissing?: boolean;
7
+ }): NormalizedTraceContext | undefined;
8
+ export declare function getGcpTraceField({ projectId, traceId, }: {
9
+ projectId?: string;
10
+ traceId: string;
11
+ }): string | undefined;
12
+ export declare function getTraceLogFields(headersInput?: TraceHeaderInput, options?: {
13
+ projectId?: string;
14
+ generateIfMissing?: boolean;
15
+ }): {};
16
+ export declare function emitStructured(payload: StructuredLogPayload): void;
17
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,YAAY,CAAA;AAuFnB,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,gBAAgB,GAAG,SAAS,EAC1C,OAAO,EAAE;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,GACnC,sBAAsB,CAAA;AACzB,wBAAgB,qBAAqB,CACnC,YAAY,CAAC,EAAE,gBAAgB,EAC/B,OAAO,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,GACxC,sBAAsB,GAAG,SAAS,CAAA;AAyBrC,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;CAChB,sBAKA;AAED,wBAAgB,iBAAiB,CAC/B,YAAY,CAAC,EAAE,gBAAgB,EAC/B,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAO,MAsBlE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,oBAAoB,QAiB3D"}
package/dist/utils.js ADDED
@@ -0,0 +1,129 @@
1
+ const XCLOUD_TRACE_HEADER = 'x-cloud-trace-context';
2
+ const TRACE_ID_REGEX = /^[a-f0-9]{32}$/i;
3
+ const CONSOLE_BY_SEVERITY = {
4
+ ALERT: 'error',
5
+ CRITICAL: 'error',
6
+ ERROR: 'error',
7
+ WARNING: 'warn',
8
+ NOTICE: 'log',
9
+ INFO: 'log',
10
+ DEBUG: 'log',
11
+ };
12
+ function normalizeHeaderValue(value) {
13
+ if (value === undefined || value === null) {
14
+ return undefined;
15
+ }
16
+ if (typeof value === 'string') {
17
+ return value;
18
+ }
19
+ if (Array.isArray(value) &&
20
+ value.length > 0 &&
21
+ typeof value[0] === 'string') {
22
+ return value[0];
23
+ }
24
+ return undefined;
25
+ }
26
+ function getHeaderValue(input, headerName) {
27
+ if (!input) {
28
+ return undefined;
29
+ }
30
+ if (typeof Headers !== 'undefined' && input instanceof Headers) {
31
+ return input.get(headerName) || undefined;
32
+ }
33
+ const lowered = headerName.toLowerCase();
34
+ for (const [key, value] of Object.entries(input)) {
35
+ if (key.toLowerCase() === lowered) {
36
+ return normalizeHeaderValue(value);
37
+ }
38
+ }
39
+ return undefined;
40
+ }
41
+ function getRandomHex(length) {
42
+ const bytes = new Uint8Array(Math.ceil(length / 2));
43
+ if (globalThis.crypto &&
44
+ typeof globalThis.crypto.getRandomValues === 'function') {
45
+ globalThis.crypto.getRandomValues(bytes);
46
+ }
47
+ else {
48
+ for (let i = 0; i < bytes.length; i += 1) {
49
+ bytes[i] = Math.floor(Math.random() * 256);
50
+ }
51
+ }
52
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, '0'))
53
+ .join('')
54
+ .slice(0, length);
55
+ }
56
+ /**
57
+ * Parses traceId from GCP X-Cloud-Trace-Context.
58
+ * Accepts "traceId;o=1" or "traceId/spanId;o=1" (spanId is ignored).
59
+ */
60
+ function parseXCloudTraceContext(xCloudTraceContext) {
61
+ if (!xCloudTraceContext) {
62
+ return undefined;
63
+ }
64
+ const [traceAndSpan] = xCloudTraceContext.trim().split(';');
65
+ const traceId = traceAndSpan.split('/')[0]?.trim();
66
+ if (!traceId || !TRACE_ID_REGEX.test(traceId)) {
67
+ return undefined;
68
+ }
69
+ return traceId.toLowerCase();
70
+ }
71
+ export function normalizeTraceContext(headersInput, options = {}) {
72
+ /* eslint-enable no-redeclare */
73
+ const shouldGenerate = options.generateIfMissing !== false;
74
+ const xCloudTraceContext = getHeaderValue(headersInput, XCLOUD_TRACE_HEADER);
75
+ const traceId = parseXCloudTraceContext(xCloudTraceContext);
76
+ if (!traceId && !shouldGenerate) {
77
+ return undefined;
78
+ }
79
+ const resolvedTraceId = traceId ?? getRandomHex(32);
80
+ const xCloudValue = `${resolvedTraceId};o=1`;
81
+ return {
82
+ traceId: resolvedTraceId,
83
+ traceHeaders: {
84
+ 'X-Cloud-Trace-Context': xCloudValue,
85
+ },
86
+ };
87
+ }
88
+ export function getGcpTraceField({ projectId, traceId, }) {
89
+ if (!projectId || !traceId) {
90
+ return undefined;
91
+ }
92
+ return `projects/${projectId}/traces/${traceId}`;
93
+ }
94
+ export function getTraceLogFields(headersInput, options = {}) {
95
+ const traceContext = normalizeTraceContext(headersInput, {
96
+ generateIfMissing: options.generateIfMissing ?? false,
97
+ });
98
+ if (!traceContext) {
99
+ return {};
100
+ }
101
+ const projectId = options.projectId ||
102
+ process.env.GOOGLE_CLOUD_PROJECT ||
103
+ process.env.GCP_PROJECT;
104
+ const traceField = getGcpTraceField({
105
+ projectId,
106
+ traceId: traceContext.traceId,
107
+ });
108
+ return {
109
+ ...(traceField ? { 'logging.googleapis.com/trace': traceField } : {}),
110
+ traceId: traceContext.traceId,
111
+ };
112
+ }
113
+ export function emitStructured(payload) {
114
+ const severity = typeof payload?.severity === 'string'
115
+ ? payload.severity.toUpperCase()
116
+ : 'INFO';
117
+ const method = CONSOLE_BY_SEVERITY[severity] || 'log';
118
+ const message = JSON.stringify(payload);
119
+ if (method === 'error') {
120
+ console.error(message);
121
+ return;
122
+ }
123
+ if (method === 'warn') {
124
+ console.warn(message);
125
+ return;
126
+ }
127
+ console.log(message);
128
+ }
129
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import baseConfig, {
2
+ nodeConfig,
3
+ typescriptConfig,
4
+ } from '../../eslint.base.config.mjs'
5
+
6
+ export default [
7
+ ...baseConfig,
8
+ {
9
+ ...typescriptConfig,
10
+ files: ['src/**/*.ts'],
11
+ rules: {
12
+ ...typescriptConfig.rules,
13
+ },
14
+ },
15
+ {
16
+ ...nodeConfig,
17
+ files: ['**/*.config.{js,mjs,cjs}'],
18
+ },
19
+ {
20
+ ignores: ['node_modules/**', 'dist/**'],
21
+ },
22
+ ]
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@kids-reporter/logger",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Shared structured logger for Kids Reporter services",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "postinstall": "yarn build",
19
+ "lint:check": "eslint src --config ./eslint.config.mjs --ext .ts",
20
+ "type-check": "tsc --noEmit -p tsconfig.json"
21
+ },
22
+ "devDependencies": {
23
+ "eslint": "^9.0.0",
24
+ "typescript": "^5.9.2"
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './types.js'
2
+ export * from './utils.js'
package/src/types.ts ADDED
@@ -0,0 +1,27 @@
1
+ export type LogSeverity =
2
+ | 'DEBUG'
3
+ | 'INFO'
4
+ | 'NOTICE'
5
+ | 'WARNING'
6
+ | 'ERROR'
7
+ | 'ALERT'
8
+ | 'CRITICAL'
9
+
10
+ export type StructuredLogPayload = {
11
+ severity: LogSeverity
12
+ message?: string
13
+ } & Record<string, unknown>
14
+
15
+ /** Supports Express req.headers where values can be string | string[]. */
16
+ export type TraceHeaderInput =
17
+ | Headers
18
+ | Record<string, string | string[] | undefined | unknown>
19
+ | undefined
20
+
21
+ /** Trace context for GCP Cloud Logging correlation. Only traceId is propagated. */
22
+ export type NormalizedTraceContext = {
23
+ traceId: string
24
+ traceHeaders: {
25
+ 'X-Cloud-Trace-Context': string
26
+ }
27
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,180 @@
1
+ import {
2
+ NormalizedTraceContext,
3
+ StructuredLogPayload,
4
+ TraceHeaderInput,
5
+ } from './types.js'
6
+
7
+ const XCLOUD_TRACE_HEADER = 'x-cloud-trace-context'
8
+ const TRACE_ID_REGEX = /^[a-f0-9]{32}$/i
9
+
10
+ const CONSOLE_BY_SEVERITY: Record<string, 'log' | 'warn' | 'error'> = {
11
+ ALERT: 'error',
12
+ CRITICAL: 'error',
13
+ ERROR: 'error',
14
+ WARNING: 'warn',
15
+ NOTICE: 'log',
16
+ INFO: 'log',
17
+ DEBUG: 'log',
18
+ }
19
+
20
+ function normalizeHeaderValue(
21
+ value: string | string[] | undefined | unknown
22
+ ): string | undefined {
23
+ if (value === undefined || value === null) {
24
+ return undefined
25
+ }
26
+ if (typeof value === 'string') {
27
+ return value
28
+ }
29
+ if (
30
+ Array.isArray(value) &&
31
+ value.length > 0 &&
32
+ typeof value[0] === 'string'
33
+ ) {
34
+ return value[0]
35
+ }
36
+ return undefined
37
+ }
38
+
39
+ function getHeaderValue(input: TraceHeaderInput, headerName: string) {
40
+ if (!input) {
41
+ return undefined
42
+ }
43
+ if (typeof Headers !== 'undefined' && input instanceof Headers) {
44
+ return input.get(headerName) || undefined
45
+ }
46
+ const lowered = headerName.toLowerCase()
47
+ for (const [key, value] of Object.entries(input)) {
48
+ if (key.toLowerCase() === lowered) {
49
+ return normalizeHeaderValue(value)
50
+ }
51
+ }
52
+ return undefined
53
+ }
54
+
55
+ function getRandomHex(length: number) {
56
+ const bytes = new Uint8Array(Math.ceil(length / 2))
57
+ if (
58
+ globalThis.crypto &&
59
+ typeof globalThis.crypto.getRandomValues === 'function'
60
+ ) {
61
+ globalThis.crypto.getRandomValues(bytes)
62
+ } else {
63
+ for (let i = 0; i < bytes.length; i += 1) {
64
+ bytes[i] = Math.floor(Math.random() * 256)
65
+ }
66
+ }
67
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, '0'))
68
+ .join('')
69
+ .slice(0, length)
70
+ }
71
+
72
+ /**
73
+ * Parses traceId from GCP X-Cloud-Trace-Context.
74
+ * Accepts "traceId;o=1" or "traceId/spanId;o=1" (spanId is ignored).
75
+ */
76
+ function parseXCloudTraceContext(
77
+ xCloudTraceContext?: string
78
+ ): string | undefined {
79
+ if (!xCloudTraceContext) {
80
+ return undefined
81
+ }
82
+ const [traceAndSpan] = xCloudTraceContext.trim().split(';')
83
+ const traceId = traceAndSpan.split('/')[0]?.trim()
84
+ if (!traceId || !TRACE_ID_REGEX.test(traceId)) {
85
+ return undefined
86
+ }
87
+ return traceId.toLowerCase()
88
+ }
89
+
90
+ // Overloads: when generateIfMissing is true, return is always NormalizedTraceContext
91
+ /* eslint-disable no-redeclare -- overload signatures + implementation */
92
+ export function normalizeTraceContext(
93
+ headersInput: TraceHeaderInput | undefined,
94
+ options: { generateIfMissing: true }
95
+ ): NormalizedTraceContext
96
+ export function normalizeTraceContext(
97
+ headersInput?: TraceHeaderInput,
98
+ options?: { generateIfMissing?: boolean }
99
+ ): NormalizedTraceContext | undefined
100
+ export function normalizeTraceContext(
101
+ headersInput?: TraceHeaderInput,
102
+ options: { generateIfMissing?: boolean } = {}
103
+ ) {
104
+ /* eslint-enable no-redeclare */
105
+ const shouldGenerate = options.generateIfMissing !== false
106
+ const xCloudTraceContext = getHeaderValue(headersInput, XCLOUD_TRACE_HEADER)
107
+ const traceId = parseXCloudTraceContext(xCloudTraceContext)
108
+
109
+ if (!traceId && !shouldGenerate) {
110
+ return undefined
111
+ }
112
+
113
+ const resolvedTraceId = traceId ?? getRandomHex(32)
114
+ const xCloudValue = `${resolvedTraceId};o=1`
115
+
116
+ return {
117
+ traceId: resolvedTraceId,
118
+ traceHeaders: {
119
+ 'X-Cloud-Trace-Context': xCloudValue,
120
+ },
121
+ }
122
+ }
123
+
124
+ export function getGcpTraceField({
125
+ projectId,
126
+ traceId,
127
+ }: {
128
+ projectId?: string
129
+ traceId: string
130
+ }) {
131
+ if (!projectId || !traceId) {
132
+ return undefined
133
+ }
134
+ return `projects/${projectId}/traces/${traceId}`
135
+ }
136
+
137
+ export function getTraceLogFields(
138
+ headersInput?: TraceHeaderInput,
139
+ options: { projectId?: string; generateIfMissing?: boolean } = {}
140
+ ) {
141
+ const traceContext = normalizeTraceContext(headersInput, {
142
+ generateIfMissing: options.generateIfMissing ?? false,
143
+ })
144
+ if (!traceContext) {
145
+ return {}
146
+ }
147
+
148
+ const projectId =
149
+ options.projectId ||
150
+ process.env.GOOGLE_CLOUD_PROJECT ||
151
+ process.env.GCP_PROJECT
152
+ const traceField = getGcpTraceField({
153
+ projectId,
154
+ traceId: traceContext.traceId,
155
+ })
156
+
157
+ return {
158
+ ...(traceField ? { 'logging.googleapis.com/trace': traceField } : {}),
159
+ traceId: traceContext.traceId,
160
+ }
161
+ }
162
+
163
+ export function emitStructured(payload: StructuredLogPayload) {
164
+ const severity =
165
+ typeof payload?.severity === 'string'
166
+ ? payload.severity.toUpperCase()
167
+ : 'INFO'
168
+ const method = CONSOLE_BY_SEVERITY[severity] || 'log'
169
+ const message = JSON.stringify(payload)
170
+
171
+ if (method === 'error') {
172
+ console.error(message)
173
+ return
174
+ }
175
+ if (method === 'warn') {
176
+ console.warn(message)
177
+ return
178
+ }
179
+ console.log(message)
180
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "rootDir": "./src",
7
+ "outDir": "./dist",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "noEmit": false,
12
+ "strict": true,
13
+ "skipLibCheck": true
14
+ },
15
+ "include": ["src/**/*.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }