diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e17f52f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - run: npm ci + - run: npm run typecheck + - run: npm test + - run: npm run build + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + # The library claims engines >= 16; prove it by exercising the built + # ESM/CJS output on every supported major (the TS test suite itself needs + # Node 23+ for type stripping, so old majors smoke-test dist instead). + compat: + needs: test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [16, 18, 20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: node scripts/smoke.mjs + - run: node scripts/smoke.cjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c4a1b72 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +# Publishes to npm when a version tag is pushed (git tag v1.2.3 && git push --tags). +# +# Authentication uses npm trusted publishing (OIDC): configure it once on +# npmjs.com under the package's Settings -> Trusted publisher, pointing at +# this repository and this workflow file. No token secret is needed, and +# provenance attestation is generated automatically. +# +# Token fallback: create a granular automation token on npmjs.com, save it +# as the NPM_TOKEN repository secret, and uncomment the env line below. + +on: + push: + tags: ['v*'] + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: 'https://registry.npmjs.org' + - run: npm ci + # prepublishOnly runs typecheck + tests + build before anything is sent. + - run: npm publish + # env: + # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..945af05 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +All notable changes to this project are documented in this file. +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and the project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +### Added +- CI workflow: full test suite on Node 24 plus a compatibility matrix that + smoke-tests the built ESM/CJS output on Node 16/18/20/22, backing the + `engines: >=16` claim with an actual run. +- Release workflow publishing to npm via trusted publishing (OIDC) with + provenance attestation. + +## [1.0.1] - 2026-08-28 + +### Changed +- Word/line modes now use a fused scan pipeline: token boundaries and + FNV-1a hashes are collected in one pass, interning runs through an + open-addressed table reading straight out of the input strings, and every + output text is a single slice — no token substrings are materialized. +- `char` mode bypasses tokenization entirely (code point scan straight into + typed arrays, surrogate-pair-safe boundary handling). +- Common token prefix/suffix is stripped before interning. + +### Performance +- Word diff on a 44 KB / 10-edit document: 1.24 ms → 0.97 ms. +- Char diff on the same document: 1.63 ms → 0.62 ms. +- Line diff: 0.33 ms → 0.27 ms. + +### Documentation +- README benchmarks now compare against jsdiff, diff-match-patch, and + fast-myers-diff end-to-end (`npm run bench`), with fairness caveats. +- npm search metadata: rewritten description and expanded keywords. + +## [1.0.0] - 2026-08-28 + +### Added +- Initial release: Myers O(ND) shortest-edit-script diff with the + linear-space middle-snake refinement, token interning to `Int32Array`, + reusable scratch buffers, and Unicode-aware word/char/line tokenization. +- `diff(a, b, { mode })`, `diffTokens(aTokens, bTokens)`, and `tokenize` + public API; ESM + CJS + browser IIFE builds with bundled type definitions. +- Verification suite: seeded fuzz round-trips and optimality checks against + a reference LCS dynamic program. diff --git a/README.md b/README.md index cf2bc63..ef8c1ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # @krkarma777/string-diff [![npm version](https://img.shields.io/npm/v/%40krkarma777%2Fstring-diff)](https://www.npmjs.com/package/@krkarma777/string-diff) +[![CI](https://img.shields.io/github/actions/workflow/status/krkarma777/string-difference-finder/ci.yml?branch=master&label=CI)](https://github.com/krkarma777/string-difference-finder/actions/workflows/ci.yml) [![weekly downloads](https://img.shields.io/npm/dw/%40krkarma777%2Fstring-diff)](https://www.npmjs.com/package/@krkarma777/string-diff) [![total downloads](https://badgen.net/npm/dt/@krkarma777/string-diff?label=total%20downloads)](https://npm-stat.com/charts.html?package=%40krkarma777%2Fstring-diff) [![minzipped size](https://img.shields.io/badge/min%2Bgzip-2.9%20kB-blue)](#how-it-works) diff --git a/scripts/smoke.cjs b/scripts/smoke.cjs new file mode 100644 index 0000000..ed1dbfd --- /dev/null +++ b/scripts/smoke.cjs @@ -0,0 +1,23 @@ +// Smoke test for the built CommonJS entry, runnable on every supported Node major. +const assert = require('node:assert'); +const { diff } = require('../dist/index.cjs'); + +const entries = diff('a b c', 'a x c'); +assert.deepStrictEqual(entries, [ + { operation: 'equal', text: 'a ' }, + { operation: 'delete', text: 'b' }, + { operation: 'insert', text: 'x' }, + { operation: 'equal', text: ' c' }, +]); + +const a = 'lorem ipsum dolor'; +const b = 'lorem IPSUM dolor sit'; +for (const mode of ['word', 'char', 'line']) { + const result = diff(a, b, { mode }); + const joinedA = result.filter(e => e.operation !== 'insert').map(e => e.text).join(''); + const joinedB = result.filter(e => e.operation !== 'delete').map(e => e.text).join(''); + assert.strictEqual(joinedA, a); + assert.strictEqual(joinedB, b); +} + +console.log(`smoke.cjs OK on node ${process.version}`); diff --git a/scripts/smoke.mjs b/scripts/smoke.mjs new file mode 100644 index 0000000..f901357 --- /dev/null +++ b/scripts/smoke.mjs @@ -0,0 +1,28 @@ +// Smoke test for the built ESM entry, runnable on every supported Node major. +import assert from 'node:assert'; +import { diff, diffTokens, tokenize } from '../dist/index.js'; + +const entries = diff('the quick fox', 'the slow fox'); +assert.deepStrictEqual(entries, [ + { operation: 'equal', text: 'the ' }, + { operation: 'delete', text: 'quick' }, + { operation: 'insert', text: 'slow' }, + { operation: 'equal', text: ' fox' }, +]); + +assert.deepStrictEqual(diff('안녕하세요 세계', '안녕하세요 지구')[0], { operation: 'equal', text: '안녕하세요 ' }); + +for (const mode of ['word', 'char', 'line']) { + const a = 'alpha beta\ngamma delta 😀'; + const b = 'alpha BETA\ngamma epsilon 😀!'; + const result = diff(a, b, { mode }); + const joinedA = result.filter(e => e.operation !== 'insert').map(e => e.text).join(''); + const joinedB = result.filter(e => e.operation !== 'delete').map(e => e.text).join(''); + assert.strictEqual(joinedA, a, `round-trip a failed in ${mode} mode`); + assert.strictEqual(joinedB, b, `round-trip b failed in ${mode} mode`); +} + +assert.deepStrictEqual(diffTokens(['x', 'y'], ['x', 'z']).length, 3); +assert.deepStrictEqual(tokenize('a b', 'word'), ['a', ' ', 'b']); + +console.log(`smoke.mjs OK on node ${process.version}`);