Skip to content

feat!: TypeScript rewrite, vCard spec compliance, E.164 phone normalization#2

Open
Balanced02 wants to merge 4 commits into
masterfrom
feature/v2-typescript-rewrite
Open

feat!: TypeScript rewrite, vCard spec compliance, E.164 phone normalization#2
Balanced02 wants to merge 4 commits into
masterfrom
feature/v2-typescript-rewrite

Conversation

@Balanced02

@Balanced02 Balanced02 commented Jul 13, 2026

Copy link
Copy Markdown
Owner

This PR implements the Version 2.0.0 rewrite of the vcfTelExtractor module into TypeScript.

Summary of Changes

  1. Modern Infrastructure:

    • Rewrote the codebase in TypeScript with strict compile checks.
    • Introduced tsup for compiling dual ESM/CommonJS distribution builds (/dist) with native type declarations (.d.ts).
    • Bumped minimum Node.js engine requirement to >=18.0.0 for modern feature support.
  2. vCard Parser Compliance:

    • Implemented standard-compliant RFC line unfolding (resolves multi-line wrapped values correctly).
    • Structured key-value colon splitting to support colons in values (e.g. URLs).
    • Extracted group prefixes (e.g., item1.TEL) and parameter attributes.
    • Unescaped special formatting characters.
  3. Advanced Extraction & Normalization:

    • Integrated optional phone number validation and E.164 formatting via libphonenumber-js.
    • Added custom normalization callback capability.
    • Added support for parsing multiple instances of the same property as arrays (default) or last-value overwrites.
  4. Testing & Isomorphism:

    • Rewrote and expanded unit tests in TypeScript (16 passing test scenarios).
    • Designed code to be isomorphic for safe filesystem/string execution across Node.js and browser runtimes.

Summary by CodeRabbit

  • New Features
    • Added browser-friendly extraction from raw vCard text (no filesystem required).
    • Added synchronous parseVcard plus async extractTel, with support for field filtering, TEL normalization, multi-value handling, and optional vCard parameter metadata.
  • Packaging / Release
    • Released version 2.0.0 with updated ESM/CJS/browser entry points and dist/ artifacts, plus Node.js >= 18.
  • Documentation
    • Reworked the README with structured API/usage guidance and expanded option details.
  • Tests
    • Expanded TypeScript-based integration tests and removed the older JavaScript test suite.

…ual CJS/ESM distribution, and browser support
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3eb148c6-21d8-4d6d-be35-77c0e732eded

📥 Commits

Reviewing files that changed from the base of the PR and between f53bafe and 2564bfb.

📒 Files selected for processing (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • .gitignore

📝 Walkthrough

Walkthrough

The 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.

Changes

vCard extraction rewrite

Layer / File(s) Summary
Typed vCard parser and normalization
src/types.ts, src/parser.ts, src/index.test.ts
Adds typed options and contact models, vCard unfolding and unescaping, configurable mappings and fields, phone normalization, parameter metadata, multi-value handling, and parser coverage.
String and file extraction entrypoint
src/index.ts, src/browser.ts, src/index.test.ts
Adds async extraction from raw vCard strings or Node file paths, browser-safe parsing, named and static parseVcard access, and integration tests for file and browser flows.
Build, package, and usage distribution
tsconfig.json, tsup.config.ts, package.json, .gitignore, README.md
Adds TypeScript bundling and declarations, CJS/ESM and browser exports, updated dependencies and scripts, ignored build output, and expanded API documentation.

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main changes: a TypeScript rewrite with vCard compliance improvements and E.164 normalization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/v2-typescript-rewrite

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Balanced02 Balanced02 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Published package contains no build output. dist is gitignored with no files allowlist → npm pack --dry-run ships only src/*.ts. All entry points target dist/, so npm install + require/import fails. (see package.json)
  2. Documented ESM import { parseVcard } throws a SyntaxError at load. export = + namespace exposes parseVcard only 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

  1. Dead /^3$|^0$/ filter carried over from v1 can silently drop valid numbers. (src/parser.ts:172)
  2. Test coverage gaps on the headline features — dual-package, params/groups, unescaping, browser guard. Tests exercise source, not dist. (src/index.test.ts)

🟢 Nits

  1. No-op try/catch; inaccurate Contact types under multiValueMode:'array'; parameters silently dropped despite "extracted" claim; stale prefix comment; wrong Wouter Vroege copyright; tracked .DS_Store; unquoted mocha glob src/**/*.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/.

Comment thread package.json
"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",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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/*.tsno 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):

Suggested change
"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.

Comment thread src/index.ts
}

// Attach parseVcard and other exports to the main function for CJS compatibility
namespace extractTel {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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 CJS require() return { default, parseVcard }, breaking the callable-require ergonomic you document in Example 1.

Comment thread README.md
The following key mappings are utilized to enhance the readability of the output:
**ES Modules (ESM) / TypeScript:**
```typescript
import extractTel, { parseVcard } from 'vcfTelExtractor';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 This import is broken in ESMparseVcard 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.

Comment thread README.md
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';

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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).

Comment thread src/parser.ts Outdated
cleaned = cleaned.substring(1);
}
// Filter out unwanted single digits like '3' or '0'
if (/^3$|^0$/.test(cleaned) || !cleaned) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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:

Suggested change
if (/^3$|^0$/.test(cleaned) || !cleaned) {
if (!cleaned) {

Comment thread src/parser.ts

// Split parameters to get the base property name
const paramParts = restOfKey.split(';');
const rawKey = paramParts[0].toUpperCase();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Comment thread src/types.ts Outdated
onlyNumbers?: boolean;

/**
* For regex-based fallback extraction, includes the '+' prefix if true.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Comment thread README.md Outdated
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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Comment thread .gitignore Outdated
@@ -1 +1,2 @@
node_modules No newline at end of file
node_modules
dist No newline at end of file

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Comment thread src/index.test.ts
},
]);
});
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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') + import from dist) — this is exactly what would have caught the broken parseVcard ESM export and the empty-tarball issue.
  • Parameters/groups: TEL;TYPE=CELL:..., item1.TEL:....
  • Unescaping: \n, \,, \;, \\.
  • The browser guard (isBrowser → path input should throw).
  • onlyNumbers combined with normalize: true, and with prefix: true/false.
  • The invalid-number fallback branch in normalizePhone.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/index.test.ts (1)

19-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for extractTel's content-vs-path detection.

All "File Input" tests exercise the path-reading branch; none call extractTel(rawVcardString) to verify the isVcardContent heuristic in src/index.ts (lines 16-20) correctly routes raw content straight to parseVcard without touching fs. That branch is core new behavior for this layer per the PR description and is currently only indirectly covered via the standalone parseVcard tests.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2377b6d and 349434d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • .gitignore
  • README.md
  • index.js
  • package.json
  • readFile.js
  • src/index.test.ts
  • src/index.ts
  • src/parser.ts
  • src/types.ts
  • tsconfig.json
  • tsup.config.ts
  • vcfTelExtractor.test.js
💤 Files with no reviewable changes (3)
  • index.js
  • vcfTelExtractor.test.js
  • readFile.js

Comment thread package.json
Comment thread src/index.ts Outdated
Comment thread src/parser.ts
Comment thread src/parser.ts

@Balanced02 Balanced02 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-run now ships all 16 dist/* files (was source-only).
  • ESM named export: real export { parseVcard } + export default + a CJS post-build shim. Confirmed all four paths now work: ESM import extractTel, { parseVcard }, ESM default, CJS require() (stays callable), CJS destructure. .d.ts exports both. The README import examples are now correct.
  • Dead /^3$|^0$/ filter removed; no-op try/catch removed; tests now run against dist/ (21 passing) with new params/escaping/browser/raw-string coverage; unescapeValue is now a correct single-pass; .DS_Store untracked; mocha glob quoted + builds first.

🟡 New issues (introduced by the fixes)

  1. onlyNumbers regression — removing the filter also removed digit-cleanup, so it now returns numbers with embedded spaces (['44 20 7946 0018']). (src/parser.ts)
  2. params emitted by default — changes contact output shape for common TYPE=-tagged vCards; undocumented, no opt-out, and ignores the fields filter. Also makes README Example 3's output wrong. (src/parser.ts / README)

🟢 Still open (minor, from last round)

  1. Contact.firstName/version typed string but can be string[]. 4. Wouter Vroege copyright.

Verdict: blockers cleared; the two 🟡 items are worth resolving before publishing 2.0.0 (esp. onlyNumbers, which silently returns malformed numbers).

Comment thread src/parser.ts Outdated
const numList = Array.isArray(nums) ? nums : [nums];
for (let num of numList) {
if (!num) continue;
let processed = num;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 embedded

This 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:

Suggested change
let processed = num;
let processed = num.replace(/[^\d+]/g, '');

(and please add an onlyNumbers test with a spaced/hyphenated TEL.)

Comment thread src/parser.ts Outdated
}

// Save field parameters to the Contact metadata
if (!currentContact.params) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 fields filter — a user who asked for only firstName/number still gets a params blob.
  • 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.

Comment thread README.md
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' }]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/types.ts Outdated

export interface Contact {
number?: string | string[];
firstName?: string;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Comment thread README.md Outdated
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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Comment thread .gitignore Outdated
@@ -1 +1,2 @@
node_modules No newline at end of file
node_modules
dist No newline at end of file

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nice — .DS_Store was untracked. To stop it coming back, add it here too:

Suggested change
dist
dist
.DS_Store

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/index.test.ts (1)

1-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for group prefix handling.

The parser handles vCard group prefixes (e.g., item1.TEL:+1234567890) at src/parser.ts lines 103-109, but no test verifies this behavior. A group-prefixed TEL should map to number correctly 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 value

Params initialization is duplicated across three branches.

The if (!currentContact.params) { currentContact.params = {}; } guard and the single-value assignment pattern repeat in the multiValueMode: '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 win

Consider failing the build when the CJS shim cannot be applied.

The catch block only logs a warning. If the shim fails for either file, CJS consumers will receive module.exports as a plain object (e.g., { default: [Function], parseVcard: ... }) instead of the callable function, silently breaking require('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

📥 Commits

Reviewing files that changed from the base of the PR and between 349434d and f53bafe.

⛔ Files ignored due to path filters (1)
  • .DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (8)
  • README.md
  • package.json
  • src/browser.ts
  • src/index.test.ts
  • src/index.ts
  • src/parser.ts
  • src/types.ts
  • tsup.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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant