From 90bba5712a171b42e358df5d881eb435ebdaaca7 Mon Sep 17 00:00:00 2001 From: Phillip Barta Date: Fri, 27 Feb 2026 01:25:13 +0100 Subject: [PATCH 1/3] refactor: add cross-environment decodeBase64 method --- .github/workflows/ci.yml | 3 +- globals.d.ts | 104 +++++++++++++++++++++++++++++++++++++++ package.json | 4 -- src/base64.ts | 75 ++++++++++++++++++++++++++++ src/index.ts | 18 +------ tsconfig.json | 2 +- 6 files changed, 183 insertions(+), 23 deletions(-) create mode 100644 globals.d.ts create mode 100644 src/base64.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fff328..0612a79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,8 @@ jobs: matrix: node-version: - 18 - - '*' + - 25 # Node.js 25 support Uint8Array.fromBase64() + - 'lts/*' steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/globals.d.ts b/globals.d.ts new file mode 100644 index 0000000..929e202 --- /dev/null +++ b/globals.d.ts @@ -0,0 +1,104 @@ +// Adapted from file://./node_modules/typescript/lib/lib.dom.d.ts so we don't have to include the entire DOM lib +// Ref: https://github.com/microsoft/TypeScript/issues/31535, https://github.com/microsoft/TypeScript/issues/41727, https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1685 + +/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ +declare function atob(data: string): string; +/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ +declare function btoa(data: string): string; + +type AllowSharedBufferSource = + | ArrayBufferLike + | ArrayBufferView; + +interface TextDecodeOptions { + stream?: boolean; +} + +interface TextDecoderOptions { + fatal?: boolean; + ignoreBOM?: boolean; +} + +/** + * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) + */ +interface TextDecoder extends TextDecoderCommon { + /** + * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) + */ + decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string; +} + +// eslint-disable-next-line no-var +declare var TextDecoder: { + prototype: TextDecoder; + new (label?: string, options?: TextDecoderOptions): TextDecoder; +}; + +interface TextEncoderEncodeIntoResult { + read: number; + written: number; +} + +/** + * TextEncoder takes a stream of code points as input and emits a stream of UTF-8 bytes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder) + */ +interface TextEncoder extends TextEncoderCommon { + /** + * Returns the result of running UTF-8's encoder. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode) + */ + encode(input?: string): Uint8Array; + /** + * Runs UTF-8's encoder on source, stores the result of that operation into destination, and returns the progress made as an object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto) + */ + encodeInto( + source: string, + destination: Uint8Array, + ): TextEncoderEncodeIntoResult; +} + +// eslint-disable-next-line no-var +declare var TextEncoder: { + prototype: TextEncoder; + new (): TextEncoder; +}; + +interface TextDecoderCommon { + /** + * Returns encoding's name, lowercased. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/encoding) + */ + readonly encoding: string; + /** + * Returns true if error mode is "fatal", otherwise false. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/fatal) + */ + readonly fatal: boolean; + /** + * Returns the value of ignore BOM. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/ignoreBOM) + */ + readonly ignoreBOM: boolean; +} + +interface TextEncoderCommon { + /** + * Returns "utf-8". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encoding) + */ + readonly encoding: string; +} diff --git a/package.json b/package.json index c9b588c..6c23986 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "devDependencies": { "@borderless/ts-scripts": "^0.15.0", "@size-limit/preset-small-lib": "^12.1.0", - "@types/node": "^20.19.35", "@vitest/coverage-v8": "^3.2.4", "size-limit": "^12.1.0", "typescript": "^5.9.3", @@ -40,9 +39,6 @@ "size-limit": [ { "path": "dist/index.js", - "ignore": [ - "node:buffer" - ], "limit": "1 kB" } ], diff --git a/src/base64.ts b/src/base64.ts new file mode 100644 index 0000000..1ea9dda --- /dev/null +++ b/src/base64.ts @@ -0,0 +1,75 @@ +type Uint8ArrayWithBase64 = typeof Uint8Array & { + fromBase64?: (str: string) => Uint8Array; +}; + +type Uint8ArrayInstanceWithBase64 = Uint8Array & { + toBase64?: () => string; +}; + +type BufferLike = { + from( + input: string, + encoding: 'base64' | 'utf-8', + ): { toString(encoding: 'utf-8' | 'base64'): string }; +}; + +const NodeBuffer = (globalThis as any).Buffer as BufferLike | undefined; +const uint8ArrayPrototype = + Uint8Array.prototype as Uint8ArrayInstanceWithBase64; + +const textDecoder = new TextDecoder('utf-8'); +const textEncoder = new TextEncoder(); + +/** + * Decode base64 string. + * @private + */ +export const decodeBase64: (str: string) => string = (() => { + // 1) Node.js (fast path) + if (typeof NodeBuffer?.from === 'function') { + return (str: string) => NodeBuffer.from(str, 'base64').toString('utf-8'); + } + + // 2) Modern Web / some runtimes + if (typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function') { + return (str: string) => + textDecoder.decode((Uint8Array as Uint8ArrayWithBase64).fromBase64!(str)); + } + + // 3) Browser fallback + return (str: string) => { + const binary = atob(str); + return textDecoder.decode( + Uint8Array.from(binary, (char) => char.charCodeAt(0)), + ); + }; +})(); + +/** + * Encode string to base64. + * @private + */ +export const encodeBase64: (str: string) => string = (() => { + // 1) Node.js (fast path) + if (typeof NodeBuffer?.from === 'function') { + return (str: string) => NodeBuffer.from(str, 'utf-8').toString('base64'); + } + + // 2) Modern Web / some runtimes + if (typeof uint8ArrayPrototype.toBase64 === 'function') { + return (str: string) => + (textEncoder.encode(str) as Uint8ArrayInstanceWithBase64).toBase64!(); + } + + // 3) Browser fallback + return (str: string) => { + const bytes = textEncoder.encode(str); + let binary = ''; + + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + + return btoa(binary); + }; +})(); diff --git a/src/index.ts b/src/index.ts index 3289d9f..c36d6c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ * MIT Licensed */ -import { Buffer } from 'node:buffer'; +import { decodeBase64, encodeBase64 } from './base64.js'; /** * Object to represent user credentials. @@ -104,19 +104,3 @@ const CREDENTIALS_REGEXP = * @private */ const CONTROL_CHARS_REGEXP = /[\x00-\x1F\x7F]/; - -/** - * Decode base64 string. - * @private - */ -function decodeBase64(str: string): string { - return Buffer.from(str, 'base64').toString(); -} - -/** - * Encode string to base64. - * @private - */ -function encodeBase64(str: string): string { - return Buffer.from(str, 'utf-8').toString('base64'); -} diff --git a/tsconfig.json b/tsconfig.json index 4a23eec..63f1653 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "outDir": "dist", "module": "nodenext", "moduleResolution": "nodenext", - "types": ["node"] + "types": ["./globals.d.ts"] }, "include": ["src/**/*"] } From d28259b86ed1e38ee5866739a6bc3e06bb9b9a78 Mon Sep 17 00:00:00 2001 From: Phillip Barta Date: Thu, 23 Apr 2026 15:34:53 +0200 Subject: [PATCH 2/3] add base64 benchmarks and switch imlementations based on result --- benchmarks/base64.bench.ts | 99 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 benchmarks/base64.bench.ts diff --git a/benchmarks/base64.bench.ts b/benchmarks/base64.bench.ts new file mode 100644 index 0000000..3b3d5f5 --- /dev/null +++ b/benchmarks/base64.bench.ts @@ -0,0 +1,99 @@ +import { bench, describe } from 'vitest'; + +type Uint8ArrayWithBase64 = typeof Uint8Array & { + fromBase64?: (str: string) => Uint8Array; +}; + +type BufferLike = { + from( + input: string, + encoding: 'base64' | 'utf-8', + ): { toString(encoding: 'utf-8' | 'base64'): string }; +}; + +const NodeBuffer = (globalThis as any).Buffer as BufferLike | undefined; + +const textDecoder = new TextDecoder('utf-8'); + +function decodeBase64WithUint8Array(str: string): string { + return textDecoder.decode( + (Uint8Array as Uint8ArrayWithBase64).fromBase64!(str), + ); +} + +function decodeBase64WithNodeBuffer(str: string): string { + return NodeBuffer!.from(str, 'base64').toString('utf-8'); +} + +function decodeBase64WithAtob(str: string): string { + const binary = atob(str); + return textDecoder.decode( + Uint8Array.from(binary, (char) => char.charCodeAt(0)), + ); +} + +const hasUint8ArrayFromBase64 = + typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function'; +const hasNodeBuffer = typeof NodeBuffer?.from === 'function'; + +const payloads = [ + { + name: 'short username/password', + decoded: 'user:password', + }, + { + name: 'common API style credential', + decoded: 'api-client-42:sk_live_b36WzMj0n95wE1y8hHkR2iS4qT7vNuPx', + }, + { + name: 'long token-like password', + decoded: + 'service-account:7f2d9c31f7f14131a65d5315f2dbdb34dc5ddacb4f57b74a04a066f53f8e92bf', + }, +].map((payload) => ({ + ...payload, + encoded: NodeBuffer!.from(payload.decoded, 'utf-8').toString('base64'), +})); + +// Sanity check that all decoding methods produce the same results before benchmarking +for (const payload of payloads) { + if (decodeBase64WithAtob(payload.encoded) !== payload.decoded) { + throw new Error(`atob decode failed for ${payload.name}`); + } + + if ( + hasUint8ArrayFromBase64 && + decodeBase64WithUint8Array(payload.encoded) !== payload.decoded + ) { + throw new Error(`Uint8Array.fromBase64 decode failed for ${payload.name}`); + } + + if ( + hasNodeBuffer && + decodeBase64WithNodeBuffer(payload.encoded) !== payload.decoded + ) { + throw new Error(`Buffer decode failed for ${payload.name}`); + } +} + +describe('decode base64 for basic-auth payloads', () => { + for (const payload of payloads) { + describe(payload.name, () => { + if (hasUint8ArrayFromBase64) { + bench('Uint8Array.fromBase64 + TextDecoder', () => { + decodeBase64WithUint8Array(payload.encoded); + }); + } + + if (hasNodeBuffer) { + bench('Buffer.from(base64).toString(utf-8)', () => { + decodeBase64WithNodeBuffer(payload.encoded); + }); + } + + bench('atob + Uint8Array.from + TextDecoder', () => { + decodeBase64WithAtob(payload.encoded); + }); + }); + } +}); From c585a052f6c679e93f1583caeb650035f77818d0 Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Thu, 14 May 2026 10:00:15 -0700 Subject: [PATCH 3/3] Only expose the two methods --- .github/workflows/ci.yml | 4 +- benchmarks/base64.bench.ts | 18 ++++--- globals.d.ts | 104 ------------------------------------- package.json | 7 +-- src/base64.spec.ts | 41 +++++++++++++++ src/base64.ts | 89 +++++++------------------------ src/index.ts | 6 +-- tsconfig.json | 4 +- 8 files changed, 80 insertions(+), 193 deletions(-) delete mode 100644 globals.d.ts create mode 100644 src/base64.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0612a79..f64634c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,9 @@ jobs: strategy: matrix: node-version: - - 18 + - 20 - 25 # Node.js 25 support Uint8Array.fromBase64() - - 'lts/*' + - '*' steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/benchmarks/base64.bench.ts b/benchmarks/base64.bench.ts index 3b3d5f5..90c4271 100644 --- a/benchmarks/base64.bench.ts +++ b/benchmarks/base64.bench.ts @@ -79,17 +79,19 @@ for (const payload of payloads) { describe('decode base64 for basic-auth payloads', () => { for (const payload of payloads) { describe(payload.name, () => { - if (hasUint8ArrayFromBase64) { - bench('Uint8Array.fromBase64 + TextDecoder', () => { + bench.skipIf(!hasUint8ArrayFromBase64)( + 'Uint8Array.fromBase64 + TextDecoder', + () => { decodeBase64WithUint8Array(payload.encoded); - }); - } + }, + ); - if (hasNodeBuffer) { - bench('Buffer.from(base64).toString(utf-8)', () => { + bench.skipIf(!hasNodeBuffer)( + 'Buffer.from(base64).toString(utf-8)', + () => { decodeBase64WithNodeBuffer(payload.encoded); - }); - } + }, + ); bench('atob + Uint8Array.from + TextDecoder', () => { decodeBase64WithAtob(payload.encoded); diff --git a/globals.d.ts b/globals.d.ts deleted file mode 100644 index 929e202..0000000 --- a/globals.d.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Adapted from file://./node_modules/typescript/lib/lib.dom.d.ts so we don't have to include the entire DOM lib -// Ref: https://github.com/microsoft/TypeScript/issues/31535, https://github.com/microsoft/TypeScript/issues/41727, https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1685 - -/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ -declare function atob(data: string): string; -/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ -declare function btoa(data: string): string; - -type AllowSharedBufferSource = - | ArrayBufferLike - | ArrayBufferView; - -interface TextDecodeOptions { - stream?: boolean; -} - -interface TextDecoderOptions { - fatal?: boolean; - ignoreBOM?: boolean; -} - -/** - * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) - */ -interface TextDecoder extends TextDecoderCommon { - /** - * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) - */ - decode(input?: AllowSharedBufferSource, options?: TextDecodeOptions): string; -} - -// eslint-disable-next-line no-var -declare var TextDecoder: { - prototype: TextDecoder; - new (label?: string, options?: TextDecoderOptions): TextDecoder; -}; - -interface TextEncoderEncodeIntoResult { - read: number; - written: number; -} - -/** - * TextEncoder takes a stream of code points as input and emits a stream of UTF-8 bytes. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder) - */ -interface TextEncoder extends TextEncoderCommon { - /** - * Returns the result of running UTF-8's encoder. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode) - */ - encode(input?: string): Uint8Array; - /** - * Runs UTF-8's encoder on source, stores the result of that operation into destination, and returns the progress made as an object. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto) - */ - encodeInto( - source: string, - destination: Uint8Array, - ): TextEncoderEncodeIntoResult; -} - -// eslint-disable-next-line no-var -declare var TextEncoder: { - prototype: TextEncoder; - new (): TextEncoder; -}; - -interface TextDecoderCommon { - /** - * Returns encoding's name, lowercased. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/encoding) - */ - readonly encoding: string; - /** - * Returns true if error mode is "fatal", otherwise false. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/fatal) - */ - readonly fatal: boolean; - /** - * Returns the value of ignore BOM. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/ignoreBOM) - */ - readonly ignoreBOM: boolean; -} - -interface TextEncoderCommon { - /** - * Returns "utf-8". - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encoding) - */ - readonly encoding: string; -} diff --git a/package.json b/package.json index 6c23986..a78e8c9 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,11 @@ "devDependencies": { "@borderless/ts-scripts": "^0.15.0", "@size-limit/preset-small-lib": "^12.1.0", - "@vitest/coverage-v8": "^3.2.4", + "@types/node": "^25.7.0", + "@vitest/coverage-v8": "^4.1.6", "size-limit": "^12.1.0", - "typescript": "^5.9.3", - "vitest": "^3.2.4" + "typescript": "^6.0.3", + "vitest": "^4.1.6" }, "engines": { "node": ">=18" diff --git a/src/base64.spec.ts b/src/base64.spec.ts new file mode 100644 index 0000000..2abbfa9 --- /dev/null +++ b/src/base64.spec.ts @@ -0,0 +1,41 @@ +import { afterAll, assert, beforeAll, describe, it, vi } from 'vitest'; + +const importBase64 = async () => { + vi.resetModules(); + + const { base64 } = await import('./base64.js'); + return base64; +}; + +describe('base64', async () => { + let base64 = await importBase64(); + + afterAll(() => { + vi.unstubAllGlobals(); + }); + + describe.skipIf(!Buffer)('Buffer', async () => { + it('should encode base64', () => { + assert.strictEqual(base64.encode('foo:bar'), 'Zm9vOmJhcg=='); + }); + + it('should decode base64', () => { + assert.strictEqual(base64.decode('Zm9vOmJhcg=='), 'foo:bar'); + }); + }); + + describe.skipIf(!Uint8Array.prototype.toBase64)('Uint8Array', async () => { + beforeAll(async () => { + vi.stubGlobal('Buffer', undefined); + base64 = await importBase64(); + }); + + it('should encode base64', () => { + assert.strictEqual(base64.encode('foo:bar'), 'Zm9vOmJhcg=='); + }); + + it('should decode base64', () => { + assert.strictEqual(base64.decode('Zm9vOmJhcg=='), 'foo:bar'); + }); + }); +}); diff --git a/src/base64.ts b/src/base64.ts index 1ea9dda..30be663 100644 --- a/src/base64.ts +++ b/src/base64.ts @@ -1,75 +1,22 @@ -type Uint8ArrayWithBase64 = typeof Uint8Array & { - fromBase64?: (str: string) => Uint8Array; -}; - -type Uint8ArrayInstanceWithBase64 = Uint8Array & { - toBase64?: () => string; -}; - -type BufferLike = { - from( - input: string, - encoding: 'base64' | 'utf-8', - ): { toString(encoding: 'utf-8' | 'base64'): string }; -}; - -const NodeBuffer = (globalThis as any).Buffer as BufferLike | undefined; -const uint8ArrayPrototype = - Uint8Array.prototype as Uint8ArrayInstanceWithBase64; - -const textDecoder = new TextDecoder('utf-8'); -const textEncoder = new TextEncoder(); - -/** - * Decode base64 string. - * @private - */ -export const decodeBase64: (str: string) => string = (() => { - // 1) Node.js (fast path) - if (typeof NodeBuffer?.from === 'function') { - return (str: string) => NodeBuffer.from(str, 'base64').toString('utf-8'); - } - - // 2) Modern Web / some runtimes - if (typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function') { - return (str: string) => - textDecoder.decode((Uint8Array as Uint8ArrayWithBase64).fromBase64!(str)); - } - - // 3) Browser fallback - return (str: string) => { - const binary = atob(str); - return textDecoder.decode( - Uint8Array.from(binary, (char) => char.charCodeAt(0)), - ); - }; -})(); - -/** - * Encode string to base64. - * @private - */ -export const encodeBase64: (str: string) => string = (() => { - // 1) Node.js (fast path) - if (typeof NodeBuffer?.from === 'function') { - return (str: string) => NodeBuffer.from(str, 'utf-8').toString('base64'); +export const base64 = (() => { + if (typeof Buffer !== 'undefined') { + return { + encode: (str: string) => Buffer.from(str, 'utf-8').toString('base64'), + decode: (str: string) => Buffer.from(str, 'base64').toString('utf-8'), + }; } - // 2) Modern Web / some runtimes - if (typeof uint8ArrayPrototype.toBase64 === 'function') { - return (str: string) => - (textEncoder.encode(str) as Uint8ArrayInstanceWithBase64).toBase64!(); - } - - // 3) Browser fallback - return (str: string) => { - const bytes = textEncoder.encode(str); - let binary = ''; - - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } - - return btoa(binary); + const textEncoder = new TextEncoder(); + const textDecoder = new TextDecoder(); + + return { + encode: (str: string) => { + const bytes = textEncoder.encode(str); + return bytes.toBase64(); + }, + decode: (str: string) => { + const bytes = Uint8Array.fromBase64(str); + return textDecoder.decode(bytes); + }, }; })(); diff --git a/src/index.ts b/src/index.ts index c36d6c4..d4d4b1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ * MIT Licensed */ -import { decodeBase64, encodeBase64 } from './base64.js'; +import { base64 } from './base64.js'; /** * Object to represent user credentials. @@ -34,7 +34,7 @@ export function parse(string: string): Credentials | undefined { if (!match) return undefined; // decode user pass - const userPass = decodeBase64(match[1]); + const userPass = base64.decode(match[1]); const colonIndex = userPass.indexOf(':'); if (colonIndex === -1) return undefined; @@ -84,7 +84,7 @@ export function format(credentials: Credentials): string { ); } - return 'Basic ' + encodeBase64(credentials.name + ':' + credentials.pass); + return 'Basic ' + base64.encode(credentials.name + ':' + credentials.pass); } /** diff --git a/tsconfig.json b/tsconfig.json index 63f1653..57f2fa9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,12 +2,12 @@ "extends": "@borderless/ts-scripts/configs/tsconfig.json", "compilerOptions": { "target": "es2023", - "lib": ["es2023"], + "lib": ["ESNext"], "rootDir": "src", "outDir": "dist", "module": "nodenext", "moduleResolution": "nodenext", - "types": ["./globals.d.ts"] + "types": ["node"] }, "include": ["src/**/*"] }