From 2339c84866245bbfc3c58389f3298d43a55ae146 Mon Sep 17 00:00:00 2001 From: Abdelrahman Bayoumi Date: Mon, 26 Oct 2026 17:18:56 +0300 Subject: [PATCH] chore: clean up repository tooling and release checks --- .gitattributes | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 6 +- .github/ISSUE_TEMPLATE/feature_request.md | 6 +- .github/PULL_REQUEST_TEMPLATE.md | 31 +++----- .github/workflows/ci.yml | 4 +- .github/workflows/npm-publish.yml | 48 ++++++++---- .gitignore | 1 + .npmignore | 3 +- .prettierignore | 2 + AGENT.md | 90 ----------------------- AGENTS.md | 4 + CHANGELOG.md | 21 ++++++ CONTRIBUTING.md | 58 ++++++++------- README.md | 2 +- README_AR.md | 4 +- eslint.config.mjs | 20 +---- package.json | 8 +- src/scripts/tashfeer.ts | 56 +++++++------- tests/artifacts.test.ts | 58 +++++++++++++-- tests/constants.test.ts | 2 +- tests/scripts.test.ts | 8 +- tests/tree-shaking.test.ts | 52 ++++++++++++- vitest.config.mts | 13 ++-- 23 files changed, 269 insertions(+), 229 deletions(-) create mode 100644 .gitattributes delete mode 100644 AGENT.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1bba01b..7a98ee0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -26,9 +26,9 @@ assignees: '' يرجى توضيح البيئة التي كنت تستخدمها عند حدوث المشكلة. يمكن ذلك أن يشمل: -- بيئة التشغيل (مثل Node.js 22.x، أو المتصفح Chrome، أو Next.js / Vite) -- إصدار الحزمة arabic-services (مثل 1.1.0) -- نظام التشغيل (مثل Windows أو Linux أو macOS) +- بيئة التشغيل (مثل Node.js 22.x، أو المتصفح Chrome، أو Next.js / Vite) +- إصدار الحزمة arabic-services (مثل 1.1.0) +- نظام التشغيل (مثل Windows أو Linux أو macOS) ## السلوك المتوقع (Expected Behavior) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index aed82f0..d9d5972 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -30,6 +30,6 @@ assignees: '' هل تعتبر هذه الميزة ذات أهمية عالية أم منخفضة؟ يرجى تحديد درجة الأهمية بوضوح. -- [ ] عالية الأهمية -- [ ] متوسطة الأهمية -- [ ] منخفضة الأهمية +- [ ] عالية الأهمية +- [ ] متوسطة الأهمية +- [ ] منخفضة الأهمية diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3711ea2..c4bf893 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,27 +1,16 @@ -## وصف الفكرة (Feature Idea) +## ملخص التغيير (Summary) -يرجى وصف الفكرة أو الميزة الجديدة التي تود إضافتها إلى المشروع بوضوح هنا. يمكن أن تكون هذه الفكرة ميزة جديدة أو تحسين على ميزة موجودة. +اشرح المشكلة والتغيير الناتج، واربط الـIssue إن وجد. -## الهدف (Goal) +## التوافق (Compatibility) -ما الهدف من هذه الميزة؟ ما الفائدة التي ستقدمها للمستخدمين والمطورين؟ يرجى توضيح الفائدة والقيمة المضافة التي ستحققها هذه الفكرة. +وضّح أي تأثير على الـAPI أو القيم الافتراضية أو النتائج أو طرق استيراد الحزمة. -## سياق الاستخدام (Use Case) +## التحقق (Validation) -يرجى توضيح سيناريوهات الاستخدام التي تتطلب هذه الميزة. ما هي الحالات التي ستُستخدم فيها؟ وما الأوضاع التي تحتاج إلى هذه الميزة؟ +اذكر الأوامر والنتائج وأي فحوص لم تُنفذ. -## توصيف تفصيلي (Detailed Description) - -يرجى تقديم توصيف مفصل لكيفية عمل هذه الميزة والتفاصيل الفنية اللازمة لتنفيذها. يمكنك توضيح واجهة المستخدم المقترحة، وكيفية تفاعل المستخدم مع الميزة، وأي تأثيرات قد تكون لها على المشروع. - -## السياق الإضافي (Additional Context) - -هل هناك أي سياق إضافي يمكن أن يساعد في فهم الفكرة وأهميتها؟ يمكنك توضيح المزيد من التفاصيل التقنية أو وضع روابط لموارد مرتبطة بالفكرة. - -## الأهمية (Priority) - -هل تعتبر هذه الميزة ذات أهمية عالية أم منخفضة؟ يرجى تحديد درجة الأهمية بوضوح. - -- [ ] عالية الأهمية -- [ ] متوسطة الأهمية -- [ ] منخفضة الأهمية +- [ ] `npm run format:check` +- [ ] `npm run lint` +- [ ] `npm test` (source tests, build, artifact tests) +- [ ] تحديث التوثيق و`[Unreleased]` عند الحاجة، مع الحفاظ على الإصدارات السابقة. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f71cd5a..0d66493 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,5 +32,5 @@ jobs: - name: Install dependencies run: npm ci - - name: Run lint, build, and tests - run: npm run lint && npm run build && npm test + - name: Check formatting, lint, and tests + run: npm run format:check && npm run lint && npm test diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index b93da67..ef0428d 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,48 +1,68 @@ name: Publish Package to npmjs -# Trigger this workflow when a release is published on: release: types: [published] workflow_dispatch: + inputs: + tag: + description: 'Existing release tag to publish (for example, v1.2.0)' + required: true + type: string jobs: build: - # Specify the runner environment runs-on: ubuntu-latest permissions: contents: write id-token: write + env: + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} steps: - # Step 1: Check out the repository's code - name: Check out repository uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 - # Step 2: Set up Node.js environment - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '24' # Node 24 includes npm 11+ required for OIDC Trusted Publishing + node-version: '24' + + - name: Verify release tag and version + run: | + node <<'NODE' + const { execFileSync } = require('node:child_process'); + const { version } = require('./package.json'); + const tag = process.env.RELEASE_TAG; + if (tag !== `v${version}`) { + throw new Error('Release tag must match the package.json version'); + } + const commit = ref => execFileSync('git', ['rev-parse', '--verify', ref], { encoding: 'utf8' }).trim(); + if (commit(`refs/tags/${tag}^{commit}`) !== commit('HEAD')) { + throw new Error('The checked-out commit must match the release tag'); + } + NODE - # Step 3: Install dependencies using npm ci - name: Install dependencies run: npm ci - # Step 4: Build package and bundles - - name: Build package and bundles - id: build - run: npm run build + - name: Check formatting, lint, and tests + id: verify + run: npm run format:check && npm run lint && npm test - # Step 5: Publish the package to npm using Trusted Publishing (OIDC) - name: Publish to npm - run: npm publish --provenance --access public + # Verification already built and tested these artifacts; local publishing keeps prepublishOnly. + run: npm publish --provenance --access public --ignore-scripts - # Step 6: Upload bundle assets to release - name: Upload bundle assets to release - if: always() && steps.build.outcome == 'success' + if: always() && steps.verify.outcome == 'success' uses: softprops/action-gh-release@v2 with: + tag_name: ${{ env.RELEASE_TAG }} + fail_on_unmatched_files: true files: | dist/umd/arabic-services.umd.js dist/umd/arabic-services.umd.js.map diff --git a/.gitignore b/.gitignore index 14abcc6..e12eb7c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ Thumbs.db # Test results and coverage coverage/ +.tree-shaking-*/ *.test.ts.snap # Environment variables diff --git a/.npmignore b/.npmignore index 5dc00f7..0f1a9c6 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,6 @@ # Testing -__tests__/ +tests/ coverage/ -jest.config.js *.test.ts *.test.ts.snap diff --git a/.prettierignore b/.prettierignore index 9b1c8b1..1684426 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,3 @@ /dist +CHANGELOG.md +package-lock.json diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index fca7945..0000000 --- a/AGENT.md +++ /dev/null @@ -1,90 +0,0 @@ -# Arabic Services Agent Instructions - -This repository contains `arabic-services`: a zero-runtime-dependency TypeScript utility library for processing, transforming, and manipulating Arabic text in Node.js and modern web browsers. - ---- - -## General Rules - -- **Inspect First**: Inspect existing code, docs, tests, and configuration before changing files. -- **Search Before Adding**: Search the existing codebase first; reuse nearby utilities, constants, regex helpers, and patterns in `src/constants/` and `src/utils/`. -- **Zero Runtime Dependencies**: The library must maintain **zero runtime dependencies** (`"dependencies"` in `package.json` must remain empty). All operations must rely strictly on standard TypeScript/JavaScript algorithms and regular expressions. -- **Minimum Safe Scope**: Prefer small, focused, reusable changes that solve the request with the lowest safe scope. -- **Never Modify Without Instructions**: Do not modify files for analysis, explanation, or review-only requests. When implementation is requested, briefly explain the root cause, assumptions, and short plan before editing. -- **Backwards Compatibility**: Existing public function signatures and return types must not break existing npm consumers without explicit major-version discussion. -- **Explain Every Change**: For every change, explain what changed, why, risks or side effects, and how to verify it. -- **Safe Commands Only**: Never run destructive Git commands (force push, hard reset, cleanups) or mass file deletions without explicit user approval. -- **Package Security**: Never hardcode, commit, or print npm publishing tokens (`NODE_AUTH_TOKEN`), CI credentials, or local environment files. - ---- - -## Project Map - -- `README.md` & `README_AR.md`: Library entry points, examples, and feature showcases in English and Arabic. -- `CHANGELOG.md`: Release notes following standard semver, documenting both Gregorian and Hijri calendar dates. -- `CONTRIBUTING.md`: Contribution guide and Conventional Commits guidelines. -- `src/index.ts`: Main entry point and public export surface (`ArabicServices`). -- `src/options.ts`: Public configuration types and interfaces (e.g., `OldArabicOptions`). -- `src/constants/`: - - `arabic-letters.ts`: Arabic alphabet mappings, variations, and substitution dictionaries. - - `tashkeel.ts`: Diacritics (Harakat, Tanween, Quranic sequential tanween). - - `banned-words.ts`: Word lists used in obfuscation/tashfeer algorithms. -- `src/scripts/`: - - `tashkeel.ts`: Tashkeel and Tatweel removal (`removeTashkeel`, `removeTatweel`). - - `old-arabic.ts`: Dotless Old Arabic script transformation (`toOldArabic`). - - `tashfeer.ts`: Visual obfuscation and banned-word encoding (`tashfeer`, `tashfeerBannedWords`, `toOldArabicAndTashfeerBannedWords`). - - `affixes.ts`: Prefix and suffix stripping heuristic (`removeArabicAffixes`). - - `letters.ts`: Conversion of words to pronounced letter names (`wordToLetters`). - - `index.ts`: Barrel file re-exporting all script operations. -- `src/utils/`: - - `string.util.ts`: Internal string manipulation and regex utility functions. -- `tests/`: - - `scripts.test.ts`: Vitest unit tests covering all text operations. - - `constants.test.ts`: Sanity checks for character mappings and constants. - - `artifacts.test.ts`: Module integrity tests for CJS, ESM, and browser UMD bundles. - - `edge-cases.test.ts`: Boundary values, orthography variations, and ReDoS resilience tests. - - `tree-shaking.test.ts`: Verification of named exports and dead code elimination. -- `webpack.config.cjs`: Bundler configuration generating UMD (`dist/umd/`) and ESM (`dist/esm/`) bundles for browser distribution. -- `tsconfig.json`: TypeScript compiler configuration targeting ES5 / CommonJS for the main npm distribution. - ---- - -## Arabic NLP & Domain Rules - -- **Pure Functions**: Every utility function must be pure, deterministic, and free of side effects. Input strings must never be mutated. -- **Unicode Code Point Integrity**: Arabic characters have unique Unicode points for letters, ligatures, Tatweel (`\u0640`), Tashkeel (`\u064B`-`\u0652`, `\u0670`, sequential tanween, etc.). Avoid naive character splitting that breaks multi-byte or combining marks; always respect Unicode boundaries. -- **Regular Expressions & ReDoS**: Regular expressions must be tested against catastrophic backtracking (ReDoS). Prefer pre-compiled regexes or Set/Dictionary lookups for character replacement rather than dynamic nested patterns. -- **Dual Runtime Support**: The core library runs in both Node.js and web browsers. Never import Node-specific built-ins (`fs`, `path`, `crypto`, `os`) inside `src/`. -- **Test-Driven Changes**: Every bug fix, edge case, or new feature in `src/scripts/` must be accompanied by unit tests in `tests/scripts.test.ts` or `tests/edge-cases.test.ts`. - ---- - -## Build, Test, and Development Commands - -- `npm run build`: Compile TypeScript into `dist/` (generates `.js` and `.d.ts`). -- `npm test`: Run Vitest test suite and generate coverage report. **Note**: Run `npm run build` before `npm test`, as tests import from the compiled `./dist` directory. -- `npm test -- -t "test name"`: Run a specific test case by name. -- `npm run dev`: Quick execution runner using `ts-node src/index.ts`. -- `npm run bundle`: Run Webpack to bundle production UMD (`arabic-services.umd.js`) and ESM (`arabic-services.esm.js`) files for browser/CDN distribution. -- `npm run lint`: Run ESLint to check code quality. -- `npm run lint:fix`: Auto-fix ESLint errors. -- `npm run format`: Format files using Prettier. -- `npm run clean`: Clean the `dist/` build directory. - ---- - -## Proposals, Reviews, And Devil's Advocate - -- Do not present only the happy path. -- Challenge assumptions and separate verified codebase evidence from guesses. -- Include counterarguments against proposed architectures or third-party tooling. -- Compare simpler, zero-dependency alternatives when available. -- Highlight performance implications on string parsing, memory allocation, and bundle size. -- Prefer concise, actionable wording suitable for PR reviews or GitHub issues. - ---- - -## Commit & PR Conventions - -- Commit messages and PR titles must follow `CONTRIBUTING.md` using the Conventional Commits specification (`feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `chore:`). -- Document new features, bug fixes, or tooling updates under `## [Unreleased]` in `CHANGELOG.md` following Keep a Changelog conventions. diff --git a/AGENTS.md b/AGENTS.md index 01d0a8a..e5e09a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,9 @@ A zero-runtime-dependency TypeScript library for Arabic text in Node.js and brow - Respect Unicode code points, UTF-16 indices and combining marks. Make lossy normalization explicit. - Do not describe affix stripping as morphological analysis or visual obfuscation as encryption. - Add focused regression tests for behavior changes, including mixed scripts, empty input and adversarial regex cases where relevant. +- Preserve intentional randomness in visual obfuscation; make randomized tests reproducible without changing production behavior. - Update affected English/Arabic docs and release notes. Follow existing contribution conventions. +- Use Conventional Commits and record notable changes under `[Unreleased]` in `CHANGELOG.md`. - Document actual APIs and commands; clearly label proposed features. Do not invent capabilities or performance claims. - Verify third-party licenses; do not copy implementations or expose secrets/private input. - Do not manually edit generated output or lockfiles unless requested. Ask before commits, pushes, publishing or destructive operations. @@ -24,7 +26,9 @@ A zero-runtime-dependency TypeScript library for Arabic text in Node.js and brow ## Validation - Use installed tools; start with the narrowest relevant lint, type or test check, then broaden when justified. Current scripts include `npm run lint` and `npm test`. +- Use `npm run test:unit` for source tests without a build. `npm test` runs source tests, builds once, then runs `test:artifacts` against compiled files and package contents. - For export/build changes, verify fresh package artifacts and declarations through CJS, ESM and browser consumers; source tests alone are insufficient. Inspect build/cleanup scripts first. - Check bundle size and tree-shaking when changing imports, exports or dependencies; measure before claiming improvements. - For documentation-only changes, check formatting and references without building. Avoid autofix during read-only reviews. +- Preserve published changelog entries and both Gregorian and Hijri dates; add new release notes under `[Unreleased]` only. Exclude `CHANGELOG.md` from bulk formatting. - Review the final diff. Report exact checks, results and skipped validation; distinguish evidence from assumptions. diff --git a/CHANGELOG.md b/CHANGELOG.md index 99266ef..0c3d1de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,32 @@ - Integration tests for CJS, ESM, and browser UMD artifacts. - Arabic NLP boundary and ReDoS resilience test suites. - Direct CDN links (jsdelivr & unpkg) and GitHub release bundle guide in documentation. +- JavaScript source maps and TypeScript declaration maps for CommonJS and native ESM builds, plus source maps for UMD and ESM browser bundles ([PR #40](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/pull/40)). +- Original TypeScript source files in the npm package and companion browser bundle maps in GitHub release uploads ([PR #40](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/pull/40)). +- Regression tests for invalid obfuscation levels, Arabic character variants, and out-of-range `setCharAt` indices ([PR #36](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/pull/36), [PR #37](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/pull/37)). + +- Automated checks for npm package contents, source/declaration map references, and real tree shaking of built ESM consumers. ### Changed - Modularized monolithic `src/scripts/scripts.ts` into focused single-responsibility files (`tashkeel.ts`, `old-arabic.ts`, `tashfeer.ts`, `affixes.ts`, `letters.ts`) while keeping public exports and backward compatibility completely unchanged. - Relocated standalone browser demo to `examples/browser/index.html`. - Unified CI workflow across Node.js 22.x and 24.x with ESLint. +- Expanded lint commands from `src/` to all non-ignored repository files. +- Declared ES2020 and DOM type libraries in the TypeScript configuration to resolve missing API type diagnostics in editors ([PR #38](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/pull/38)). +- Updated the npm publishing workflow to Node.js 24. +- Consolidated agent instructions in `AGENTS.md`. + +- Separated source and artifact test commands; `npm test` now builds once between the two suites. +- Standardized LF line endings, refreshed contribution and PR guidance, and clarified visual-obfuscation terminology. +- Release publishing now validates the tag/version and runs formatting, lint, and tests before publishing the verified build. + +### Fixed +- Prevent invalid levels from hanging `tashfeer`, `tashfeerBannedWords`, and `toOldArabicAndTashfeerBannedWords`: non-number values throw `TypeError`; negative, fractional, and non-finite numbers throw `RangeError`. Random-index generation handles empty input and caps the requested count at the word length ([#35](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/issues/35), [PR #36](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/pull/36)). +- Attempt browser bundle uploads after a successful build even if npm publishing fails ([commit 598035a](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/commit/598035a)). + +- Removed the unused `dev` command, which depended on the uninstalled `ts-node` runner. + +[Full Changelog](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/compare/v1.1.0...main) ## [1.1.0](https://github.com/Seen-Arabic/Arabic-Services-JavaScript/releases/tag/v1.1.0) - 2026-10-14 (03 Jumada al-Awwal 1448) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c82ae8a..2d264a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,18 +7,18 @@ Thank you for your interest in contributing to Arabic Services! This library is ## 🛠️ Getting Started 1. **Fork and clone** the repository: - ```bash - git clone https://github.com//Arabic-Services-JavaScript.git - cd Arabic-Services-JavaScript - ``` + ```bash + git clone https://github.com//Arabic-Services-JavaScript.git + cd Arabic-Services-JavaScript + ``` 2. **Install dependencies**: - ```bash - npm install - ``` -3. **Verify tests pass**: - ```bash - npm test - ``` + ```bash + npm ci + ``` +3. **Verify the project**: + ```bash + npm test + ``` --- @@ -44,7 +44,7 @@ We follow the [Conventional Commits](https://www.conventionalcommits.org/) speci ``` feat: add sequential tanween to tashkeel list -fix: handle word prefixes correctly in removeAffixes +fix: handle word prefixes correctly in removeArabicAffixes docs: clarify browser bundle usage via CDN test: add test cases for dotless noon transformation chore: update devDependencies to latest versions @@ -57,25 +57,33 @@ chore: update devDependencies to latest versions Before submitting a Pull Request, please ensure: 1. **Tests pass**: - ```bash - npm test - ``` - Add unit tests in `tests/scripts.test.ts` for any new logic or bug fix. + ```bash + npm test + ``` + `npm test` runs source tests, builds the package once, then checks the compiled artifacts. + Use `npm run test:unit` (or `npm run test:watch`) during development without building. + After an existing build, use `npm run test:artifacts` to check CJS/ESM/UMD consumers, npm package contents, source maps, and real bundle tree shaking. It reports minified sizes for a single-function consumer and the full object; results depend on the imports and installed bundler version. + Add focused source regression tests for new logic or bug fixes, and artifact tests for packaging or export changes. 2. **Code is clean and formatted**: - ```bash - npm run lint - npm run format - ``` -3. **TypeScript builds cleanly**: - ```bash - npm run build - ``` + ```bash + npm run lint + npm run format:check + ``` + Use `npm run format` to apply formatting when needed. Git and the formatter use LF line endings; the changelog and generated lockfile are excluded from bulk formatting. +3. **Release notes are current**: Add notable changes under `[Unreleased]` in `CHANGELOG.md`. Preserve published entries, their formatting, and Gregorian and Hijri dates. + +## Release verification + +The publishing workflow checks out the release tag, verifies it matches `v` plus the `package.json` version, and runs formatting, lint, source tests, a fresh build, and artifact tests before publishing. Manual workflow runs require the existing tag as input. + +CI publishes those verified artifacts with [`--ignore-scripts`](https://docs.npmjs.com/cli/v11/using-npm/config/#ignore-scripts) to avoid rebuilding them. A local `npm publish` still runs `prepublishOnly` to build first. Publishing and release creation require maintainer authorization. --- ## 🕌 Arabic Text & NLP Guidelines When contributing text processing logic: -- **Pure Functions**: Keep functions deterministic, side-effect free, and performant. + +- **Text Operations**: Keep library logic independent of environment state. Preserve intentional randomness in visual obfuscation and make its tests reproducible. Visual obfuscation does not provide cryptographic protection. - **Unicode Awareness**: Arabic diacritics (Harakat/Tashkeel) and special characters (Tatweel, Wasla, Madda) have specific Unicode codepoints. Make sure regex patterns and character checks account for them correctly. - **Dual Runtime Support**: Ensure changes work seamlessly in both Node.js and web browsers. Avoid importing Node-only packages into `src/`. diff --git a/README.md b/README.md index 8ba9230..f82dac4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Can be used in Node.js and the browser. - + diff --git a/README_AR.md b/README_AR.md index c9bca3a..9696cd5 100644 --- a/README_AR.md +++ b/README_AR.md @@ -23,7 +23,7 @@ - + @@ -76,7 +76,7 @@ npm install arabic-services #### نظام ES Module ودعم Tree Shaking (موصى به): -استيراد الدوال التي تحتاجها فقط لتقليل حجم الحزمة إلى أدنى حد (أقل من 400 بايت للدالة): +استيراد الدوال التي تحتاجها فقط لتقليل حجم الحزمة: ```javascript import { removeTashkeel, removeTatweel, toOldArabic } from 'arabic-services'; diff --git a/eslint.config.mjs b/eslint.config.mjs index 14522b8..d9f72ec 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,20 +1,6 @@ import tseslint from 'typescript-eslint'; import prettierRecommended from 'eslint-plugin-prettier/recommended'; -export default tseslint.config( - ...tseslint.configs.recommended, - prettierRecommended, - { - rules: { - 'prettier/prettier': [ - 'error', - { - endOfLine: 'auto', - }, - ], - }, - }, - { - ignores: ['dist/**', 'coverage/**', 'node_modules/**', '*.config.*'], - } -); +export default tseslint.config(...tseslint.configs.recommended, prettierRecommended, { + ignores: ['dist/**', 'coverage/**', 'node_modules/**', '.tree-shaking-*/**', '*.config.*'], +}); diff --git a/package.json b/package.json index 8be1cfd..8a1aba0 100644 --- a/package.json +++ b/package.json @@ -29,15 +29,17 @@ "./package.json": "./package.json" }, "scripts": { - "dev": "ts-node src/index.ts", "build:cjs": "tsc -p tsconfig.json", "build:esm": "tsc -p tsconfig.esm.json && node -e \"fs.writeFileSync('dist/esm/package.json', JSON.stringify({ type: 'module', sideEffects: false }))\"", "build": "npm run clean && npm run build:cjs && npm run build:esm && npm run bundle", "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"", - "test": "vitest run --coverage", + "test": "npm run test:unit && npm run build && npm run test:artifacts", + "test:unit": "vitest run --coverage", + "test:artifacts": "vitest run --mode artifacts", "test:watch": "vitest", "prepublishOnly": "npm run build", - "format": "npx prettier --write . --ignore-path .gitignore", + "format": "prettier --write . --ignore-path .gitignore --ignore-path .prettierignore", + "format:check": "prettier --check . --ignore-path .gitignore --ignore-path .prettierignore", "lint": "eslint . --quiet", "lint:fix": "eslint . --fix", "bundle": "webpack" diff --git a/src/scripts/tashfeer.ts b/src/scripts/tashfeer.ts index 5c5477b..5745590 100644 --- a/src/scripts/tashfeer.ts +++ b/src/scripts/tashfeer.ts @@ -12,8 +12,8 @@ import { removeArabicAffixes } from './affixes.js'; import { toOldArabic } from './old-arabic.js'; /** - * Validates the encryption level to guarantee bounded execution and prevent infinite loops. - * @param {unknown} level - The input encryption level. + * Rejects invalid obfuscation levels before random-index generation. + * @param {unknown} level - The input obfuscation level. */ function validateTashfeerLevel(level: unknown): void { if (typeof level !== 'number') { @@ -25,11 +25,11 @@ function validateTashfeerLevel(level: unknown): void { } /** - * Calculates the encryption level based on the input level and the length of the word. + * Calculates the obfuscation level based on the input level and the length of the word. * - * @param {number} level - The input encryption level. - * @param {number} wordLength - The length of the word to be encrypted. - * @returns {number} The calculated encryption level. + * @param {number} level - The input obfuscation level. + * @param {number} wordLength - The length of the word to be obfuscated. + * @returns {number} The calculated obfuscation level. */ function calculateEncryptionLevel(level: number, wordLength: number): number { if (wordLength <= 0) { @@ -51,7 +51,7 @@ function calculateEncryptionLevel(level: number, wordLength: number): number { } /** - * Generates a list of random indexes for encryption. + * Generates a list of random indexes for obfuscation. * @param {number} numOfIndexesToNeeded - The number of random indexes to generate. * @param {number} wordLength - The length of the word. * @returns {number[]} An array of random indexes. @@ -76,10 +76,10 @@ function getRandomIndexes(numOfIndexesToNeeded: number, wordLength: number): num } /** - * Processes the word for encryption using random indexes. - * @param {string} word - The word to be encrypted. - * @param {number[]} randomIndexes - The random indexes for encryption. - * @returns {string} The encrypted word. + * Processes the word for obfuscation using random indexes. + * @param {string} word - The word to be obfuscated. + * @param {number[]} randomIndexes - The random indexes for obfuscation. + * @returns {string} The obfuscated word. */ function tashfeerWord(word: string, randomIndexes: number[]): string { let outputWord = ''; @@ -98,7 +98,7 @@ function tashfeerWord(word: string, randomIndexes: number[]): string { outputWord += 'ـ'; } - // Add the replacement letter to the encrypted word + // Add the replacement letter to the obfuscated word outputWord += letter; } else { // If the character doesn't need to be replaced, add it as it is @@ -135,27 +135,27 @@ function tashfeerCharacter(character: string): string { } /** - * Performs tashfeer encryption on a given word. - * @param {string} word - The input word to be encrypted. - * @param {number} [level=0] - The encryption level (default is 0). - * @returns {string} The encrypted word. + * Performs tashfeer obfuscation on a given word. + * @param {string} word - The input word to be obfuscated. + * @param {number} [level=0] - The obfuscation level (default is 0). + * @returns {string} The obfuscated word. */ function tashfeerHandler(word: string, level: number = 0): string { const wordLength = word.length; - // Calculate the encryption level based on the input level and word length + // Calculate the obfuscation level based on the input level and word length const n = calculateEncryptionLevel(level, wordLength); - // Generate a list of random indexes for encryption + // Generate a list of random indexes for obfuscation const randomIndexes = getRandomIndexes(n, wordLength); - // Process the word for encryption using random indexes + // Process the word for obfuscation using random indexes const outputWord = tashfeerWord(word, randomIndexes); return outputWord; } /** - * Performs tashfeer encryption on a given sentence. - * @param {string} sentence - The input sentence to be encrypted. - * @param {number} [levelOfTashfeer=1] - The encryption level. - * @returns {string} The encrypted sentence. + * Performs tashfeer obfuscation on a given sentence. + * @param {string} sentence - The input sentence to be obfuscated. + * @param {number} [levelOfTashfeer=1] - The obfuscation level. + * @returns {string} The obfuscated sentence. */ export function tashfeer(sentence: string, levelOfTashfeer: number = 1): string { validateTashfeerLevel(levelOfTashfeer); @@ -196,12 +196,12 @@ function checkIfBannedWord(string: string): boolean { } /** - * Performs tashfeer encryption on a given sentence, but only for words that are considered "banned" words. + * Performs tashfeer obfuscation on a given sentence, but only for words that are considered "banned" words. * Banned words are determined based on a predefined similarity ratio. * - * @param {string} sentence - The input sentence to be encrypted. - * @param {number} [levelOfTashfeer=2] - The encryption level (default is 2). - * @returns {string} The encrypted sentence with tashfeer applied to banned words. + * @param {string} sentence - The input sentence to be obfuscated. + * @param {number} [levelOfTashfeer=2] - The obfuscation level (default is 2). + * @returns {string} The obfuscated sentence with tashfeer applied to banned words. */ export function tashfeerBannedWords(sentence: string, levelOfTashfeer: number = 2): string { validateTashfeerLevel(levelOfTashfeer); @@ -220,7 +220,7 @@ export function tashfeerBannedWords(sentence: string, levelOfTashfeer: number = /** * Converts a sentence to old Arabic script and applies tashfeer to banned words. * @param {string} sentence - The input sentence. - * @param {number} [levelOfTashfeer=2] - The encryption level (default is 2). + * @param {number} [levelOfTashfeer=2] - The obfuscation level (default is 2). * @returns {string} The processed sentence. */ export function toOldArabicAndTashfeerBannedWords(sentence: string, levelOfTashfeer: number = 2): string { diff --git a/tests/artifacts.test.ts b/tests/artifacts.test.ts index 80980d5..c6d7d0d 100644 --- a/tests/artifacts.test.ts +++ b/tests/artifacts.test.ts @@ -1,4 +1,4 @@ -import { execSync } from 'node:child_process'; +import { execFileSync, execSync } from 'node:child_process'; import * as fs from 'node:fs'; import { createRequire } from 'node:module'; import * as path from 'node:path'; @@ -43,7 +43,7 @@ describe('Package Artifacts & Module Integrity', () => { it('should support standalone CommonJS require() in isolated Node.js process', () => { const script = ` - const { removeTashkeel, toOldArabic, ArabicServices } = require('./dist'); + const { removeTashkeel, toOldArabic, ArabicServices } = require('arabic-services'); const res1 = removeTashkeel('مَرْحَبًا'); const res2 = ArabicServices.removeTatweel('رائـــــع'); console.log(JSON.stringify({ res1, res2, hasToOldArabic: typeof toOldArabic === 'function' })); @@ -60,7 +60,7 @@ describe('Package Artifacts & Module Integrity', () => { it('should support standalone ES Module import in isolated Node.js process', () => { const script = ` - import { removeTashkeel, toOldArabic, ArabicServices } from './dist/esm/index.js'; + import { removeTashkeel, toOldArabic, ArabicServices } from 'arabic-services'; const res1 = removeTashkeel('مَرْحَبًا'); const res2 = ArabicServices.removeTatweel('رائـــــع'); console.log(JSON.stringify({ res1, res2, hasToOldArabic: typeof toOldArabic === 'function' })); @@ -105,7 +105,7 @@ describe('Package Artifacts & Module Integrity', () => { it('should support in-process CommonJS loading via Node.js createRequire()', () => { const nodeRequire = createRequire(import.meta.url); - const cjsDist = nodeRequire('../dist'); + const cjsDist = nodeRequire('arabic-services'); expect(cjsDist).toBeDefined(); expect(typeof cjsDist.removeTashkeel).toBe('function'); @@ -120,9 +120,57 @@ describe('Package Artifacts & Module Integrity', () => { expect(typeof window).toBe('undefined'); expect(typeof document).toBe('undefined'); - const dist = await import('../dist'); + const dist = await import('arabic-services'); expect(dist.removeTashkeel('فَحَصَ')).toBe('فحص'); expect(dist.removeTatweel('مـلـف')).toBe('ملف'); expect(dist.toOldArabic('بَيْت')).toBe('ٮىٮ'); }); + + it('should package entry points and source maps with resolvable original sources', () => { + const npmCli = process.env.npm_execpath; + if (!npmCli) throw new Error('Run package checks through npm run test:artifacts'); + const [pack] = JSON.parse( + execFileSync(process.execPath, [npmCli, 'pack', '--dry-run', '--ignore-scripts', '--json'], { + cwd: rootDir, + encoding: 'utf8', + timeout: 10_000, + }), + ) as [{ files: { path: string }[] }]; + const files = new Set(pack.files.map((file) => file.path)); + for (const entry of [packageJson.main, packageJson.module, packageJson.types, packageJson.unpkg]) { + expect(files.has(entry.replace(/^\.\//, '')), `Packaged entry point: ${entry}`).toBe(true); + } + expect([...files].some((file) => /^(tests|coverage|\.github)\//.test(file))).toBe(false); + for (const source of fs.readdirSync(path.join(rootDir, 'src'), { recursive: true })) { + if (typeof source === 'string' && source.endsWith('.ts')) { + expect(files.has(`src/${source.replaceAll(path.sep, '/')}`), `Packaged source: ${source}`).toBe(true); + } + } + + for (const file of files) { + if (!file.startsWith('dist/') || (!file.endsWith('.js') && !file.endsWith('.d.ts'))) continue; + const output = fs.readFileSync(path.join(rootDir, file), 'utf8'); + const mapName = output.match(/\/\/# sourceMappingURL=(.+)/)?.[1].trim(); + if (!mapName) throw new Error(`Missing source map reference: ${file}`); + const mapPath = path.posix.join(path.posix.dirname(file), mapName); + expect(files.has(mapPath), `Packaged map: ${mapPath}`).toBe(true); + const map = JSON.parse(fs.readFileSync(path.join(rootDir, mapPath), 'utf8')) as { + version: number; + file: string; + sourceRoot?: string; + sources: string[]; + sourcesContent?: (string | null)[]; + mappings: string; + }; + expect(map.version).toBe(3); + expect(map.mappings.length, `Mappings: ${mapPath}`).toBeGreaterThan(0); + expect(path.posix.join(path.posix.dirname(mapPath), map.file)).toBe(file); + expect(map.sources.length, `Original sources: ${mapPath}`).toBeGreaterThan(0); + map.sources.forEach((source, index) => { + if (typeof map.sourcesContent?.[index] === 'string') return; + const sourcePath = path.posix.join(path.posix.dirname(mapPath), map.sourceRoot ?? '', source); + expect(files.has(sourcePath), `Original source for ${mapPath}: ${sourcePath}`).toBe(true); + }); + } + }, 15_000); }); diff --git a/tests/constants.test.ts b/tests/constants.test.ts index cf61f69..7867488 100644 --- a/tests/constants.test.ts +++ b/tests/constants.test.ts @@ -1,4 +1,4 @@ -import { ArabicServices } from '../dist'; +import { ArabicServices } from '../src'; describe('STANDARD_LETTERS', () => { it('should have array length of 34', () => { diff --git a/tests/scripts.test.ts b/tests/scripts.test.ts index 419467e..811a330 100644 --- a/tests/scripts.test.ts +++ b/tests/scripts.test.ts @@ -1,4 +1,4 @@ -import { ArabicServices } from '../dist'; +import { ArabicServices } from '../src'; describe('#removeTashkeel()', () => { describe('Test with Poetry Text', () => { @@ -125,7 +125,7 @@ describe('#toOldArabic()', () => { }); describe('#toOldArabicAndTashfeerBannedWords()', () => { - it('should perform tashfeer encryption on banned words only and convert the rest to old arabic script', () => { + it('should perform tashfeer obfuscation on banned words only and convert the rest to old arabic script', () => { const sentence = 'جيش العدو يقتل الأطفال'; const result = ArabicServices.toOldArabicAndTashfeerBannedWords(sentence); expect(result).not.toEqual(sentence); @@ -324,7 +324,7 @@ describe('#similarityScore', () => { }); describe('#tashfeerBannedWords', () => { - it('should perform tashfeer encryption on banned words only', () => { + it('should perform tashfeer obfuscation on banned words only', () => { const sentence = 'جيش العدو يقتل الأطفال'; const result = ArabicServices.tashfeerBannedWords(sentence); expect(result).not.toEqual(sentence); @@ -334,7 +334,7 @@ describe('#tashfeerBannedWords', () => { expect(result).not.toMatch(/يقتل/); }); - it('should not perform tashfeer encryption on non-banned words', () => { + it('should not perform tashfeer obfuscation on non-banned words', () => { const sentence = 'هذه جملة غير مشفرة'; const result = ArabicServices.tashfeerBannedWords(sentence); expect(result).toEqual(sentence); diff --git a/tests/tree-shaking.test.ts b/tests/tree-shaking.test.ts index 222ffc7..b6bbe67 100644 --- a/tests/tree-shaking.test.ts +++ b/tests/tree-shaking.test.ts @@ -1,4 +1,8 @@ import { execSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as vm from 'node:vm'; +import webpack from 'webpack'; import ArabicServicesDefault, { ArabicServices, removeTashkeel, @@ -6,7 +10,7 @@ import ArabicServicesDefault, { toOldArabic, TASHKEEL, setCharAt, -} from '../dist'; +} from '../dist/esm/index.js'; describe('Tree Shaking & Named Exports', () => { it('should support direct named export of functions', () => { @@ -44,4 +48,50 @@ describe('Tree Shaking & Named Exports', () => { expect(result.hasArabicServices).toBe('object'); expect(result.sampleResult).toBe('مرحبا'); }); + + it('should omit unused APIs when bundling a single named import', async () => { + const tempDir = fs.mkdtempSync(path.join(path.resolve(__dirname, '..'), '.tree-shaking-')); + const entryPoint = JSON.stringify('arabic-services'); + try { + fs.writeFileSync( + path.join(tempDir, 'single.mjs'), + `import { removeTashkeel } from ${entryPoint}; globalThis.result = removeTashkeel('مَرْحَبًا');`, + ); + fs.writeFileSync( + path.join(tempDir, 'full.mjs'), + `import { ArabicServices } from ${entryPoint}; globalThis.result = ArabicServices;`, + ); + await new Promise((resolve, reject) => { + webpack( + { + mode: 'production', + entry: { single: path.join(tempDir, 'single.mjs'), full: path.join(tempDir, 'full.mjs') }, + output: { path: path.join(tempDir, 'output'), filename: '[name].js' }, + devtool: false, + }, + (error, stats) => { + if (error) reject(error); + else if (!stats || stats.hasErrors()) + reject(new Error(stats?.toString({ all: false, errors: true }))); + else resolve(); + }, + ); + }); + const singleBundle = fs.readFileSync(path.join(tempDir, 'output/single.js'), 'utf8'); + const fullBundle = fs.readFileSync(path.join(tempDir, 'output/full.js'), 'utf8'); + const singleContext = { result: undefined }; + const fullContext = { result: undefined }; + vm.runInNewContext(singleBundle, singleContext); + vm.runInNewContext(fullBundle, fullContext); + expect(singleContext.result).toBe('مرحبا'); + expect(fullContext.result).toHaveProperty('tashfeerBannedWords', expect.any(Function)); + expect(singleBundle).not.toContain('tashfeerBannedWords'); + expect(Buffer.byteLength(singleBundle)).toBeLessThan(Buffer.byteLength(fullBundle) / 2); + console.info( + `Minified consumer bundles: single=${Buffer.byteLength(singleBundle)} B, full=${Buffer.byteLength(fullBundle)} B`, + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }, 15_000); }); diff --git a/vitest.config.mts b/vitest.config.mts index 9c9e720..bc61a41 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,18 +1,17 @@ import { defineConfig } from 'vitest/config'; -import path from 'node:path'; -export default defineConfig({ +const artifactTests = ['tests/artifacts.test.ts', 'tests/tree-shaking.test.ts']; + +export default defineConfig(({ mode }) => ({ test: { globals: true, environment: 'node', - include: ['tests/**/*.test.ts'], + include: mode === 'artifacts' ? artifactTests : ['tests/**/*.test.ts'], + exclude: mode === 'artifacts' ? [] : artifactTests, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], include: ['src/**/*.ts'], }, - alias: { - '../dist': path.resolve(import.meta.dirname, './src'), - }, }, -}); +}));