diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 4890dc4..432da8f 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -11,13 +11,21 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' + cache: 'npm' # Added caching for faster PR checks + - name: Install dependencies run: npm ci + - name: Build run: npm run build - - name: Run tests - run: npm test + + - name: Run Unit Tests + run: npm run test:unit + + - name: Run Prototype Integration Tests + run: npm run test:integration \ No newline at end of file diff --git a/package.json b/package.json index e6f42c1..8355ed5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc", - "test": " npm run build && node --test", + "test": "npm run build && node --test dist/tests", + "test:unit": "node --test dist/tests/transformations dist/tests/validations dist/tests/formatting dist/tests/analyzing", + "test:integration": "node --test dist/tests/integration", "prepublishOnly": "npm run build", "format": "prettier --write ." }, diff --git a/src/analyzing/index.ts b/src/analyzing/index.ts index 20055ac..e260c5e 100644 --- a/src/analyzing/index.ts +++ b/src/analyzing/index.ts @@ -23,6 +23,8 @@ import { patternCount } from './patternCount'; import { vowelConsonantCount } from './vowelConsonantCount'; import { checkMultiplePatterns } from './checkMultiplePatterns'; import { checkSubsequence } from './checkSubsequence'; +import { functionWordCount } from './functionWordCount'; +import { contentWordCount } from './contentWordCount'; import { checkStringRotations } from './stringRotation'; import { lexicographicalRank } from './lexicographicalRank'; @@ -37,6 +39,8 @@ export const analyzing = { vowelConsonantCount, checkMultiplePatterns, checkSubsequence, + functionWordCount, + contentWordCount, checkStringRotations, - lexicographicalRank + lexicographicalRank, }; diff --git a/src/index.ts b/src/index.ts index c8295bf..4d427de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,14 +3,157 @@ export * from './formatting'; export * from './transformations'; export * from './validations'; -import { analyzing } from './analyzing'; -import { formatting } from './formatting'; -import { transformations } from './transformations'; -import { validations } from './validations'; +import * as analyzing from './analyzing'; +import * as formatting from './formatting'; +import * as transformations from './transformations'; +import * as validations from './validations'; + +declare global { + interface String { + /* ========================= + Transformations + ========================= */ + + truncateText(maxLength: number, suffix?: string): string; + toSlug(): string; + capitalizeWords(): string; + removeSpecialChars(): string; + removeWords(words: string[]): string; + removeDuplicates(): string; + initials(): string; + + camelCase(): string; + pascalCase(): string; + snakeCase(): string; + kebabCase(): string; + titleCase(): string; + constantCase(): string; + + escapeHTML(): string; + maskSegment(start: number, end: number, maskChar?: string): string; + deburr(): string; + splitChunks(chunkSize: number): string[]; + + numberToText(lang?: string): string; + reverseWordsInString(): string; + + stringPermutations(): string[]; + stringPermutationsGenerator(): Generator; + stringCombinations(): string[]; + + /* ========================= + Validations + ========================= */ + + isURL(): boolean; + isEmail(): boolean; + isDate(): boolean; + isEmpty(): boolean; + isSlug(): boolean; + isTypeOf(type: string): boolean; + + isIPv4(): boolean; + isIPv6(): boolean; + isHexColor(): boolean; + + isPalindrome(): boolean; + isCoordinates(): boolean; + + isLowerCase(): boolean; + isUpperCase(): boolean; + isAlphabetic(): boolean; + isAlphaNumeric(): boolean; + + isAnagram(other: string): boolean; + isMacAddress(): boolean; + isPanagram(): boolean; + + /* ========================= + Analysis + ========================= */ + + wordCount(): number; + contentWordCount(): number; + functionWordCount(): number; + readingDuration(wordsPerMinute?: number): number; + + characterCount(): number; + characterFrequency(): Record; + + stringSimilarity(other: string): number; + complexity(): { + score: number; + uniqueness: number; + length: number; + }; + + patternCount(pattern: string | RegExp): number; + vowelConsonantCount(): { + vowels: number; + consonants: number; + }; + + checkMultiplePatterns(patterns: string[]): Record; + checkSubsequence(subsequence: string): boolean; + stringRotation(other: string): boolean; + + lexicographicalRank(): number; + + /* ========================= + Formatting + ========================= */ + + capitalize(): string; + formatNumber(locale?: string): string; + formatPhone(countryCode?: string): string; + + formatDuration(): string; + trim(): string; + + formatRomanNumeral(): string; + formatPercentage(decimals?: number): string; + formatFileSize(): string; + + formatOrdinal(): string; + formatList(conjunction?: string): string; + + formatCreditCard(): string; + + formatToOctal(prefix?: boolean): string; + formatTemperature(from: 'C' | 'F' | 'K', to: 'C' | 'F' | 'K'): string; + + formatScientific(precision?: number): string; + formatToBinary(groupBits?: boolean): string; + formatToHexadecimal(prefix?: boolean): string; + formatToDecimal(base: 2 | 8 | 16): number; + } +} + +export function extendStringPrototype(): void { + const modules = [analyzing, formatting, transformations, validations]; + + modules.forEach((module) => { + Object.keys(module).forEach((key) => { + const fn = (module as any)[key]; + + if (typeof fn === 'function' && !String.prototype.hasOwnProperty(key)) { + Object.defineProperty(String.prototype, key, { + value: function (this: string, ...args: any[]) { + return fn(this, ...args); + }, + writable: true, + configurable: true, + enumerable: false, + }); + } + }); + }); +} export default { analyzing, formatting, transformations, validations, + extendStringPrototype, }; diff --git a/src/tests/analyzing/contentWordCount.test.ts b/src/tests/analyzing/contentWordCount.test.ts index 621b051..edb1e72 100644 --- a/src/tests/analyzing/contentWordCount.test.ts +++ b/src/tests/analyzing/contentWordCount.test.ts @@ -1,19 +1,21 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { contentWordCount } from "../../analyzing/contentWordCount.js"; +import test, { describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { contentWordCount } from '../../analyzing/contentWordCount.js'; -test("counts content words in a normal sentence", () => { - assert.equal(contentWordCount("This is a test of the system"), 2); -}); +describe('contentWordCount', () => { + test('counts content words in a normal sentence', () => { + assert.equal(contentWordCount('This is a test of the system'), 2); + }); -test("returns 0 when there are no content words", () => { - assert.equal(contentWordCount("is the at of"), 0); -}); + test('returns 0 when there are no content words', () => { + assert.equal(contentWordCount('is the at of'), 0); + }); -test("ignores case and punctuation", () => { - assert.equal(contentWordCount("Elephants, ELEPHANTS, elephants!"), 3); -}); + test('ignores case and punctuation', () => { + assert.equal(contentWordCount('Elephants, ELEPHANTS, elephants!'), 3); + }); -test("returns 0 for empty string", () => { - assert.equal(contentWordCount(""), 0); + test('returns 0 for empty string', () => { + assert.equal(contentWordCount(''), 0); + }); }); diff --git a/src/tests/analyzing/functionWordCount.test.ts b/src/tests/analyzing/functionWordCount.test.ts index ee69264..5e1c9e0 100644 --- a/src/tests/analyzing/functionWordCount.test.ts +++ b/src/tests/analyzing/functionWordCount.test.ts @@ -1,19 +1,21 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { functionWordCount } from "../../analyzing/functionWordCount.js"; +import test, { describe } from 'node:test'; +import assert from 'node:assert/strict'; +import { functionWordCount } from '../../analyzing/functionWordCount.js'; -test("counts function words in a normal sentence", () => { - assert.equal(functionWordCount("This is a test of the system"), 5); -}); +describe('functionWordCount', () => { + test('counts function words in a normal sentence', () => { + assert.equal(functionWordCount('This is a test of the system'), 5); + }); -test("returns 0 when there are no function words", () => { - assert.equal(functionWordCount("Elephants run fast"), 0); -}); + test('returns 0 when there are no function words', () => { + assert.equal(functionWordCount('Elephants run fast'), 0); + }); -test("ignores case and punctuation", () => { - assert.equal(functionWordCount("The, THE, the!"), 3); -}); + test('ignores case and punctuation', () => { + assert.equal(functionWordCount('The, THE, the!'), 3); + }); -test("returns 0 for empty string", () => { - assert.equal(functionWordCount(""), 0); + test('returns 0 for empty string', () => { + assert.equal(functionWordCount(''), 0); + }); }); diff --git a/src/tests/integration/prototype.test.ts b/src/tests/integration/prototype.test.ts new file mode 100644 index 0000000..ad64e1d --- /dev/null +++ b/src/tests/integration/prototype.test.ts @@ -0,0 +1,46 @@ +import { describe, it, before } from 'node:test'; +import assert from 'node:assert'; +// Adjust the path to reach src/index.ts from src/tests/integration/ +import { + extendStringPrototype, + transformations, + validations, + analyzing, + formatting, +} from '../../index'; + +describe('Global String Prototype Extension', () => { + before(() => { + extendStringPrototype(); + }); + + const categories = [ + { name: 'Transformations', module: transformations }, + { name: 'Validations', module: validations }, + { name: 'Analyzing', module: analyzing }, + { name: 'Formatting', module: formatting }, + ]; + + categories.forEach(({ name, module }) => { + describe(`${name} module`, () => { + Object.keys(module).forEach((fnName) => { + it(`should have ${fnName} attached to String.prototype`, () => { + assert.strictEqual(typeof (String.prototype as any)[fnName], 'function'); + }); + }); + }); + }); + + it('should ensure methods are non-enumerable', () => { + const str = 'test'; + const keys: string[] = []; + + // Using Object(str) to avoid the TS2407 error you encountered earlier + for (const key in Object(str)) { + keys.push(key); + } + + // Verify that a common method like 'camelCase' isn't leaking into loops + assert.strictEqual(keys.includes('camelCase'), false); + }); +}); diff --git a/src/transformations/index.ts b/src/transformations/index.ts index 187d8db..4af37e1 100644 --- a/src/transformations/index.ts +++ b/src/transformations/index.ts @@ -13,6 +13,8 @@ export { toSlug } from './toSlug'; export { truncateText } from './truncateText'; export { escapeHtml } from './escapeHTML'; export { maskSegment } from './maskSegment'; +export { deburr } from './deburr'; +export { splitChunks } from './splitChunks'; export { numberToText } from './numberToText/main'; export { reverseWordsInString } from './reverseWordsInString '; export { stringPermutations, stringPermutationsGenerator } from './stringPermutations'; @@ -34,31 +36,33 @@ import { truncateText } from './truncateText'; import { escapeHtml } from './escapeHTML'; import { maskSegment } from './maskSegment'; import { deburr } from './deburr'; +import { splitChunks } from './splitChunks'; import { numberToText } from './numberToText/main'; import { reverseWordsInString } from './reverseWordsInString '; import { stringPermutations, stringPermutationsGenerator } from './stringPermutations'; import { stringCombinations } from './stringCombinations'; export const transformations = { - camelCase, - capitalizeWords, - constantCase, - initials, - kebabCase, - pascalCase, - removeDuplicates, - removeSpecialChars, - removeWords, - snakeCase, - titleCase, - toSlug, - truncateText, - escapeHtml, - maskSegment, - deburr, - numberToText, - reverseWordsInString, - stringPermutations, - stringPermutationsGenerator, - stringCombinations + camelCase, + capitalizeWords, + constantCase, + initials, + kebabCase, + pascalCase, + removeDuplicates, + removeSpecialChars, + removeWords, + snakeCase, + titleCase, + toSlug, + truncateText, + escapeHtml, + maskSegment, + deburr, + splitChunks, + numberToText, + reverseWordsInString, + stringPermutations, + stringPermutationsGenerator, + stringCombinations, }; diff --git a/src/validations/index.ts b/src/validations/index.ts index 35754f4..4662f62 100644 --- a/src/validations/index.ts +++ b/src/validations/index.ts @@ -7,14 +7,15 @@ export { isURL } from './isURL'; export { isIPv4 } from './isIPv4'; export { isIPv6 } from './isIPv6'; export { isHexColor } from './isHexColor'; -export { isPalindrome } from './isPalindrome' -export {isLowerCase} from './isLowerCase'; -export {isUpperCase} from './isUpperCase'; +export { isPalindrome } from './isPalindrome'; +export { isLowerCase } from './isLowerCase'; +export { isUpperCase } from './isUpperCase'; export { isAlphabetic } from './isAlphabetic'; export { isAlphaNumeric } from './isAlphaNumeric'; -export { isAnagram } from './isAnagram'; +export { isAnagram } from './isAnagram'; export { isPanagram } from './isPanagram'; export { isMacAddress } from './isMacAddress'; +export { isTypeOf } from './isTypeOf'; import { isCoordinates } from './isCoordinates'; import { isDate } from './isDate'; @@ -33,6 +34,7 @@ import { isAlphaNumeric } from './isAlphaNumeric'; import { isAnagram } from './isAnagram'; import { isPanagram } from './isPanagram'; import { isMacAddress } from './isMacAddress'; +import { isTypeOf } from './isTypeOf'; export const validations = { isCoordinates, @@ -51,5 +53,6 @@ export const validations = { isAlphaNumeric, isAnagram, isPanagram, - isMacAddress + isMacAddress, + isTypeOf, };