Skip to content

Commit c585a05

Browse files
committed
Only expose the two methods
1 parent d28259b commit c585a05

8 files changed

Lines changed: 80 additions & 193 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ jobs:
1111
strategy:
1212
matrix:
1313
node-version:
14-
- 18
14+
- 20
1515
- 25 # Node.js 25 support Uint8Array.fromBase64()
16-
- 'lts/*'
16+
- '*'
1717
steps:
1818
- uses: actions/checkout@v6
1919
- uses: actions/setup-node@v6

benchmarks/base64.bench.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,19 @@ for (const payload of payloads) {
7979
describe('decode base64 for basic-auth payloads', () => {
8080
for (const payload of payloads) {
8181
describe(payload.name, () => {
82-
if (hasUint8ArrayFromBase64) {
83-
bench('Uint8Array.fromBase64 + TextDecoder', () => {
82+
bench.skipIf(!hasUint8ArrayFromBase64)(
83+
'Uint8Array.fromBase64 + TextDecoder',
84+
() => {
8485
decodeBase64WithUint8Array(payload.encoded);
85-
});
86-
}
86+
},
87+
);
8788

88-
if (hasNodeBuffer) {
89-
bench('Buffer.from(base64).toString(utf-8)', () => {
89+
bench.skipIf(!hasNodeBuffer)(
90+
'Buffer.from(base64).toString(utf-8)',
91+
() => {
9092
decodeBase64WithNodeBuffer(payload.encoded);
91-
});
92-
}
93+
},
94+
);
9395

9496
bench('atob + Uint8Array.from + TextDecoder', () => {
9597
decodeBase64WithAtob(payload.encoded);

globals.d.ts

Lines changed: 0 additions & 104 deletions
This file was deleted.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@
2828
"devDependencies": {
2929
"@borderless/ts-scripts": "^0.15.0",
3030
"@size-limit/preset-small-lib": "^12.1.0",
31-
"@vitest/coverage-v8": "^3.2.4",
31+
"@types/node": "^25.7.0",
32+
"@vitest/coverage-v8": "^4.1.6",
3233
"size-limit": "^12.1.0",
33-
"typescript": "^5.9.3",
34-
"vitest": "^3.2.4"
34+
"typescript": "^6.0.3",
35+
"vitest": "^4.1.6"
3536
},
3637
"engines": {
3738
"node": ">=18"

src/base64.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { afterAll, assert, beforeAll, describe, it, vi } from 'vitest';
2+
3+
const importBase64 = async () => {
4+
vi.resetModules();
5+
6+
const { base64 } = await import('./base64.js');
7+
return base64;
8+
};
9+
10+
describe('base64', async () => {
11+
let base64 = await importBase64();
12+
13+
afterAll(() => {
14+
vi.unstubAllGlobals();
15+
});
16+
17+
describe.skipIf(!Buffer)('Buffer', async () => {
18+
it('should encode base64', () => {
19+
assert.strictEqual(base64.encode('foo:bar'), 'Zm9vOmJhcg==');
20+
});
21+
22+
it('should decode base64', () => {
23+
assert.strictEqual(base64.decode('Zm9vOmJhcg=='), 'foo:bar');
24+
});
25+
});
26+
27+
describe.skipIf(!Uint8Array.prototype.toBase64)('Uint8Array', async () => {
28+
beforeAll(async () => {
29+
vi.stubGlobal('Buffer', undefined);
30+
base64 = await importBase64();
31+
});
32+
33+
it('should encode base64', () => {
34+
assert.strictEqual(base64.encode('foo:bar'), 'Zm9vOmJhcg==');
35+
});
36+
37+
it('should decode base64', () => {
38+
assert.strictEqual(base64.decode('Zm9vOmJhcg=='), 'foo:bar');
39+
});
40+
});
41+
});

src/base64.ts

Lines changed: 18 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,22 @@
1-
type Uint8ArrayWithBase64 = typeof Uint8Array & {
2-
fromBase64?: (str: string) => Uint8Array;
3-
};
4-
5-
type Uint8ArrayInstanceWithBase64 = Uint8Array & {
6-
toBase64?: () => string;
7-
};
8-
9-
type BufferLike = {
10-
from(
11-
input: string,
12-
encoding: 'base64' | 'utf-8',
13-
): { toString(encoding: 'utf-8' | 'base64'): string };
14-
};
15-
16-
const NodeBuffer = (globalThis as any).Buffer as BufferLike | undefined;
17-
const uint8ArrayPrototype =
18-
Uint8Array.prototype as Uint8ArrayInstanceWithBase64;
19-
20-
const textDecoder = new TextDecoder('utf-8');
21-
const textEncoder = new TextEncoder();
22-
23-
/**
24-
* Decode base64 string.
25-
* @private
26-
*/
27-
export const decodeBase64: (str: string) => string = (() => {
28-
// 1) Node.js (fast path)
29-
if (typeof NodeBuffer?.from === 'function') {
30-
return (str: string) => NodeBuffer.from(str, 'base64').toString('utf-8');
31-
}
32-
33-
// 2) Modern Web / some runtimes
34-
if (typeof (Uint8Array as Uint8ArrayWithBase64).fromBase64 === 'function') {
35-
return (str: string) =>
36-
textDecoder.decode((Uint8Array as Uint8ArrayWithBase64).fromBase64!(str));
37-
}
38-
39-
// 3) Browser fallback
40-
return (str: string) => {
41-
const binary = atob(str);
42-
return textDecoder.decode(
43-
Uint8Array.from(binary, (char) => char.charCodeAt(0)),
44-
);
45-
};
46-
})();
47-
48-
/**
49-
* Encode string to base64.
50-
* @private
51-
*/
52-
export const encodeBase64: (str: string) => string = (() => {
53-
// 1) Node.js (fast path)
54-
if (typeof NodeBuffer?.from === 'function') {
55-
return (str: string) => NodeBuffer.from(str, 'utf-8').toString('base64');
1+
export const base64 = (() => {
2+
if (typeof Buffer !== 'undefined') {
3+
return {
4+
encode: (str: string) => Buffer.from(str, 'utf-8').toString('base64'),
5+
decode: (str: string) => Buffer.from(str, 'base64').toString('utf-8'),
6+
};
567
}
578

58-
// 2) Modern Web / some runtimes
59-
if (typeof uint8ArrayPrototype.toBase64 === 'function') {
60-
return (str: string) =>
61-
(textEncoder.encode(str) as Uint8ArrayInstanceWithBase64).toBase64!();
62-
}
63-
64-
// 3) Browser fallback
65-
return (str: string) => {
66-
const bytes = textEncoder.encode(str);
67-
let binary = '';
68-
69-
for (let i = 0; i < bytes.length; i++) {
70-
binary += String.fromCharCode(bytes[i]);
71-
}
72-
73-
return btoa(binary);
9+
const textEncoder = new TextEncoder();
10+
const textDecoder = new TextDecoder();
11+
12+
return {
13+
encode: (str: string) => {
14+
const bytes = textEncoder.encode(str);
15+
return bytes.toBase64();
16+
},
17+
decode: (str: string) => {
18+
const bytes = Uint8Array.fromBase64(str);
19+
return textDecoder.decode(bytes);
20+
},
7421
};
7522
})();

src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* MIT Licensed
77
*/
88

9-
import { decodeBase64, encodeBase64 } from './base64.js';
9+
import { base64 } from './base64.js';
1010

1111
/**
1212
* Object to represent user credentials.
@@ -34,7 +34,7 @@ export function parse(string: string): Credentials | undefined {
3434
if (!match) return undefined;
3535

3636
// decode user pass
37-
const userPass = decodeBase64(match[1]);
37+
const userPass = base64.decode(match[1]);
3838
const colonIndex = userPass.indexOf(':');
3939
if (colonIndex === -1) return undefined;
4040

@@ -84,7 +84,7 @@ export function format(credentials: Credentials): string {
8484
);
8585
}
8686

87-
return 'Basic ' + encodeBase64(credentials.name + ':' + credentials.pass);
87+
return 'Basic ' + base64.encode(credentials.name + ':' + credentials.pass);
8888
}
8989

9090
/**

tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"extends": "@borderless/ts-scripts/configs/tsconfig.json",
33
"compilerOptions": {
44
"target": "es2023",
5-
"lib": ["es2023"],
5+
"lib": ["ESNext"],
66
"rootDir": "src",
77
"outDir": "dist",
88
"module": "nodenext",
99
"moduleResolution": "nodenext",
10-
"types": ["./globals.d.ts"]
10+
"types": ["node"]
1111
},
1212
"include": ["src/**/*"]
1313
}

0 commit comments

Comments
 (0)