Skip to content

Commit 1d3c8e7

Browse files
committed
Migrate addon to N-API; add async & prebuilds
Migrate native addon from nan to node-addon-api and add a JS wrapper/ESM entry (index.js / index.mjs). Implement genAsync (worker-thread async) and make output length configurable (1–22, default 8) with input validation and a 64KB input limit. Replace hex-path with an optimized Base62 encoder, add thorough OpenSSL return-value checks, and update binding.gyp for N-API and Windows OpenSSL settings. Add prebuild support (prebuildify + node-gyp-build), a prebuild CI workflow, and integrate prebuild download into the publish workflow so most users won't need a compiler. Expand CI to test Linux/macOS/Windows across Node 20/22/24. Update package.json (version → 1.2.0, exports field, scripts, deps/devDeps) and add bench.js, CLI length/version flags, TypeScript defs (index.d.ts), .editorconfig and ESLint config, README and CHANGELOG entries documenting the changes.
1 parent 8fb996b commit 1d3c8e7

19 files changed

Lines changed: 1048 additions & 133 deletions

.editorconfig

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.cpp]
12+
indent_size = 4
13+
14+
[Makefile]
15+
indent_style = tab

.eslintrc.cjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
env: {
3+
node: true,
4+
es2022: true
5+
},
6+
parserOptions: {
7+
ecmaVersion: 2022
8+
},
9+
rules: {
10+
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
11+
'no-undef': 'error',
12+
'no-process-exit': 'off',
13+
eqeqeq: ['error', 'always', { null: 'ignore' }],
14+
'no-var': 'error',
15+
'prefer-const': 'error'
16+
}
17+
};

.github/workflows/ci.yml

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,53 @@ name: CI
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [main]
66
pull_request:
7-
branches: [ main ]
7+
branches: [main]
88

99
jobs:
1010
build:
11-
runs-on: ubuntu-latest
12-
1311
strategy:
12+
fail-fast: false
1413
matrix:
15-
node-version: [20.x, 21.x, 22.x]
14+
os: [ubuntu-latest, macos-latest, windows-latest]
15+
node-version: [20.x, 22.x, 24.x]
16+
17+
runs-on: ${{ matrix.os }}
1618

1719
steps:
1820
- uses: actions/checkout@v4
19-
21+
2022
- name: Use Node.js ${{ matrix.node-version }}
2123
uses: actions/setup-node@v4
2224
with:
2325
node-version: ${{ matrix.node-version }}
24-
25-
- name: Install OpenSSL
26+
27+
- name: Install OpenSSL (Ubuntu)
28+
if: runner.os == 'Linux'
2629
run: |
2730
sudo apt-get update
2831
sudo apt-get install -y libssl-dev
29-
32+
33+
- name: Install OpenSSL (macOS)
34+
if: runner.os == 'macOS'
35+
run: brew install openssl
36+
37+
- name: Install OpenSSL (Windows)
38+
if: runner.os == 'Windows'
39+
run: choco install openssl -y
40+
shell: cmd
41+
42+
- name: Set OpenSSL env (Windows)
43+
if: runner.os == 'Windows'
44+
run: echo "OPENSSL_ROOT=C:\Program Files\OpenSSL-Win64" >> $env:GITHUB_ENV
45+
shell: pwsh
46+
3047
- name: Install dependencies
3148
run: npm ci
32-
49+
3350
- name: Build
3451
run: npm run build
35-
52+
3653
- name: Test
3754
run: npm test

.github/workflows/prebuild.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Prebuild
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_call:
8+
workflow_dispatch:
9+
10+
jobs:
11+
prebuild:
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
include:
16+
- os: ubuntu-latest
17+
arch: x64
18+
- os: ubuntu-24.04-arm
19+
arch: arm64
20+
- os: macos-latest
21+
arch: x64+arm64
22+
- os: windows-latest
23+
arch: x64
24+
25+
runs-on: ${{ matrix.os }}
26+
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- name: Setup Node.js
31+
uses: actions/setup-node@v4
32+
with:
33+
node-version: "20.x"
34+
35+
- name: Install OpenSSL (Ubuntu)
36+
if: runner.os == 'Linux'
37+
run: |
38+
sudo apt-get update
39+
sudo apt-get install -y libssl-dev
40+
41+
- name: Install OpenSSL (macOS)
42+
if: runner.os == 'macOS'
43+
run: brew install openssl
44+
45+
- name: Install OpenSSL (Windows)
46+
if: runner.os == 'Windows'
47+
run: choco install openssl -y
48+
shell: cmd
49+
50+
- name: Set OpenSSL env (Windows)
51+
if: runner.os == 'Windows'
52+
run: echo "OPENSSL_ROOT=C:\Program Files\OpenSSL-Win64" >> $env:GITHUB_ENV
53+
shell: pwsh
54+
55+
- name: Install dependencies
56+
run: npm ci
57+
58+
- name: Prebuild
59+
run: npx prebuildify --napi --strip
60+
61+
- name: Upload prebuilds
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: prebuilds-${{ matrix.os }}-${{ matrix.arch }}
65+
path: prebuilds/

.github/workflows/publish.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
name: NPM Publish
22

33
on:
4-
# release:
5-
# types: [created]
4+
release:
5+
types: [created]
66
workflow_dispatch:
77

88
jobs:
9+
prebuild:
10+
uses: ./.github/workflows/prebuild.yml
11+
912
publish:
13+
needs: prebuild
1014
runs-on: ubuntu-latest
1115

1216
steps:
@@ -18,11 +22,20 @@ jobs:
1822
node-version: "20.x"
1923
registry-url: "https://registry.npmjs.org"
2024

21-
- name: Install dependencies
25+
- name: Download all prebuilds
26+
uses: actions/download-artifact@v4
27+
with:
28+
pattern: prebuilds-*
29+
path: prebuilds/
30+
merge-multiple: true
31+
32+
- name: Install OpenSSL
2233
run: |
2334
sudo apt-get update
2435
sudo apt-get install -y libssl-dev
25-
npm ci
36+
37+
- name: Install dependencies
38+
run: npm ci
2639

2740
- name: Build
2841
run: npm run build

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules/
22
build/
3+
prebuilds/
34
local.test.js

.npmignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
build/
33
test.js
44
local.test.js
5+
bench.js
56
CHANGELOG.md
7+
.editorconfig
8+
.eslintrc.cjs
9+
.gitignore

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.0] - 2026-03-26
9+
10+
### Added
11+
- `genAsync(input, length?)` — async version using worker threads for non-blocking operation
12+
- Configurable output length: `gen(input, length?)` with length 1-22 (default: 8)
13+
- ESM support via `index.mjs` and `"exports"` field in package.json
14+
- Prebuilt binaries via `prebuildify` — no compiler needed on most platforms
15+
- Input type validation — non-string arguments now throw `TypeError`
16+
- Input size limit (64KB) — oversized inputs throw `RangeError`
17+
- OpenSSL error checking — all EVP API return values are validated
18+
- Multi-platform CI testing (Linux, macOS, Windows)
19+
- CLI `--length` / `-l` option for custom output length
20+
- CLI `--version` / `-v` flag
21+
- `.editorconfig` and ESLint configuration
22+
- Benchmark script (`npm run bench`)
23+
- Collision probability documentation in README
24+
25+
### Changed
26+
- Migrated from `nan` to `node-addon-api` (N-API) for ABI stability across Node.js versions
27+
- `install` script now uses `node-gyp-build` (tries prebuilt first, falls back to compile)
28+
- Entry point changed from `./build/Release/shortener.node` to `./index.js` (JS wrapper)
29+
- Performance: eliminated intermediate hex string conversion in SHA-256 → Base62 pipeline
30+
- Performance: optimized Base62 encoding (reverse-build instead of O(n²) prepend)
31+
- CI now tests Node.js 20, 22, 24 across Linux, macOS, and Windows
32+
- Publish workflow now builds prebuilt binaries before publishing
33+
- Updated Windows OpenSSL library reference
34+
35+
### Removed
36+
- Removed `nan` dependency (replaced by `node-addon-api`)
37+
838
## [1.1.0] - 2026-01-27
939

1040
### Added

README.md

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,64 @@
22

33
# theShortener
44

5-
A fast string shortener that generates 8-character codes using SHA-256 and Base62 encoding, implemented as a native Node.js addon in C++.
5+
A fast string shortener that generates short codes using SHA-256 and Base62 encoding, implemented as a native Node.js addon in C++.
66

77
## Features
8-
- Fast native C++ implementation
9-
- Consistent 8-character outputs
8+
- Fast native C++ implementation (N-API)
9+
- Configurable output length (1-22 characters, default 8)
10+
- Async support (`genAsync`) for non-blocking operation
11+
- Prebuilt binaries for major platforms (no compiler needed)
1012
- Cryptographically secure using SHA-256
11-
- Base62 encoding (0-9, a-z, A-Z)
12-
- Deterministic results - same input produces same output
13+
- Base62 encoding (0-9, A-Z, a-z)
14+
- Deterministic results - same input always produces same output
15+
- ESM and CommonJS support
16+
- CLI included
1317

1418
## Installation
1519
```bash
16-
npm install theshortener --save
20+
npm install theshortener
1721
```
1822

23+
Prebuilt binaries are included for Linux, macOS, and Windows. Falls back to compiling from source if no prebuild is available (requires C++ compiler + OpenSSL dev libraries).
24+
1925
## Usage
26+
27+
### CommonJS
2028
```javascript
2129
const shortener = require('theshortener');
2230

23-
// Generate short code
24-
const shortCode = shortener.gen('https://google.com');
25-
console.log(shortCode); // Outputs: "Qhq1TQ2n"
31+
const code = shortener.gen('https://google.com');
32+
console.log(code); // "Qhq1TQ2n"
33+
34+
// Custom length (1-22)
35+
const short = shortener.gen('https://google.com', 6);
36+
console.log(short); // "Qhq1TQ"
37+
```
38+
39+
### ESM
40+
```javascript
41+
import { gen, genAsync } from 'theshortener';
42+
43+
const code = gen('https://google.com');
44+
```
45+
46+
### Async
47+
```javascript
48+
const { genAsync } = require('theshortener');
49+
50+
const code = await genAsync('https://google.com');
51+
console.log(code); // "Qhq1TQ2n"
52+
53+
// Custom length
54+
const long = await genAsync('https://google.com', 16);
55+
```
56+
57+
### TypeScript
58+
```typescript
59+
import { gen, genAsync } from 'theshortener';
60+
61+
const code: string = gen('https://google.com');
62+
const asyncCode: string = await genAsync('https://google.com', 12);
2663
```
2764

2865
## CLI Usage
@@ -33,13 +70,33 @@ npm i -g theshortener
3370
Use from command line:
3471
```bash
3572
theshortener https://google.com
36-
Qhq1TQ2n
73+
# Qhq1TQ2n
74+
75+
theshortener --length 12 https://google.com
76+
77+
theshortener --version
78+
theshortener --help
3779
```
3880

81+
## API
82+
83+
### `gen(input: string, length?: number): string`
84+
85+
Generate a short code synchronously.
86+
87+
- **input** — String to shorten (max 64KB)
88+
- **length** — Output length, 1-22 (default: 8)
89+
- **returns** — Alphanumeric string of the specified length
90+
- **throws**`TypeError` if input is missing, not a string, or empty; `RangeError` if input exceeds 64KB or length is out of range
91+
92+
### `genAsync(input: string, length?: number): Promise<string>`
93+
94+
Same as `gen()` but runs SHA-256 + Base62 encoding in a worker thread.
95+
3996
## Development
4097
#### Prerequisites:
4198

42-
- Node.js
99+
- Node.js >= 20
43100
- C++ compiler
44101
- OpenSSL development libraries
45102

@@ -53,18 +110,21 @@ npm run build
53110
npm test
54111
```
55112

56-
## TypeScript
57-
```typescript
58-
import shortener from 'theshortener';
59-
60-
const code: string = shortener.gen('https://google.com');
113+
#### Run benchmarks:
114+
```bash
115+
npm run bench
61116
```
62117

118+
## Collision Probability
119+
120+
The default 8-character output uses 64 bits of the SHA-256 hash, providing approximately 47.6 bits of entropy in Base62. By the birthday paradox, you can expect a ~50% collision probability after roughly 2³² (~4.3 billion) unique inputs. For fewer collisions, use a longer output length.
121+
63122
## Notes
64123

65-
- The output is always 8 characters long
124+
- Uses N-API (Node-API) for ABI stability across Node.js versions
66125
- Uses OpenSSL for SHA-256 hashing
67-
- This is a one-way hashing implementation. Reverse lookups (getting original STR from short code) require storing STR-to-hash mappings in a database.
126+
- This is a one-way hashing implementation. Reverse lookups (getting original string from short code) require storing input-to-code mappings in a database
127+
- Shorter lengths are prefixes of longer outputs (same input)
68128

69129
## License
70130
MIT

0 commit comments

Comments
 (0)