feat!: TypeScript rewrite, vCard spec compliance, E.164 phone normalization#2
feat!: TypeScript rewrite, vCard spec compliance, E.164 phone normalization#2Balanced02 wants to merge 4 commits into
Conversation
…ual CJS/ESM distribution, and browser support
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe package is rewritten in TypeScript with vCard string and file extraction, configurable parsing and phone normalization, browser and Node entrypoints, CJS/ESM build outputs, expanded tests, and revised documentation. ChangesvCard extraction rewrite
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant extractTel
participant FileSystem
participant parseVcard
Caller->>extractTel: provide vCard text or file path
extractTel->>parseVcard: parse raw vCard text
extractTel->>FileSystem: read file path as UTF-8
FileSystem-->>extractTel: return vCard text
extractTel->>parseVcard: parse file contents
parseVcard-->>Caller: return contacts or phone numbers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Balanced02
left a comment
There was a problem hiding this comment.
Review: v2.0.0 TypeScript rewrite
Solid direction — the RFC line-unfolding, colon-in-value handling, multi-value arrays, and E.164 normalization are real improvements over the v1 whole-file regex, and the test suite (16 passing, verified locally) is a good foundation. Being strict as requested, though, I would not merge/publish this as-is: I built the package and reproduced two blocking problems.
🔴 Blocking
- Published package contains no build output.
distis gitignored with nofilesallowlist →npm pack --dry-runships onlysrc/*.ts. All entry points targetdist/, sonpm install+require/importfails. (see package.json) - Documented ESM
import { parseVcard }throws aSyntaxErrorat load.export =+ namespace exposesparseVcardonly as a static property / CJS prop, not an ESM named export. The README's primary import and Example 3 don't run. (see src/index.ts, README)
🟡 Should fix
- Dead
/^3$|^0$/filter carried over from v1 can silently drop valid numbers. (src/parser.ts:172) - Test coverage gaps on the headline features — dual-package, params/groups, unescaping, browser guard. Tests exercise source, not
dist. (src/index.test.ts)
🟢 Nits
- No-op try/catch; inaccurate
Contacttypes undermultiValueMode:'array'; parameters silently dropped despite "extracted" claim; staleprefixcomment; wrongWouter Vroegecopyright; tracked.DS_Store; unquoted mocha globsrc/**/*.test.ts(relies on shell globbing — quote it so mocha expands it).
Verification: npm ci && npm run build && npm test (16 pass) + npm pack --dry-run + runtime ESM/CJS import checks against the built dist/.
| "main": "index.js", | ||
| "version": "2.0.0", | ||
| "description": "A robust, spec-compliant module to extract contacts and telephone numbers from vCard (.vcf) files, supporting streams, string inputs, and phone number normalization.", | ||
| "main": "./dist/index.js", |
There was a problem hiding this comment.
🔴 Critical — the published package ships no build output. dist is in .gitignore, and there is no files allowlist or .npmignore, so npm excludes the entire build. I verified with npm pack --dry-run: the tarball contains only README.md, package.json, tsconfig.json, tsup.config.ts, and src/*.ts — no dist/ at all.
Yet every entry point here (main, module, types, exports) points into dist/, so after npm install vcftelextractor@2.0.0 both require() and import fail with MODULE_NOT_FOUND / ERR_PACKAGE_PATH_NOT_EXPORTED. This is release-blocking.
Add an explicit allowlist (it overrides .gitignore):
| "main": "./dist/index.js", | |
| "files": ["dist"], | |
| "main": "./dist/index.js", |
Also add a "prepare": "tsup" (so git installs build too) and run npm publish --dry-run in CI to catch this class of bug.
| } | ||
|
|
||
| // Attach parseVcard and other exports to the main function for CJS compatibility | ||
| namespace extractTel { |
There was a problem hiding this comment.
🟠 export = + namespace merge makes parseVcard a static property, not an ESM named export. After tsup build, dist/index.mjs emits only export default require_index() and dist/index.d.ts only export { extractTel as default }.
I confirmed the runtime consequence — the README's documented import extractTel, { parseVcard } from '...' throws at load:
SyntaxError: The requested module './dist/index.mjs' does not provide an export named 'parseVcard'
Only import extractTel from '...' (then extractTel.parseVcard) or CJS destructuring work. So the "dual package" headline is half-broken for ESM.
Pick one and make code + README + .d.ts agree:
- (a) docs-only fix:
import extractTel from 'vcfTelExtractor'; const { parseVcard } = extractTel; - (b) real named exports:
export default extractTel; export { parseVcard };— but be aware this makes CJSrequire()return{ default, parseVcard }, breaking the callable-requireergonomic you document in Example 1.
| The following key mappings are utilized to enhance the readability of the output: | ||
| **ES Modules (ESM) / TypeScript:** | ||
| ```typescript | ||
| import extractTel, { parseVcard } from 'vcfTelExtractor'; |
There was a problem hiding this comment.
🟠 This import is broken in ESM — parseVcard is not a named export (see my note in src/index.ts). Verified it throws SyntaxError: ... does not provide an export named 'parseVcard' at module load. Use import extractTel from 'vcfTelExtractor'; const { parseVcard } = extractTel;, or fix the underlying export.
| This module is designed to handle errors gracefully by rejecting the promise returned by the extractTel function. Users should implement appropriate error handling in their applications. | ||
| #### Example 3: Direct String Parsing (Browser-Safe) | ||
| ```javascript | ||
| import { parseVcard } from 'vcfTelExtractor'; |
There was a problem hiding this comment.
🟠 Same broken ESM named import as line 28 — this Example 3 throws a SyntaxError at load for ESM/TS consumers, so the "Browser-Safe" example does not actually run as written. Update it (or fix the export).
| cleaned = cleaned.substring(1); | ||
| } | ||
| // Filter out unwanted single digits like '3' or '0' | ||
| if (/^3$|^0$/.test(cleaned) || !cleaned) { |
There was a problem hiding this comment.
🟡 This /^3$|^0$/ filter is a leftover from the v1 whole-file-regex approach, where splitting on whitespace produced stray 3/0 tokens. In this structured parser cleaned is a real TEL value, so the digit filter can now only silently drop legitimate numbers (e.g. a short extension TEL:3).
Recommend dropping the value check and keeping just the emptiness guard:
| if (/^3$|^0$/.test(cleaned) || !cleaned) { | |
| if (!cleaned) { |
|
|
||
| // Split parameters to get the base property name | ||
| const paramParts = restOfKey.split(';'); | ||
| const rawKey = paramParts[0].toUpperCase(); |
There was a problem hiding this comment.
🟢 The PR description says parameters are "extracted", but paramParts[0] keeps only the base property name and all parameters (TYPE=CELL, PREF, ENCODING=QUOTED-PRINTABLE, CHARSET=) plus the group prefix are silently discarded. That's a fine default, but (a) QUOTED-PRINTABLE/charset values then won't be decoded despite the "spec compliant" claim, and (b) the description overstates behavior. Align the docs, or actually surface params.
| onlyNumbers?: boolean; | ||
|
|
||
| /** | ||
| * For regex-based fallback extraction, includes the '+' prefix if true. |
There was a problem hiding this comment.
🟢 Stale doc comment: "For regex-based fallback extraction" refers to the removed v1 regex path. In v2, prefix only controls whether the leading + is kept in onlyNumbers output. Please update.
| WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
| FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | ||
| OTHER DEALINGS IN THE SOFTWARE. | ||
| MIT License. Copyright (c) Wouter Vroege. |
There was a problem hiding this comment.
🟢 Wrong copyright holder — Wouter Vroege looks copied from another project's license. Should be the actual author (Adepoju Daniel / Balanced02). Also confirm the repo's LICENSE file matches.
| @@ -1 +1,2 @@ | |||
| node_modules No newline at end of file | |||
| node_modules | |||
| dist No newline at end of file | |||
There was a problem hiding this comment.
🟢 Since you're editing .gitignore: a .DS_Store is currently tracked in the repo (and included in the npm tarball). Add .DS_Store here and run git rm --cached .DS_Store.
| }, | ||
| ]); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🟡 16 passing is a good start, but the tests run against the TypeScript source, not the built dist/, so they can't catch packaging/interop bugs. Notably missing:
- A dual-package smoke test (
require('../dist')+importfromdist) — this is exactly what would have caught the brokenparseVcardESM export and the empty-tarball issue. - Parameters/groups:
TEL;TYPE=CELL:...,item1.TEL:.... - Unescaping:
\n,\,,\;,\\. - The browser guard (
isBrowser→ path input should throw). onlyNumberscombined withnormalize: true, and withprefix: true/false.- The invalid-number fallback branch in
normalizePhone.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/index.test.ts (1)
19-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing coverage for
extractTel's content-vs-path detection.All "File Input" tests exercise the path-reading branch; none call
extractTel(rawVcardString)to verify theisVcardContentheuristic in src/index.ts (lines 16-20) correctly routes raw content straight toparseVcardwithout touchingfs. That branch is core new behavior for this layer per the PR description and is currently only indirectly covered via the standaloneparseVcardtests.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/index.test.ts` around lines 19 - 53, Add a test in the “extractTel Function (File Input)” suite that passes a raw vCard string directly to extractTel and verifies the parsed result, while asserting readFileStub is not called. Exercise the isVcardContent detection path and retain the existing file-input coverage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@package.json`:
- Around line 5-13: Add a package-level files configuration in package.json to
explicitly include the dist directory in published npm contents. Ensure the
built artifacts referenced by main, module, types, and exports—dist/index.js,
dist/index.mjs, and dist/index.d.ts—are included despite .gitignore.
In `@src/index.ts`:
- Around line 28-35: Remove the direct dynamic `fs` import from the
browser-reachable code in the file-path parsing branch of the main entrypoint.
Move that branch into a Node-only entrypoint or configure a browser export
condition so browser builds resolve an implementation that never references
`fs`, while preserving the existing `_parseVcard` behavior for Node file inputs.
In `@src/parser.ts`:
- Around line 22-28: Update unescapeValue to decode all supported escape
sequences in a single left-to-right replacement pass, matching escaped
backslashes, newlines, commas, and semicolons without chained replacements.
Preserve each escape’s existing decoded value while ensuring a literal backslash
followed by n, comma, or semicolon remains intact.
- Around line 158-179: The onlyNumbers branch in the parser still rewrites
custom normalize results and applies unexplained single-digit filtering. Update
the loop around contact.number so onlyNumbers preserves the output produced by
the existing normalize flow, and remove the hard-coded 3/0 filter unless the
behavior is explicitly required and covered by a test; retain the intended
prefix handling and empty-value exclusion.
---
Nitpick comments:
In `@src/index.test.ts`:
- Around line 19-53: Add a test in the “extractTel Function (File Input)” suite
that passes a raw vCard string directly to extractTel and verifies the parsed
result, while asserting readFileStub is not called. Exercise the isVcardContent
detection path and retain the existing file-input coverage unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e81df37b-1179-476d-a510-01266ebecc48
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (12)
.gitignoreREADME.mdindex.jspackage.jsonreadFile.jssrc/index.test.tssrc/index.tssrc/parser.tssrc/types.tstsconfig.jsontsup.config.tsvcfTelExtractor.test.js
💤 Files with no reviewable changes (3)
- index.js
- vcfTelExtractor.test.js
- readFile.js
…s, and test coverage
Balanced02
left a comment
There was a problem hiding this comment.
Re-review — fix commit 61cc9c9
I rebuilt from this commit (npm ci && npm run build && npm test, npm pack --dry-run, plus ESM/CJS consumer scripts against dist/). Both blockers are genuinely resolved — nice work.
✅ Verified fixed
- Packaging:
files: ["dist"]added →npm pack --dry-runnow ships all 16dist/*files (was source-only). - ESM named export: real
export { parseVcard }+export default+ a CJS post-build shim. Confirmed all four paths now work: ESMimport extractTel, { parseVcard }, ESM default, CJSrequire()(stays callable), CJS destructure..d.tsexports both. The README import examples are now correct. - Dead
/^3$|^0$/filter removed; no-op try/catch removed; tests now run againstdist/(21 passing) with new params/escaping/browser/raw-string coverage;unescapeValueis now a correct single-pass;.DS_Storeuntracked; mocha glob quoted + builds first.
🟡 New issues (introduced by the fixes)
onlyNumbersregression — removing the filter also removed digit-cleanup, so it now returns numbers with embedded spaces (['44 20 7946 0018']). (src/parser.ts)paramsemitted by default — changes contact output shape for commonTYPE=-tagged vCards; undocumented, no opt-out, and ignores thefieldsfilter. Also makes README Example 3's output wrong. (src/parser.ts / README)
🟢 Still open (minor, from last round)
Contact.firstName/versiontypedstringbut can bestring[]. 4.Wouter Vroegecopyright.
Verdict: blockers cleared; the two 🟡 items are worth resolving before publishing 2.0.0 (esp. onlyNumbers, which silently returns malformed numbers).
| const numList = Array.isArray(nums) ? nums : [nums]; | ||
| for (let num of numList) { | ||
| if (!num) continue; | ||
| let processed = num; |
There was a problem hiding this comment.
🟡 New regression introduced by removing the /^3$|^0$/ filter. Removing that filter was right, but the sibling num.replace(/[^\d+]/g, '') digit-cleanup got dropped along with it, so onlyNumbers no longer strips internal spaces/hyphens. Verified against the built dist:
parseVcard('BEGIN:VCARD\nTEL:+44 20 7946-0018\nEND:VCARD', { onlyNumbers: true, prefix: false })
// => ['44 20 7946 0018'] ← spaces embeddedThis regresses both v1 (which returned clean digit strings) and the previous revision. The existing test only uses +1234567890 (no separators), so it doesn't catch it. Restore the cleanup:
| let processed = num; | |
| let processed = num.replace(/[^\d+]/g, ''); |
(and please add an onlyNumbers test with a spaced/hyphenated TEL.)
| } | ||
|
|
||
| // Save field parameters to the Contact metadata | ||
| if (!currentContact.params) { |
There was a problem hiding this comment.
🟡 params is now emitted by default and changes the contact shape for very common vCards (TEL;TYPE=CELL, EMAIL;TYPE=WORK, …). Verified:
parseVcard('BEGIN:VCARD\nFN:Jane Doe\nTEL;TYPE=CELL:+15555555555\nEND:VCARD', { fields: ['firstName','number'] })
// => [{ params: { number: [ { TYPE: ['CELL'] } ] }, firstName: 'Jane Doe', number: '+15555555555' }]Concerns:
- It's undocumented — neither the README API section nor the options table mention
params, and there's no flag to opt out (e.g.includeParams?: boolean). - It ignores the
fieldsfilter — a user who asked for onlyfirstName/numberstill gets aparamsblob. - It makes README Example 3's stated output wrong (see my note there).
Consider making params opt-in, or at minimum document it in ExtractTelOptions/README and have it respect fields.
| We welcome and appreciate contributions to enhance vcfTelExtractor. Please ensure any contributed code is accompanied by relevant tests. | ||
| const contacts = parseVcard(rawVcard, { fields: ['firstName', 'number'] }); | ||
| console.log(contacts); | ||
| // [{ firstName: 'Jane Doe', number: '+15555555555' }] |
There was a problem hiding this comment.
This documented output is now inaccurate — because the input uses TEL;TYPE=CELL, the actual result also includes params: { number: [{ TYPE: ['CELL'] }] } (see my note in src/parser.ts). Either update the expected output here or make params opt-in.
|
|
||
| export interface Contact { | ||
| number?: string | string[]; | ||
| firstName?: string; |
There was a problem hiding this comment.
🟢 Still open from the last round: under the default multiValueMode: 'array', repeated FN/VERSION become arrays at runtime, but firstName and version are typed string (while number/email are string | string[]). Make them consistent.
| WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
| FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | ||
| OTHER DEALINGS IN THE SOFTWARE. | ||
| MIT License. Copyright (c) Wouter Vroege. |
There was a problem hiding this comment.
🟢 Still open: copyright holder Wouter Vroege looks copied from another project. Should be the actual author (Adepoju Daniel / Balanced02); confirm the LICENSE file matches too.
| @@ -1 +1,2 @@ | |||
| node_modules No newline at end of file | |||
| node_modules | |||
| dist No newline at end of file | |||
There was a problem hiding this comment.
🟢 Nice — .DS_Store was untracked. To stop it coming back, add it here too:
| dist | |
| dist | |
| .DS_Store |
…ontact type properties and copyright
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/index.test.ts (1)
1-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for group prefix handling.
The parser handles vCard group prefixes (e.g.,
item1.TEL:+1234567890) atsrc/parser.tslines 103-109, but no test verifies this behavior. A group-prefixed TEL should map tonumbercorrectly and the prefix should be discarded. This was flagged in a previous review as a missing test case.🧪 Suggested test
it('should handle group-prefixed properties (e.g., item1.TEL)', () => { const data = `BEGIN:VCARD\nVERSION:3.0\nFN:Jane Doe\nitem1.TEL:+15555555555\nEND:VCARD`; const result = parseVcard(data); expect(result).to.deep.equal([ { number: '+15555555555', firstName: 'Jane Doe', version: '3.0', }, ]); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/index.test.ts` around lines 1 - 295, Add a parseVcard test covering a grouped TEL property such as item1.TEL, and assert it produces the normal number field with the group prefix removed while preserving the contact name and version.src/parser.ts (1)
160-195: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueParams initialization is duplicated across three branches.
The
if (!currentContact.params) { currentContact.params = {}; }guard and the single-value assignment pattern repeat in themultiValueMode: 'last'and new-key branches (lines 180-185 and 189-194 are identical). Extracting a small helper would reduce duplication and the risk of them diverging.♻️ Suggested helper extraction
+ const setParams = (mappedKey: string, propertyParams: Record<string, string[]>) => { + if (!currentContact.params) { + currentContact.params = {}; + } + currentContact.params[mappedKey] = [propertyParams]; + }; + // Save value & align parameters array length if (currentContact[mappedKey] !== undefined) { // ... array branch unchanged ... } else { currentContact[mappedKey] = parsedVal; - if (includeParams && propertyParams) { - if (!currentContact.params) { - currentContact.params = {}; - } - currentContact.params[mappedKey] = [propertyParams]; - } + if (includeParams && propertyParams) setParams(mappedKey, propertyParams); } } else { currentContact[mappedKey] = parsedVal; - if (includeParams && propertyParams) { - if (!currentContact.params) { - currentContact.params = {}; - } - currentContact.params[mappedKey] = [propertyParams]; - } + if (includeParams && propertyParams) setParams(mappedKey, propertyParams); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/parser.ts` around lines 160 - 195, Extract the repeated parameter initialization and single-value assignment logic from the currentContact save branches into a small local helper, then reuse it in the multiValueMode non-array branch and the new-key branch. Preserve the existing includeParams/propertyParams conditions and resulting currentContact.params[mappedKey] structure, while leaving the array-mode append behavior unchanged.tsup.config.ts (1)
16-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider failing the build when the CJS shim cannot be applied.
The
catchblock only logs a warning. If the shim fails for either file, CJS consumers will receivemodule.exportsas a plain object (e.g.,{ default: [Function], parseVcard: ... }) instead of the callable function, silently breakingrequire('vcfTelExtractor')()usage. Since this shim is essential to the package's CJS contract, a silent warn risks shipping a broken package.♻️ Proposed fix: rethrow to fail the build
} catch (err) { - console.warn(`Could not append CJS shim to dist/${filename} (maybe ESM-only build or file missing):`, err); + console.error(`Could not append CJS shim to dist/${filename}:`, err); + throw err; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tsup.config.ts` around lines 16 - 38, Update the catch block in the tsup onSuccess callback to rethrow the shim application error after reporting it, instead of only issuing a warning. Ensure a failure processing either filename in filesToShim propagates out of onSuccess and causes the build to fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/index.test.ts`:
- Around line 1-295: Add a parseVcard test covering a grouped TEL property such
as item1.TEL, and assert it produces the normal number field with the group
prefix removed while preserving the contact name and version.
In `@src/parser.ts`:
- Around line 160-195: Extract the repeated parameter initialization and
single-value assignment logic from the currentContact save branches into a small
local helper, then reuse it in the multiValueMode non-array branch and the
new-key branch. Preserve the existing includeParams/propertyParams conditions
and resulting currentContact.params[mappedKey] structure, while leaving the
array-mode append behavior unchanged.
In `@tsup.config.ts`:
- Around line 16-38: Update the catch block in the tsup onSuccess callback to
rethrow the shim application error after reporting it, instead of only issuing a
warning. Ensure a failure processing either filename in filesToShim propagates
out of onSuccess and causes the build to fail.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0eba9888-e941-4384-a756-0bbb200e537e
⛔ Files ignored due to path filters (1)
.DS_Storeis excluded by!**/.DS_Store
📒 Files selected for processing (8)
README.mdpackage.jsonsrc/browser.tssrc/index.test.tssrc/index.tssrc/parser.tssrc/types.tstsup.config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/types.ts
- package.json
- README.md
.DS_Store was previously untracked but never added to .gitignore, so it could be re-added accidentally. Add it to the ignore list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This PR implements the Version 2.0.0 rewrite of the
vcfTelExtractormodule into TypeScript.Summary of Changes
Modern Infrastructure:
tsupfor compiling dual ESM/CommonJS distribution builds (/dist) with native type declarations (.d.ts).>=18.0.0for modern feature support.vCard Parser Compliance:
item1.TEL) and parameter attributes.Advanced Extraction & Normalization:
libphonenumber-js.Testing & Isomorphism:
Summary by CodeRabbit
parseVcardplus asyncextractTel, with support for field filtering, TEL normalization, multi-value handling, and optional vCard parameter metadata.dist/artifacts, plus Node.js >= 18.