nik-id 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sumitro Aji Prabowo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,492 @@
1
+ # nik-id
2
+
3
+ [![npm version](https://img.shields.io/npm/v/nik-id.svg)](https://www.npmjs.com/package/nik-id)
4
+ [![CI](https://github.com/sumitroajiprabowo/nik-id/actions/workflows/ci.yml/badge.svg)](https://github.com/sumitroajiprabowo/nik-id/actions/workflows/ci.yml)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-green.svg)](https://nodejs.org/)
8
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](https://www.npmjs.com/package/nik-id)
9
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://www.npmjs.com/package/nik-id)
10
+
11
+ Parser, validator, dan generator **NIK** (Nomor Induk Kependudukan) Indonesia.
12
+
13
+ TypeScript-first, zero dependencies, 100% test coverage, dual ESM+CJS, tree-shakeable.
14
+
15
+ ```bash
16
+ npm install nik-id
17
+ ```
18
+
19
+ Tidak perlu database, API call, atau file eksternal. Cukup install dan langsung pakai.
20
+
21
+ ## Fitur
22
+
23
+ - **Validasi format** -- cek apakah NIK valid (panjang, digit, provinsi, tanggal lahir, nomor urut)
24
+ - **Parse komponen** -- ekstrak kode wilayah, tanggal lahir, gender, dan nomor urut dari NIK
25
+ - **Generate NIK** -- buat NIK yang valid secara format untuk testing, seeding, atau demo
26
+ - **Discriminated union** -- return type aman di-narrow pakai `result.valid` (TypeScript-friendly)
27
+ - **Error bahasa Indonesia** -- semua pesan error dalam bahasa Indonesia
28
+ - **Tree-shakeable** -- import hanya fungsi yang dibutuhkan via sub-path
29
+ - **TypeScript-first** -- strict types, auto-complete di IDE
30
+ - **Dual ESM + CJS** -- support semua environment (browser, Node.js, Bun, Deno)
31
+ - **Zero dependencies** -- tidak ada dependency runtime sama sekali
32
+ - **100% coverage** -- statements, branches, functions, lines
33
+
34
+ ## Struktur NIK
35
+
36
+ NIK (Nomor Induk Kependudukan) adalah nomor identitas penduduk Indonesia yang terdiri dari **16 digit angka**:
37
+
38
+ ```
39
+ 3204 07 650885 0001
40
+ │ │ │ └── Nomor urut registrasi (4 digit, 0001-9999)
41
+ │ │ └── Tanggal lahir DDMMYY — perempuan: DD + 40 (jadi 41-71)
42
+ │ └── Kode kecamatan (2 digit, format Kemendagri)
43
+ └── Kode kabupaten/kota (4 digit, format Kemendagri)
44
+ ```
45
+
46
+ | Posisi | Digit | Keterangan | Contoh |
47
+ |--------|-------|------------|--------|
48
+ | 1-2 | 2 digit | Kode provinsi Kemendagri (11-97) | `32` = Jawa Barat |
49
+ | 3-4 | 2 digit | Kode kabupaten/kota | `04` = Kab. Bandung |
50
+ | 5-6 | 2 digit | Kode kecamatan | `07` = Nagreg |
51
+ | 7-8 | 2 digit | Tanggal lahir (DD), perempuan +40 | `65` = tanggal 25 (perempuan) |
52
+ | 9-10 | 2 digit | Bulan lahir (MM) | `08` = Agustus |
53
+ | 11-12 | 2 digit | Tahun lahir (YY) | `85` = 1985 |
54
+ | 13-16 | 4 digit | Nomor urut registrasi | `0001` |
55
+
56
+ **Catatan penting:**
57
+ - NIK menggunakan kode **Kemendagri** (Kementerian Dalam Negeri), bukan BPS
58
+ - Perempuan: tanggal lahir ditambah 40 (DD 01-31 → DD 41-71)
59
+ - Tahun 2 digit: disambiguasi berdasarkan tahun sekarang (YY > sekarang → 1900+YY, else 2000+YY)
60
+
61
+ ## Quick Start
62
+
63
+ ```typescript
64
+ import { validateNIK, parseNIK, generateNIK } from 'nik-id';
65
+
66
+ // Validasi — cek apakah NIK valid
67
+ const valid = validateNIK("3204076508850001");
68
+ console.log(valid); // { valid: true }
69
+
70
+ // Parse — ekstrak semua komponen
71
+ const parsed = parseNIK("3204076508850001");
72
+ if (parsed.valid) {
73
+ console.log(parsed.provinceCode); // "32"
74
+ console.log(parsed.regencyCode); // "3204"
75
+ console.log(parsed.districtCode); // "320407"
76
+ console.log(parsed.gender); // "F"
77
+ console.log(parsed.birthDate); // Date: 1985-08-25
78
+ console.log(parsed.sequenceNumber); // "0001"
79
+ }
80
+
81
+ // Generate — buat NIK untuk testing
82
+ const nik = generateNIK({ gender: "F", birthDate: new Date("1985-08-25") });
83
+ console.log(nik); // "xxxxxx6508850001" (wilayah random)
84
+ ```
85
+
86
+ ## API Reference
87
+
88
+ ### `validateNIK(nik: string): ValidationResult`
89
+
90
+ Validasi format NIK secara bertahap (fail-fast). Return discriminated union:
91
+
92
+ ```typescript
93
+ import { validateNIK } from 'nik-id';
94
+ // atau: import { validateNIK } from 'nik-id/validate';
95
+
96
+ // NIK valid
97
+ validateNIK("3204076508850001");
98
+ // → { valid: true }
99
+
100
+ // Panjang salah
101
+ validateNIK("123");
102
+ // → { valid: false, error: "NIK harus 16 digit" }
103
+
104
+ // Mengandung huruf
105
+ validateNIK("320407650885000A");
106
+ // → { valid: false, error: "NIK hanya boleh berisi angka" }
107
+
108
+ // Kode provinsi di luar range
109
+ validateNIK("0004076508850001");
110
+ // → { valid: false, error: "Kode provinsi tidak valid" }
111
+
112
+ // Tanggal tidak ada di kalender (31 Februari)
113
+ validateNIK("3204073102850001");
114
+ // → { valid: false, error: "Tanggal lahir tidak valid" }
115
+
116
+ // Nomor urut 0000
117
+ validateNIK("3204076508850000");
118
+ // → { valid: false, error: "Nomor urut tidak valid" }
119
+ ```
120
+
121
+ ### `parseNIK(nik: string): NIKResult`
122
+
123
+ Parse NIK menjadi komponen-komponennya. Memvalidasi terlebih dahulu — kalau tidak valid, return error.
124
+
125
+ ```typescript
126
+ import { parseNIK } from 'nik-id';
127
+ // atau: import { parseNIK } from 'nik-id/parse';
128
+
129
+ const result = parseNIK("3204076508850001");
130
+ if (result.valid) {
131
+ console.log(result.nik); // "3204076508850001"
132
+ console.log(result.provinceCode); // "32" — Jawa Barat
133
+ console.log(result.regencyCode); // "3204" — Kab. Bandung
134
+ console.log(result.districtCode); // "320407" — Nagreg
135
+ console.log(result.gender); // "F" — Perempuan
136
+ console.log(result.birthDate); // Date: 1985-08-25
137
+ console.log(result.sequenceNumber); // "0001"
138
+ } else {
139
+ console.log(result.error);
140
+ }
141
+ ```
142
+
143
+ **Komponen yang diekstrak:**
144
+
145
+ | Field | Tipe | Keterangan | Contoh |
146
+ |-------|------|------------|--------|
147
+ | `nik` | `string` | NIK asli yang di-parse | `"3204076508850001"` |
148
+ | `provinceCode` | `string` | Kode provinsi Kemendagri (2 digit) | `"32"` |
149
+ | `regencyCode` | `string` | Kode kabupaten/kota Kemendagri (4 digit) | `"3204"` |
150
+ | `districtCode` | `string` | Kode kecamatan Kemendagri (6 digit) | `"320407"` |
151
+ | `birthDate` | `Date` | Tanggal lahir (object Date) | `Date(1985-08-25)` |
152
+ | `gender` | `"M" \| "F"` | Jenis kelamin | `"F"` |
153
+ | `sequenceNumber` | `string` | Nomor urut registrasi (4 digit) | `"0001"` |
154
+
155
+ ### `generateNIK(options?: GenerateOptions): string`
156
+
157
+ Generate NIK yang valid secara format. Semua parameter opsional — yang tidak diisi akan di-random.
158
+
159
+ ```typescript
160
+ import { generateNIK } from 'nik-id';
161
+ // atau: import { generateNIK } from 'nik-id/generate';
162
+
163
+ // Full random
164
+ generateNIK();
165
+
166
+ // Gender spesifik
167
+ generateNIK({ gender: "F" });
168
+ generateNIK({ gender: "M" });
169
+
170
+ // Tanggal lahir spesifik
171
+ generateNIK({ birthDate: new Date("1985-08-25") });
172
+
173
+ // Gender + tanggal lahir
174
+ generateNIK({ gender: "F", birthDate: new Date("1985-08-25") });
175
+
176
+ // Wilayah spesifik (harus konsisten dari provinsi ke kecamatan)
177
+ generateNIK({
178
+ provinceCode: "32",
179
+ regencyCode: "3204",
180
+ districtCode: "320407",
181
+ });
182
+
183
+ // Semua opsi sekaligus
184
+ generateNIK({
185
+ provinceCode: "32",
186
+ regencyCode: "3204",
187
+ districtCode: "320407",
188
+ gender: "F",
189
+ birthDate: new Date("1985-08-25"),
190
+ });
191
+ ```
192
+
193
+ **Opsi yang tersedia:**
194
+
195
+ | Opsi | Tipe | Keterangan |
196
+ |------|------|------------|
197
+ | `provinceCode` | `string?` | Kode provinsi Kemendagri (2 digit, 11-97) |
198
+ | `regencyCode` | `string?` | Kode kabupaten/kota (4 digit, harus match provinsi) |
199
+ | `districtCode` | `string?` | Kode kecamatan (6 digit, harus match kabupaten) |
200
+ | `gender` | `"M" \| "F"?` | Jenis kelamin |
201
+ | `birthDate` | `Date?` | Tanggal lahir (kalau tidak diisi, random 1950-2005) |
202
+
203
+ **Error yang di-throw:**
204
+
205
+ ```typescript
206
+ // Kode provinsi di luar range
207
+ generateNIK({ provinceCode: "99" });
208
+ // → Error: "Kode provinsi tidak valid (harus 11-97)"
209
+
210
+ // Kode kabupaten tidak cocok dengan provinsi
211
+ generateNIK({ provinceCode: "32", regencyCode: "3301" });
212
+ // → Error: 'Kode kabupaten/kota "3301" tidak sesuai dengan kode provinsi "32"'
213
+
214
+ // Kode kecamatan tidak cocok dengan kabupaten
215
+ generateNIK({ provinceCode: "32", regencyCode: "3204", districtCode: "320501" });
216
+ // → Error: 'Kode kecamatan "320501" tidak sesuai dengan kode kabupaten/kota "3204"'
217
+ ```
218
+
219
+ > **Catatan:** NIK yang dihasilkan valid secara **format** tapi **bukan** NIK asli milik orang sungguhan. Cocok untuk testing, seeding database, atau demo.
220
+
221
+ ## Validasi yang Dilakukan
222
+
223
+ `validateNIK` dan `parseNIK` melakukan pengecekan bertahap (fail-fast — berhenti di error pertama):
224
+
225
+ | # | Pengecekan | Detail |
226
+ |---|-----------|--------|
227
+ | 1 | **Tipe data** | Input harus bertipe `string` |
228
+ | 2 | **Panjang** | Harus tepat 16 karakter |
229
+ | 3 | **Format** | Harus semua digit angka (0-9) |
230
+ | 4 | **Kode provinsi** | 2 digit pertama harus dalam range 11-97 |
231
+ | 5 | **Tanggal lahir** | DD (01-31 atau 41-71), MM (01-12), valid di kalender |
232
+ | 6 | **Nomor urut** | Digit 13-16 tidak boleh `0000` |
233
+
234
+ > **Catatan:** Validasi bersifat **format-only**. Package ini tidak mengecek apakah kode wilayah benar-benar terdaftar di database Kemendagri. Untuk lookup nama wilayah, gunakan package [`kode-wilayah-id`](https://www.npmjs.com/package/kode-wilayah-id).
235
+
236
+ ### Error Messages
237
+
238
+ Semua pesan error dalam Bahasa Indonesia:
239
+
240
+ | Error | Penyebab | Contoh Input |
241
+ |-------|----------|-------------|
242
+ | `NIK harus berupa string` | Input bukan bertipe string | `12345`, `null`, `undefined` |
243
+ | `NIK harus 16 digit` | Panjang bukan 16 karakter | `"123"`, `""` |
244
+ | `NIK hanya boleh berisi angka` | Mengandung karakter non-digit | `"320407650885000A"` |
245
+ | `Kode provinsi tidak valid` | 2 digit pertama di luar range 11-97 | `"0004076508850001"` |
246
+ | `Tanggal lahir tidak valid` | Tanggal/bulan invalid atau tidak ada di kalender | `"3204073102850001"` |
247
+ | `Nomor urut tidak valid` | 4 digit terakhir adalah `0000` | `"3204076508850000"` |
248
+
249
+ ## Tree-shaking / Sub-path Imports
250
+
251
+ Import hanya fungsi yang dibutuhkan — bundler hanya include kode yang di-import:
252
+
253
+ ```typescript
254
+ // Hanya validasi
255
+ import { validateNIK } from 'nik-id/validate';
256
+
257
+ // Hanya parsing
258
+ import { parseNIK } from 'nik-id/parse';
259
+
260
+ // Hanya generator
261
+ import { generateNIK } from 'nik-id/generate';
262
+
263
+ // Types only (zero runtime)
264
+ import type { NIKResult, ValidationResult, GenerateOptions } from 'nik-id/types';
265
+
266
+ // Atau import semua sekaligus
267
+ import { validateNIK, parseNIK, generateNIK } from 'nik-id';
268
+ ```
269
+
270
+ ### Bundle Size
271
+
272
+ Package ini sangat ringan karena zero dependencies dan hanya berisi logika validasi:
273
+
274
+ | Import | Ukuran (minified) |
275
+ |--------|-------------------|
276
+ | `nik-id/validate` | ~1.8 KB |
277
+ | `nik-id/parse` | ~2.8 KB |
278
+ | `nik-id/generate` | ~2.3 KB |
279
+ | `nik-id` (full) | ~5.2 KB |
280
+
281
+ ## Integrasi dengan kode-wilayah-id
282
+
283
+ Package ini **tidak** menyertakan data wilayah — hanya mengembalikan kode Kemendagri. Untuk resolve ke nama wilayah, gunakan package [`kode-wilayah-id`](https://www.npmjs.com/package/kode-wilayah-id):
284
+
285
+ ```bash
286
+ npm install kode-wilayah-id
287
+ ```
288
+
289
+ ```typescript
290
+ import { parseNIK } from 'nik-id';
291
+ import {
292
+ getProvinceByKemendagriCode,
293
+ getRegencyByKemendagriCode,
294
+ getDistrictByKemendagriCode,
295
+ } from 'kode-wilayah-id';
296
+
297
+ const result = parseNIK("3204076508850001");
298
+ if (result.valid) {
299
+ const province = getProvinceByKemendagriCode(result.provinceCode);
300
+ const regency = getRegencyByKemendagriCode(result.regencyCode);
301
+ const district = getDistrictByKemendagriCode(result.districtCode);
302
+
303
+ console.log(province?.name); // "JAWA BARAT"
304
+ console.log(regency?.name); // "KAB. BANDUNG"
305
+ console.log(district?.name); // "NAGREG"
306
+ }
307
+ ```
308
+
309
+ ## Discriminated Union Pattern
310
+
311
+ Return type `parseNIK` dan `validateNIK` menggunakan discriminated union — TypeScript bisa narrow type otomatis berdasarkan `result.valid`:
312
+
313
+ ```typescript
314
+ import { parseNIK } from 'nik-id';
315
+ import type { NIKResult, NIKValid, NIKInvalid } from 'nik-id/types';
316
+
317
+ const result: NIKResult = parseNIK(input);
318
+
319
+ if (result.valid) {
320
+ // TypeScript tahu ini NIKValid — semua field tersedia
321
+ console.log(result.provinceCode); // ✅ string
322
+ console.log(result.gender); // ✅ "M" | "F"
323
+ console.log(result.birthDate); // ✅ Date
324
+ } else {
325
+ // TypeScript tahu ini NIKInvalid — hanya error
326
+ console.log(result.error); // ✅ string
327
+ // console.log(result.gender); // ❌ compile error!
328
+ }
329
+ ```
330
+
331
+ Pattern yang sama berlaku untuk `validateNIK`:
332
+
333
+ ```typescript
334
+ import { validateNIK } from 'nik-id';
335
+
336
+ const result = validateNIK(input);
337
+ if (result.valid) {
338
+ // ValidationValid — tidak ada field lain
339
+ console.log("NIK valid!");
340
+ } else {
341
+ // ValidationInvalid — ada field error
342
+ console.log(result.error);
343
+ }
344
+ ```
345
+
346
+ ## Contoh Penggunaan
347
+
348
+ Lihat folder [`examples/`](examples/) untuk contoh lengkap di berbagai framework dan runtime:
349
+
350
+ | Framework | File | Fitur |
351
+ |-----------|------|-------|
352
+ | **Node.js** | [`node.ts`](examples/node.ts) | Basic usage — validasi, parse, generate |
353
+ | **React** | [`react.tsx`](examples/react.tsx) | Form validasi KTP dengan feedback real-time |
354
+ | **Next.js** | [`nextjs.tsx`](examples/nextjs.tsx) | API route + server component |
355
+ | **Express** | [`express.ts`](examples/express.ts) | REST API endpoint validasi NIK |
356
+ | **Hono** | [`hono.ts`](examples/hono.ts) | Lightweight REST API validasi NIK |
357
+ | **Bun** | [`bun.ts`](examples/bun.ts) | Native Bun HTTP server |
358
+ | **Deno** | [`deno.ts`](examples/deno.ts) | Native Deno server |
359
+
360
+ ```bash
361
+ # Jalankan contoh Node.js
362
+ npx tsx examples/node.ts
363
+ ```
364
+
365
+ ## Types
366
+
367
+ Semua type tersedia di `nik-id/types`:
368
+
369
+ ```typescript
370
+ import type {
371
+ NIKValid,
372
+ NIKInvalid,
373
+ NIKResult,
374
+ ValidationValid,
375
+ ValidationInvalid,
376
+ ValidationResult,
377
+ GenerateOptions,
378
+ } from 'nik-id/types';
379
+ ```
380
+
381
+ ### NIKResult (parseNIK)
382
+
383
+ ```typescript
384
+ // Hasil valid
385
+ interface NIKValid {
386
+ valid: true;
387
+ nik: string; // "3204076508850001"
388
+ provinceCode: string; // "32"
389
+ regencyCode: string; // "3204"
390
+ districtCode: string; // "320407"
391
+ birthDate: Date; // Date(1985-08-25)
392
+ gender: "M" | "F"; // "F"
393
+ sequenceNumber: string; // "0001"
394
+ }
395
+
396
+ // Hasil invalid
397
+ interface NIKInvalid {
398
+ valid: false;
399
+ error: string; // "NIK harus 16 digit"
400
+ }
401
+
402
+ type NIKResult = NIKValid | NIKInvalid;
403
+ ```
404
+
405
+ ### ValidationResult (validateNIK)
406
+
407
+ ```typescript
408
+ interface ValidationValid {
409
+ valid: true;
410
+ }
411
+
412
+ interface ValidationInvalid {
413
+ valid: false;
414
+ error: string;
415
+ }
416
+
417
+ type ValidationResult = ValidationValid | ValidationInvalid;
418
+ ```
419
+
420
+ ### GenerateOptions (generateNIK)
421
+
422
+ ```typescript
423
+ interface GenerateOptions {
424
+ provinceCode?: string; // "32" (2 digit, 11-97)
425
+ regencyCode?: string; // "3204" (4 digit, harus match provinsi)
426
+ districtCode?: string; // "320407" (6 digit, harus match kabupaten)
427
+ gender?: "M" | "F"; // Jenis kelamin
428
+ birthDate?: Date; // Tanggal lahir (default: random 1950-2005)
429
+ }
430
+ ```
431
+
432
+ ## Encoding Tanggal Lahir
433
+
434
+ NIK meng-encode tanggal lahir dalam 6 digit (posisi 7-12) dengan format DDMMYY:
435
+
436
+ ### Gender dan DD
437
+
438
+ | Gender | Range DD | Contoh | Arti |
439
+ |--------|----------|--------|------|
440
+ | Laki-laki | 01-31 | DD=`15` | Lahir tanggal 15 |
441
+ | Perempuan | 41-71 | DD=`65` | Lahir tanggal 25 (65-40=25) |
442
+ | Invalid | 00, 32-40, 72-99 | -- | Tidak valid |
443
+
444
+ ### Disambiguasi Tahun (YY)
445
+
446
+ Karena NIK hanya menyimpan 2 digit tahun, perlu disambiguasi:
447
+
448
+ | YY | Tahun sekarang (2 digit) | Hasil | Logika |
449
+ |----|--------------------------|-------|--------|
450
+ | 85 | 26 | 1985 | 85 > 26 → 1900 + 85 |
451
+ | 02 | 26 | 2002 | 02 ≤ 26 → 2000 + 02 |
452
+ | 26 | 26 | 2026 | 26 ≤ 26 → 2000 + 26 |
453
+ | 27 | 26 | 1927 | 27 > 26 → 1900 + 27 |
454
+ | 00 | 26 | 2000 | 00 ≤ 26 → 2000 + 00 |
455
+ | 99 | 26 | 1999 | 99 > 26 → 1900 + 99 |
456
+
457
+ ## Kode Wilayah Kemendagri
458
+
459
+ NIK menggunakan kode wilayah format Kemendagri (bukan BPS):
460
+
461
+ | Level | Panjang | Contoh | Keterangan |
462
+ |-------|---------|--------|------------|
463
+ | Provinsi | 2 digit | `"32"` | Jawa Barat |
464
+ | Kabupaten/Kota | 4 digit | `"3204"` | Kab. Bandung |
465
+ | Kecamatan | 6 digit | `"320407"` | Nagreg |
466
+
467
+ Range kode provinsi yang valid: **11** (Aceh) sampai **97** (Papua Barat Daya).
468
+
469
+ > Package ini hanya memvalidasi **range** kode provinsi (11-97), tidak mengecek apakah kode spesifik benar-benar ada. Untuk lookup nama wilayah, gunakan [`kode-wilayah-id`](https://www.npmjs.com/package/kode-wilayah-id).
470
+
471
+ ## Contributing
472
+
473
+ Kontribusi sangat diterima!
474
+
475
+ ```bash
476
+ git clone https://github.com/sumitroajiprabowo/nik-id.git
477
+ cd nik-id
478
+ npm install
479
+ npm run lint # Lint check (Biome)
480
+ npm run format:check # Format check
481
+ npm run typecheck # TypeScript check
482
+ npm run test:coverage # Test dengan coverage (harus 100%)
483
+ npm run build # Build ESM + CJS
484
+ ```
485
+
486
+ ## Changelog
487
+
488
+ Lihat [CHANGELOG.md](CHANGELOG.md) untuk riwayat perubahan.
489
+
490
+ ## Lisensi
491
+
492
+ [MIT](LICENSE) (c) [Sumitro Aji Prabowo](https://github.com/sumitroajiprabowo)
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/generate.ts
21
+ var generate_exports = {};
22
+ __export(generate_exports, {
23
+ generateNIK: () => generateNIK
24
+ });
25
+ module.exports = __toCommonJS(generate_exports);
26
+ var MIN_PROVINCE_CODE = 11;
27
+ var MAX_PROVINCE_CODE = 97;
28
+ var MIN_BIRTH_YEAR = 1950;
29
+ var MAX_BIRTH_YEAR = 2005;
30
+ function randomInt(min, max) {
31
+ return Math.floor(Math.random() * (max - min + 1)) + min;
32
+ }
33
+ function padZero(num, length) {
34
+ return String(num).padStart(length, "0");
35
+ }
36
+ function getDaysInMonth(year, month) {
37
+ return new Date(year, month, 0).getDate();
38
+ }
39
+ function randomBirthDate() {
40
+ const year = randomInt(MIN_BIRTH_YEAR, MAX_BIRTH_YEAR);
41
+ const month = randomInt(1, 12);
42
+ const maxDay = getDaysInMonth(year, month);
43
+ const day = randomInt(1, maxDay);
44
+ return new Date(year, month - 1, day);
45
+ }
46
+ function generateNIK(options = {}) {
47
+ const { gender, birthDate } = options;
48
+ let provinceCode;
49
+ if (options.provinceCode !== void 0) {
50
+ const pc = Number.parseInt(options.provinceCode, 10);
51
+ if (Number.isNaN(pc) || pc < MIN_PROVINCE_CODE || pc > MAX_PROVINCE_CODE) {
52
+ throw new Error("Kode provinsi tidak valid (harus 11-97)");
53
+ }
54
+ provinceCode = padZero(pc, 2);
55
+ } else {
56
+ provinceCode = padZero(randomInt(MIN_PROVINCE_CODE, MAX_PROVINCE_CODE), 2);
57
+ }
58
+ let regencyCode;
59
+ if (options.regencyCode !== void 0) {
60
+ if (!options.regencyCode.startsWith(provinceCode)) {
61
+ throw new Error(
62
+ `Kode kabupaten/kota "${options.regencyCode}" tidak sesuai dengan kode provinsi "${provinceCode}"`
63
+ );
64
+ }
65
+ regencyCode = options.regencyCode;
66
+ } else {
67
+ regencyCode = provinceCode + padZero(randomInt(1, 99), 2);
68
+ }
69
+ let districtCode;
70
+ if (options.districtCode !== void 0) {
71
+ if (!options.districtCode.startsWith(regencyCode)) {
72
+ throw new Error(
73
+ `Kode kecamatan "${options.districtCode}" tidak sesuai dengan kode kabupaten/kota "${regencyCode}"`
74
+ );
75
+ }
76
+ districtCode = options.districtCode;
77
+ } else {
78
+ districtCode = regencyCode + padZero(randomInt(1, 99), 2);
79
+ }
80
+ const birth = birthDate ?? randomBirthDate();
81
+ const day = birth.getDate();
82
+ const month = birth.getMonth() + 1;
83
+ const year = birth.getFullYear();
84
+ const selectedGender = gender ?? (Math.random() < 0.5 ? "M" : "F");
85
+ const encodedDay = selectedGender === "F" ? day + 40 : day;
86
+ const ddStr = padZero(encodedDay, 2);
87
+ const mmStr = padZero(month, 2);
88
+ const yyStr = padZero(year % 100, 2);
89
+ const seq = padZero(randomInt(1, 9999), 4);
90
+ return districtCode + ddStr + mmStr + yyStr + seq;
91
+ }
92
+ // Annotate the CommonJS export names for ESM import in node:
93
+ 0 && (module.exports = {
94
+ generateNIK
95
+ });
@@ -0,0 +1,83 @@
1
+ import { GenerateOptions } from './types.cjs';
2
+
3
+ /**
4
+ * Modul generator NIK (Nomor Induk Kependudukan).
5
+ *
6
+ * Menyediakan fungsi {@link generateNIK} untuk membuat NIK yang valid
7
+ * secara format, baik full random maupun dengan parameter tertentu
8
+ * (wilayah, gender, tanggal lahir).
9
+ *
10
+ * NIK yang dihasilkan valid secara **format** — semua aturan digit terpenuhi —
11
+ * tapi **bukan** NIK asli milik orang sungguhan. Cocok untuk testing,
12
+ * seeding database, atau demo.
13
+ *
14
+ * @module generate
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { generateNIK } from 'nik-id/generate';
19
+ *
20
+ * // Full random
21
+ * const nik1 = generateNIK();
22
+ *
23
+ * // Perempuan lahir 25 Agustus 1985
24
+ * const nik2 = generateNIK({ gender: "F", birthDate: new Date("1985-08-25") });
25
+ *
26
+ * // Wilayah spesifik
27
+ * const nik3 = generateNIK({
28
+ * provinceCode: "32",
29
+ * regencyCode: "3204",
30
+ * districtCode: "320407",
31
+ * });
32
+ * ```
33
+ */
34
+
35
+ /**
36
+ * Generate NIK (Nomor Induk Kependudukan) yang valid secara format.
37
+ *
38
+ * Semua parameter opsional — yang tidak diisi akan di-random.
39
+ * NIK yang dihasilkan memenuhi semua aturan format:
40
+ * - Kode provinsi dalam range 11-97
41
+ * - Tanggal lahir valid secara kalender
42
+ * - Gender tercermin di encoding DD (perempuan: DD + 40)
43
+ * - Sequence number dalam range 0001-9999
44
+ *
45
+ * **Catatan:** NIK ini bukan data asli — hanya valid secara format,
46
+ * cocok untuk testing dan demo.
47
+ *
48
+ * @param options - Opsi untuk men-generate NIK (semua opsional)
49
+ * @returns String NIK 16 digit yang valid secara format
50
+ * @throws {Error} Jika `provinceCode` di luar range 11-97
51
+ * @throws {Error} Jika `regencyCode` tidak diawali `provinceCode`
52
+ * @throws {Error} Jika `districtCode` tidak diawali `regencyCode`
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // Full random
57
+ * generateNIK();
58
+ * // "3204071508900123"
59
+ *
60
+ * // Gender spesifik
61
+ * generateNIK({ gender: "F" });
62
+ * // "xxxxxx5508900456" (DD + 40 untuk perempuan)
63
+ *
64
+ * // Tanggal lahir spesifik
65
+ * generateNIK({ birthDate: new Date("1985-08-25") });
66
+ * // "xxxxxx2508850789"
67
+ *
68
+ * // Gender dan tanggal lahir
69
+ * generateNIK({ gender: "F", birthDate: new Date("1985-08-25") });
70
+ * // "xxxxxx6508850234" (25 + 40 = 65 untuk perempuan)
71
+ *
72
+ * // Wilayah lengkap
73
+ * generateNIK({
74
+ * provinceCode: "32",
75
+ * regencyCode: "3204",
76
+ * districtCode: "320407",
77
+ * });
78
+ * // "3204071508900001"
79
+ * ```
80
+ */
81
+ declare function generateNIK(options?: GenerateOptions): string;
82
+
83
+ export { generateNIK };