diff --git a/WRITEUP.md b/WRITEUP.md new file mode 100644 index 0000000..4b5763c --- /dev/null +++ b/WRITEUP.md @@ -0,0 +1,86 @@ +## Summary + +When enabling the allowJs option in the tsconfig.json, the tests fail with the following error: + +```less +✖ 0479083575 to be parsed to a valid number (0.881922ms) + TypeError [Error]: Cannot read properties of undefined (reading 'hasOwnProperty') + at isSupportedCountry (/path/to/project/node_modules/.pnpm/libphonenumber-js@1.11.15/node_modules/libphonenumber-js/source/metadata.js:510:28) + at new PhoneNumberMatcher (/path/to/project/node_modules/.pnpm/libphonenumber-js@1.11.15/node_modules/libphonenumber-js/source/PhoneNumberMatcher.js:157:49) + at findPhoneNumbersInText (/path/to/project/node_modules/.pnpm/libphonenumber-js@1.11.15/node_modules/libphonenumber-js/source/findPhoneNumbersInText.js:6:18) + at call (/path/to/project/node_modules/.pnpm/libphonenumber-js@1.11.15/node_modules/libphonenumber-js/min/index.cjs.js:14:14) + at findPhoneNumbersInText (/path/to/project/node_modules/.pnpm/libphonenumber-js@1.11.15/node_modules/libphonenumber-js/min/index.cjs.js:63:9) + at TestContext. (/path/to/project/test/test-file.test.ts:10:24) +``` + +## Steps to Reproduce + +1. Set up a TypeScript project with the following tsconfig.json + +```json +{ + "compilerOptions": { + "allowJs": true, + "outDir": "dist", + "moduleResolution": "node16", + "esModuleInterop": true, + "strict": false, + "resolveJsonModule": true + } +} +``` + +2. Install libphonenumber-js (version: 1.11.15). +3. Create the following test file: + +```typescript +import { expect } from "expect"; +import { describe, it } from "node:test"; +import { findPhoneNumbersInText } from "libphonenumber-js"; + +const tests = ["0479083575"]; +describe("findPhoneNumbersInText", async () => { + for (const item of tests) { + it(`${item} to be parsed to a valid number`, async () => { + const [parsed] = findPhoneNumbersInText(item, "AU"); + expect(parsed).toEqual( + expect.objectContaining({ + endsAt: 10, + startsAt: 0, + number: expect.objectContaining({ + country: "AU", + number: "+61479083575", + }), + }) + ); + }); + } +}); +``` + +4. Run the test using node:test or any compatible runner. + +## Expected Behaviour + +The test should parse the phone number correctly, returning a valid result. + +## Actual Behaviour + +The test fails with a `TypeError`: + +```javacript +TypeError [Error]: Cannot read properties of undefined (reading 'hasOwnProperty') +``` + +## Additional Context + +- This issue is not present when `allowJs` is disabled in tsconfig.json. +- The error originates in the isSupportedCountry method from the metadata.js file in libphonenumber-js. +- The problem seems to be related to the interaction between the library's code and the TypeScript configuration when JavaScript files are allowed. + +## Environment + +- libphonenumber-js version: 1.11.15 +- Node.js version: 22.x +- TypeScript version: 5.x +- Test Runner: node:test diff --git a/linkify.ts b/linkify.ts deleted file mode 100644 index fed92b3..0000000 --- a/linkify.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { findPhoneNumbers } from "./phone_numbers"; - -import linkifyHtml from "linkify-html"; - -export const linkify = (origText: string) => { - if (!origText?.length) { - return ""; - } - - let newText = origText; - - // NOTE: this lib wont find phone numbers that are close together i.e. "0432227052, 0432227053" - // it thinks thats one large phone number. - try { - const getPhoneNumbers = findPhoneNumbers(origText); - console.warn( - getPhoneNumbers, - "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<," - ); - let tmpText = origText; - - let runningAdditionCount = 0; - let lastIndex = 0; - - for (const foundPhoneNumber of getPhoneNumbers) { - const validatedNumber = foundPhoneNumber.number.number; - - if (lastIndex > 0 && lastIndex > foundPhoneNumber.startsAt) continue; - - const pattern = origText.substring( - foundPhoneNumber.startsAt, - foundPhoneNumber.endsAt - ); - - // `/.*<\/a>/g` - const testOne = generatePhoneATagRegex({ first: validatedNumber }); - // `/\+61738572222<\/a>/g` - const testTwo = generatePhoneATagRegex({ second: validatedNumber }); - // `/.*<\/a>/g` - const testThree = generatePhoneATagRegex({ first: pattern }); - // `/0432227352<\/a>/g` - const testFour = generatePhoneATagRegex({ second: pattern }); - - // only search where the number was found, - // otherwise subsequent phone numbers in a description wouldn't get linkified - const largestPhoneNumber = biggest([validatedNumber, pattern]); - - if (lastIndex > 0) - tmpText = tmpText.substring(lastIndex, origText.length); - - const searchFromText = tmpText.match( - new RegExp(`` - ) - ); - - let alreadyLinkified = false; - const match = searchFromHref || searchFromText || false; - - if (match) { - const foundIndex = match?.index ?? 0; - const start = foundIndex + lastIndex; - const areaToSearch = origText.substring( - start, - start + match?.[0]?.length - ); - - lastIndex = foundIndex + match?.[0]?.length; - - alreadyLinkified = [ - areaToSearch.search(testOne) > -1, - areaToSearch.search(testTwo) > -1, - areaToSearch.search(testThree) > -1, - areaToSearch.search(testFour) > -1, - ].some((r) => r); - } - if (!alreadyLinkified) { - const toAdd = `${pattern}`; - - newText = replaceBetween( - newText, - foundPhoneNumber.startsAt + runningAdditionCount, - foundPhoneNumber.endsAt + runningAdditionCount, - toAdd - ); - - runningAdditionCount += toAdd.length - pattern.length; - } - } - } catch (error) { - console.error(error); - } - - newText = linkifyHtml(newText, { - defaultProtocol: "https", - }); - - return newText; -}; - -/** - * stolen from MDN as RegExp doesn't do this itself: - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - */ -function escapeRegExp(string: string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -/** - * returns a string with between start and end replaced with `what` - */ -function replaceBetween( - string: string, - start: number, - end: number, - what: string -) { - return string.substring(0, start) + what + string.substring(end); -} - -/** - * get the longest string in strings - */ -function biggest(strings: string[]) { - let biggestString = strings[0]; - for (let i = 0; i < strings.length; i++) { - const element = strings[i]; - if (element.length > biggestString.length) { - biggestString = element; - } - } - return biggestString; -} - -/** - * generates RegExp with args in first and second positions or anything (.*) - * e.g. `/.*<\/a>/g` - */ -function generatePhoneATagRegex({ - first, - second, -}: { - first?: string; - second?: string; -}) { - const firstSearch = first ? escapeRegExp(first) : ".*"; - const secondSearch = second ? `(.*?)${escapeRegExp(second)}(.*?)` : ".*"; - return new RegExp(`${secondSearch}<\/a>`, "g"); -} - -export default { linkify }; diff --git a/phone_numbers.ts b/phone_numbers.ts deleted file mode 100644 index 9cf23c3..0000000 --- a/phone_numbers.ts +++ /dev/null @@ -1,73 +0,0 @@ -import libphonenumber from 'google-libphonenumber' -import { - isValidPhoneNumber, - findPhoneNumbersInText, - parsePhoneNumberWithError, -} from 'libphonenumber-js' - -const phoneUtil = libphonenumber.PhoneNumberUtil.getInstance() -const PNF = libphonenumber.PhoneNumberFormat - -const AUSTRALIA = 'AU' - -export const isValidNumber = (value: string) => { - if (!value) return false - try { - return isValidPhoneNumber(value, AUSTRALIA) - } catch (err) { - try { - // fallback to google-libphonenumber - const valid = phoneUtil.isValidNumberForRegion( - phoneUtil.parse(value, AUSTRALIA), - AUSTRALIA - ) - - return valid - } catch (err) { - return value - } - } -} - -export const formatNumber = ( - value: string, - format: 'national' | 'e164' = 'national' -) => { - if (!value) return '' - try { - if (typeof value !== 'string') return '' - - const phoneNumber = parsePhoneNumberWithError(value, AUSTRALIA) - - return format === 'national' - ? phoneNumber.formatNational() - : phoneNumber.format('E.164') - } catch (err) { - try { - // fallback to google-libphonenumber - const proto = phoneUtil.parse(value, AUSTRALIA) - - const shape = format === 'national' ? PNF.NATIONAL : PNF.E164 - value = phoneUtil.format(proto, shape) - - return value - } catch (err) { - throw new TypeError( - `Value is not a valid phone number of the form 0412 345 678 (7-15 digits): ${value}` - ) - } - } -} - -export const findPhoneNumbers = (value: string) => { - if (!value) return [] - try { - if (typeof value !== 'string') return [] - - const phoneNumbers = findPhoneNumbersInText(value, AUSTRALIA) - - return phoneNumbers - } catch (err) { - return [] - } -} diff --git a/test/index.test.ts b/test/index.test.ts deleted file mode 100644 index 53f912a..0000000 --- a/test/index.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { linkify } from "../linkify"; -import { describe, it } from "node:test"; -import expect from "expect"; - -const tests = [ - ["0479083575", '0479083575'], - // [ - // '0479083575', - // '0479083575', - // ], - // [ - // "this is a long string with multiple phone numbers like this one 0432227352 and then we need to have another one a fair whack away just to test that it will do both 0432227352", - // 'this is a long string with multiple phone numbers like this one 0432227352 and then we need to have another one a fair whack away just to test that it will do both 0432227352', - // ], - // [ - // 'this is a long string with multiple phone numbers like this one 0432227352 and then we need to have another one a fair whack away just to test that it will do both 0432227352', - // 'this is a long string with multiple phone numbers like this one 0432227352 and then we need to have another one a fair whack away just to test that it will do both 0432227352', - // ], - // /** - // * This case fails because libphonenumber-js thinks "0432227352, 0432227354" is one phone number - // */ - // // [ - // // "what if there are two numbers close together like 0431117051, 0437772345", - // // 'what if there are two numbers close together like 0431117051, 0437772345', - // // ], - // [ - // '

Physiotherapy by Appointment

Fizzio Clinics appointments please call 1300 693 499

', - // '

Physiotherapy by Appointment

Fizzio Clinics appointments please call 1300 693 499

', - // ], - // [ - // '

this is my phone number 0431117051

', - // '

this is my phone number 0431117051

', - // ], - // [ - // '

this is my phone number 0431117051

', - // '

this is my phone number 0431117051

', - // ], - // [ - // 'google', - // 'google', - // ], - // ["(04) 7908 3838", '(04) 7908 3838'], - // ["04 3888 8888", '04 3888 8888'], - // ["(07) 3857 2222", '(07) 3857 2222'], - // ["073857222", "073857222"], - // ["google.com.au", 'google.com.au'], - // [ - // "google.com.au and test@gmail.com address", - // 'google.com.au and test@gmail.com address', - // ], -]; - -describe.skip("linkify", () => { - for (const [a, expected] of tests) { - it(`${a} to be parsed to ${expected}`, async () => { - const parsed = linkify(a); - - expect(parsed).toBe(expected); - }); - } -}); diff --git a/test/lphjs.test.js b/test/lphjs.test.js deleted file mode 100644 index 80e949b..0000000 --- a/test/lphjs.test.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -Object.defineProperty(exports, "__esModule", { value: true }); -var expect_1 = require("expect"); -var node_test_1 = require("node:test"); -var libphonenumber_js_1 = require("libphonenumber-js"); -var tests = ["0479083575"]; -(0, node_test_1.describe)("findPhoneNumbersInText", function () { - var _loop_1 = function (item) { - (0, node_test_1.it)("".concat(item, " to be parsed to a valid number"), function () { return __awaiter(void 0, void 0, void 0, function () { - var parsed; - return __generator(this, function (_a) { - parsed = (0, libphonenumber_js_1.findPhoneNumbersInText)(item, "AU")[0]; - (0, expect_1.expect)(parsed).toEqual(expect_1.expect.objectContaining({ - endsAt: 10, - startsAt: 0, - number: expect_1.expect.objectContaining({ - country: "AU", - number: "+61479083575", - }), - })); - return [2 /*return*/]; - }); - }); }); - }; - for (var _i = 0, tests_1 = tests; _i < tests_1.length; _i++) { - var item = tests_1[_i]; - _loop_1(item); - } -}); diff --git a/test/lphjs.test.ts b/test/lphjs.test.ts index 3e2dfac..039b780 100644 --- a/test/lphjs.test.ts +++ b/test/lphjs.test.ts @@ -1,10 +1,10 @@ import { expect } from "expect"; import { describe, it } from "node:test"; -// import { findPhoneNumbersInText } from "libphonenumber-js"; +import { findPhoneNumbersInText } from "libphonenumber-js"; const tests = ["0479083575"]; describe("findPhoneNumbersInText", async () => { - const { findPhoneNumbersInText } = await import("libphonenumber-js"); + // const { findPhoneNumbersInText } = await import("libphonenumber-js"); for (const item of tests) { it(`${item} to be parsed to a valid number`, async () => { const [parsed] = findPhoneNumbersInText(item, "AU"); diff --git a/tsconfig.json b/tsconfig.json index 9744df5..0616170 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "resolveJsonModule": true, "outDir": "dist", "declaration": false, - "allowJs": true, + "allowJs": false, "checkJs": false, "baseUrl": ".",