From 377b094f2dd6f6d3bdc1d0ce5a3abb067496099d Mon Sep 17 00:00:00 2001 From: Richard Russell <2265225+rars@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:36:07 +0000 Subject: [PATCH] feat(ngx-diff): allow diff computation to run in Web worker for large diffs --- README.md | 85 +++ angular.json | 8 +- package-lock.json | 693 +++++++----------- package.json | 2 +- projects/ngx-diff/package.json | 10 +- .../progress-bar/progress-bar.component.html | 5 + .../progress-bar/progress-bar.component.scss | 30 + .../progress-bar.component.spec.ts | 23 + .../progress-bar/progress-bar.component.ts | 9 + .../side-by-side-diff.component.html | 117 +-- .../side-by-side-diff.component.spec.ts | 177 ++++- .../side-by-side-diff.component.ts | 73 +- .../unified-diff/unified-diff.component.html | 79 +- .../unified-diff.component.spec.ts | 96 +-- .../unified-diff/unified-diff.component.ts | 61 +- .../diff-match-patch.service.ts | 125 +++- projects/ngx-diff/styles/default-theme.css | 6 + src/app/app.component.html | 3 +- .../diff-web-worker-factory.service.spec.ts | 16 + .../diff-web-worker-factory.service.ts | 13 + src/app/web-workers/diff-web-worker.worker.ts | 25 + src/main.ts | 3 + tsconfig.worker.json | 15 + 23 files changed, 1073 insertions(+), 601 deletions(-) create mode 100644 projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.html create mode 100644 projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.scss create mode 100644 projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.spec.ts create mode 100644 projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.ts create mode 100644 src/app/services/diff-web-worker-factory/diff-web-worker-factory.service.spec.ts create mode 100644 src/app/services/diff-web-worker-factory/diff-web-worker-factory.service.ts create mode 100644 src/app/web-workers/diff-web-worker.worker.ts create mode 100644 tsconfig.worker.json diff --git a/README.md b/README.md index 77e6c06..404ae5c 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ To create your own theme, override the relevant CSS variables; for example, in y --ngx-diff-selected-border-color: #000; --ngx-diff-selected-line-background-color: #d6f1ff; + --ngx-diff-progress-background-color: #dfdfdf; + --ngx-diff-progress-foreground-color: #aaaaaa; + --ngx-diff-line-number-width: 2rem; --ngx-diff-line-number-width-dynamic-padding: 1rem; --ngx-diff-border-width: 1px; @@ -128,6 +131,88 @@ Then use this class in your desired component in your HTML template: It is recommended to use these settings rather than attempt to override styles based upon DOM structure or class names that are internal details that may change. +## Run diff in Web Worker + +**New in version 13.1.0** + +Very large diffs can take over 100ms to compute in Js. To avoid locking the UI main thread, you can configure this to run in a Web worker as follows: + +1. In your application create a new Web worker: + +```bash +npx ng g web-worker web-workers/DiffWebWorker +``` + +2. In your newly created file `diff-web-worker.worker.ts`, add implementation that will compute the diff: + +```js +/// + +import { DiffMatchPatch } from 'diff-match-patch-ts'; + +addEventListener('message', ({ data }) => { + try { + if (typeof data.before !== 'string' || typeof data.after !== 'string') { + throw new TypeError('Input data for diffing must be strings.'); + } + + const dmp = new DiffMatchPatch(); + const diffs = dmp.diff_lineMode(data.before, data.after); + + postMessage({ id: data.id, status: 'success', diffs }); + } catch (error: any) { + postMessage({ id: data.id, status: 'error', error: { message: error.message, stack: error.stack } }); + } +}); +``` + +3. Create a factory service that will create the Web worker: + +```bash +npx ng g s services/diff-web-worker-factory/DiffWebWorkerFactory +``` + +And implement the `IDiffWebWorkerFactory` interface in the generated diff-web-worker-factory.service.ts: + +```js +import { Injectable } from '@angular/core'; +import { IDiffWebWorkerFactory } from 'ngx-diff'; + +@Injectable() +export class DiffWebWorkerFactoryService implements IDiffWebWorkerFactory { + public createWorker(): Worker | undefined { + if (typeof Worker !== 'undefined') { + return new Worker(new URL('../../web-workers/diff-web-worker.worker', import.meta.url)); + } else { + return undefined; + } + } +} + +``` + +4. Specify the factory service in your `main.ts` file: + +```js +import { NGX_DIFF_WEB_WORKER_FACTORY } from 'ngx-diff'; +import { DiffWebWorkerFactoryService } from './app/services/diff-web-worker-factory/diff-web-worker-factory.service'; + +if (environment.production) { + enableProdMode(); +} + +bootstrapApplication(AppComponent, { + providers: [ + importProvidersFrom(BrowserModule, AppRoutingModule), + provideZonelessChangeDetection(), + /* Add this line below: */ + { provide: NGX_DIFF_WEB_WORKER_FACTORY, useClass: DiffWebWorkerFactoryService }, + ], +}).catch((err) => console.error(err)); +``` + +Now `ngx-diff` will detect that you have a Web worker factory configured and use that to run the diff on a Web worker instead of the main UI thread. + ## Version history | Angular Version | ngx-diff Version | diff --git a/angular.json b/angular.json index 302db82..42a0c10 100644 --- a/angular.json +++ b/angular.json @@ -24,7 +24,8 @@ "sourceMap": true, "optimization": false, "namedChunks": true, - "browser": "src/main.ts" + "browser": "src/main.ts", + "webWorkerTsConfig": "tsconfig.worker.json" }, "configurations": { "production": { @@ -78,7 +79,10 @@ } }, "test": { - "builder": "@angular/build:unit-test" + "builder": "@angular/build:unit-test", + "options": { + "webWorkerTsConfig": "tsconfig.worker.json" + } } } }, diff --git a/package-lock.json b/package-lock.json index 71918d4..b163ffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@angular-eslint/builder": "^21.0.0", "@angular-eslint/eslint-plugin": "^21.0.0", "@angular-eslint/eslint-plugin-template": "^21.0.0", - "@angular-eslint/schematics": "21.0.0", + "@angular-eslint/schematics": "21.0.1", "@angular-eslint/template-parser": "^21.0.0", "@angular/build": "^21.0.0", "@angular/cli": "^21.0.0", @@ -280,13 +280,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2100.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2100.0.tgz", - "integrity": "sha512-BNt6Rw53WauCw31ku/r/ksVIY+Pi8XZptsSUIHiDUeqB2iZOWu4L3c5kuDGmoGkGByY588H48hfR2MgIpBhgAg==", + "version": "0.2100.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2100.1.tgz", + "integrity": "sha512-MLxTT6EE7NHuCen9yGdv9iT2vtB/fAdXTRnulOWfVa/SVmGoKawBGCNOAPpI2yA8Fb/D5xlU6ThS1ggDsiCqrQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.0", + "@angular-devkit/core": "21.0.1", "rxjs": "7.8.2" }, "engines": { @@ -296,9 +296,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.0.0.tgz", - "integrity": "sha512-d3n5GvrwqN1AUkWE3Wd8rrdY2u6/5bzorlZVT5W4CcH7ekAIoMu4SBTbSJ7bfRe/l2z/A1WZ6hFlnQzLclOjJA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.0.1.tgz", + "integrity": "sha512-AGdAu0hV2TLCWYHiyVSxUFbpR2chO+xA4OkRrG2YirQGcqJTmr651C4rWDkheWqeWDxMicZklqKaTw66mNSUkw==", "dev": true, "license": "MIT", "dependencies": { @@ -324,13 +324,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.0.0.tgz", - "integrity": "sha512-8zwXp8OTzJO3IY3Ge3lLqXokNAtQy6kM1FeTyPT20M+0AQHTX9WJlGaYEWdLYI9WwNPWy1/Iq6AaZNcR5phPpw==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.0.1.tgz", + "integrity": "sha512-3koB1xJNkqMg7g6JwH2rhQO268WjnPVA852lwoLW7wzSZRpJH0kHtZsnY9FYOC2kbmAGnCWWbnPLJ5/T1wemoA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.0", + "@angular-devkit/core": "21.0.1", "jsonc-parser": "3.3.1", "magic-string": "0.30.19", "ora": "9.0.0", @@ -343,9 +343,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.0.0.tgz", - "integrity": "sha512-+d2H2MBo6DgaZnTZ5v6arqrqYrP0qyQNcpQ6UlECGFwx5SC2zgQFHfKjJXWBaH8/nv2QF/zNXeV9/OEpmNw4PA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.0.1.tgz", + "integrity": "sha512-6BqpmW0XvjTOs2YOHwzeZcQ32eL8vs8SCHjt1cQnq1+libOVDXky1eb/jRs7ouyA49UagLDoM34K1kjrYo8P3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -358,21 +358,21 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.0.0.tgz", - "integrity": "sha512-jHO8ifzv+E/kkDzqrIZ5NCbLlLh9fuWoiuBmcjMdRJDDz6bjKDREuouqflvbzxIPxf6pAwI11yCn2nWSEMEYhA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.0.1.tgz", + "integrity": "sha512-Kb59SopkJ2sDgZSysL+ZqcfqM2cbK+gciAyHljkrCUsqo66eEq5KCZUU//RVoo4MHi+qL/dFy54JG/+A/35xcQ==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.0.0.tgz", - "integrity": "sha512-Ry6MPJey2MCE7JbhZ9KXJmao4WpH9BarBZL8tcmaqbRQqb9N8KRQlWNMAtPdfKN/y1P5m87PwL/QYVVRt2A34w==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.0.1.tgz", + "integrity": "sha512-tSb5qgIwoMrX3Z17dSsHrNFWrgBWafxK7IQudU0RXxdzq6joq1qDrzHwLT3Jn+Y6ocn0jdavAefEGHAhomCjcQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.0", - "@angular-eslint/utils": "21.0.0", + "@angular-eslint/bundled-angular-compiler": "21.0.1", + "@angular-eslint/utils": "21.0.1", "ts-api-utils": "^2.1.0" }, "peerDependencies": { @@ -382,19 +382,19 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.0.0.tgz", - "integrity": "sha512-N05NMslhY+isR/aSPnNjaC+l5OV1dDrGnxPETQ7Oxag3MyPUrwLv/HRWC+DxSJkJMAQT7GnxxNx4JyfHQWusFA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.0.1.tgz", + "integrity": "sha512-DF1WEMalbV1hNKxbu3nwK1yUa+E2FQpNz0KDORU65/vdCffeuftCetobrsAS7zDgJ6FO+Fsb+ZeCzNKEhhh1vA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.0", - "@angular-eslint/utils": "21.0.0", + "@angular-eslint/bundled-angular-compiler": "21.0.1", + "@angular-eslint/utils": "21.0.1", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { - "@angular-eslint/template-parser": "21.0.0", + "@angular-eslint/template-parser": "21.0.1", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", @@ -402,30 +402,30 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.0.0.tgz", - "integrity": "sha512-lu28D3H/LS+7kf95AP3tk6GYxOkhePxeF2DuUm/xDESmZjecmi8sUB7PmApaBx/vwae3afkBIHHNGy2b2Hxa3g==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.0.1.tgz", + "integrity": "sha512-IdtGdRPuJctHuiZ8v8SN3MqWiUa3cD9Q5jFvIRkAkjpHXXmTk5PYelSjUP8UX/zfUfFkxHXghasTmJd2+252OQ==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.0.0", - "@angular-eslint/eslint-plugin-template": "21.0.0", + "@angular-eslint/eslint-plugin": "21.0.1", + "@angular-eslint/eslint-plugin-template": "21.0.1", "ignore": "7.0.5", "semver": "7.7.3", "strip-json-comments": "3.1.1" } }, "node_modules/@angular-eslint/template-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.0.0.tgz", - "integrity": "sha512-bkqFdIbK7CB0TZVcfxr+xRQdoZ+07S5DlrurmsT1zu4ttfYd5KY1QgtngpMOvBOlPBy3uKQ23QkW/EQ4RB1OxA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.0.1.tgz", + "integrity": "sha512-1KocmjmBP0qlKQGRhRGN0MGvLxf1q2KDWbvzn7ZGdQrIDLC/hFJ8YmnOWsPrM9RxiZi0o5BxCCu9D7KlbthxIg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.0", + "@angular-eslint/bundled-angular-compiler": "21.0.1", "eslint-scope": "^9.0.0" }, "peerDependencies": { @@ -434,13 +434,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.0.0.tgz", - "integrity": "sha512-Ner0su/FKzwOlvVWn61XCVn3g6JoQY3+Cuq1qilMYstO1HwuGJY6CZIsih8RMim0cSpSPO+VlBtg2V+/Czi9yw==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.0.1.tgz", + "integrity": "sha512-tovWIDiEsfSAsPWH+/wL9Hfl/Hc+2j2IP+Z85I6uWTbynLVdyURx8gmJjKBUTSCmcyrgBnTbnnlr4DTM6/aFOg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.0.0" + "@angular-eslint/bundled-angular-compiler": "21.0.1" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -449,9 +449,9 @@ } }, "node_modules/@angular/animations": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.0.tgz", - "integrity": "sha512-9AX4HFJmSP8SFNiweKNxasBzn3zbL3xRtwaUxw1I+x/WAzubm4ZziLnXqb+tai7C4UmwV+9XDlRVPfw5WxJ9zg==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.1.tgz", + "integrity": "sha512-P7i/jpNnzXwo0vHEG0cDXYojwTz0WQlXJHrmOJzLVveyfcFwgXYXJxhGGUI2+k21YrlJTKkR/4QZTEJ0GP0f8Q==", "license": "MIT", "peer": true, "dependencies": { @@ -461,18 +461,18 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.0.0" + "@angular/core": "21.0.1" } }, "node_modules/@angular/build": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.0.0.tgz", - "integrity": "sha512-TobXT9fXZVee1yULlcOVowOurCUoJlku8st5vzkRZekP520qRjBSEbIk8V2emkFbzgzOeJUtXv1pvrBY7yAYhQ==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.0.1.tgz", + "integrity": "sha512-AQFZWG5TtujCRs7ncajeBZpl/hLBKkuF0lZSziJL8tsgBru0hz0OobOkEuS/nb3FuCRQfva8YP2EPhLdcuo50g==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2100.0", + "@angular-devkit/architect": "0.2100.1", "@babel/core": "7.28.4", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -515,7 +515,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.0.0", + "@angular/ssr": "^21.0.1", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -565,19 +565,19 @@ } }, "node_modules/@angular/cli": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.0.tgz", - "integrity": "sha512-713DfTD/ThIy/BOmZ+8zhXo/OhPE9jYaAS0UhXVhtp2ptqzRqSzLvW9fWgtqP4ITAqulOoitiWPLXxOEQ2Cixw==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.1.tgz", + "integrity": "sha512-i0+7jwf19D73yAzR/lL4+eKVhooM+J055qfSaJWL5QLCF9/JSSjMPCG8I/qIGNdVr+lVmWvvxqpt7O7kR3zfUw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2100.0", - "@angular-devkit/core": "21.0.0", - "@angular-devkit/schematics": "21.0.0", + "@angular-devkit/architect": "0.2100.1", + "@angular-devkit/core": "21.0.1", + "@angular-devkit/schematics": "21.0.1", "@inquirer/prompts": "7.9.0", "@listr2/prompt-adapter-inquirer": "3.0.5", "@modelcontextprotocol/sdk": "1.20.1", - "@schematics/angular": "21.0.0", + "@schematics/angular": "21.0.1", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.40.1", "ini": "5.0.0", @@ -601,9 +601,9 @@ } }, "node_modules/@angular/common": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.0.tgz", - "integrity": "sha512-uFvQDYU5X5nEnI9C4Bkdxcu4aIzNesGLJzmFlnwChVxB4BxIRF0uHL0oRhdkInGTIzPDJPH4nF6B/22c5gDVqA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.1.tgz", + "integrity": "sha512-EqdTGpFp7PVdTVztO7TB6+QxdzUbYXKKT2jwG2Gg+PIQZ2A8XrLPRmGXyH/DLlc5IhnoJlLbngmBRCLCO4xWog==", "license": "MIT", "peer": true, "dependencies": { @@ -613,14 +613,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.0.0", + "@angular/core": "21.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.0.tgz", - "integrity": "sha512-6jCH3UYga5iokj5F40SR4dlwo9ZRMkT8YzHCTijwZuDX9zvugp9jPof092RvIeNsTvCMVfGWuM9yZ1DRUsU/yg==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.1.tgz", + "integrity": "sha512-YRzHpThgCaC9b3xzK1Wx859ePeHEPR7ewQklUB5TYbpzVacvnJo38PcSAx/nzOmgX9y4mgyros6LzECmBb8d8w==", "license": "MIT", "peer": true, "dependencies": { @@ -631,9 +631,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.0.0.tgz", - "integrity": "sha512-KTXp+e2UPGyfFew6Wq95ULpHWQ20dhqkAMZ6x6MCYfOe2ccdnGYsAbLLmnWGmSg5BaOI4B0x/1XCFZf/n6WDgA==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.0.1.tgz", + "integrity": "sha512-BxGLtL5bxlaaAs/kSN4oyXhMfvzqsj1Gc4Jauz39R4xtgOF5cIvjBtj6dJ9mD3PK0s6zaFi7WYd0YwWkxhjgMA==", "dev": true, "license": "MIT", "peer": true, @@ -655,7 +655,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.0", + "@angular/compiler": "21.0.1", "typescript": ">=5.9 <6.0" }, "peerDependenciesMeta": { @@ -665,9 +665,9 @@ } }, "node_modules/@angular/core": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.0.tgz", - "integrity": "sha512-bqi8fT4csyITeX8vdN5FJDBWx5wuWzdCg4mKSjHd+onVzZLyZ8bcnuAKz4mklgvjvwuXoRYukmclUurLwfq3Rg==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.1.tgz", + "integrity": "sha512-z0G9Bwzgqr0fQVbtMgqwl+SbbiqtJD7I2xT6U5p45LetKHojcfigH29dxi/vqALPwEdgb2nSIx7RqVhoyynraQ==", "license": "MIT", "peer": true, "dependencies": { @@ -677,9 +677,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.0.0", + "@angular/compiler": "21.0.1", "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0" + "zone.js": "~0.15.0 || ~0.16.0" }, "peerDependenciesMeta": { "@angular/compiler": { @@ -691,9 +691,9 @@ } }, "node_modules/@angular/forms": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.0.tgz", - "integrity": "sha512-kcudwbZs/ddKqaELz4eEW9kOGCsX61qsf9jkQsGTARBEOUcU2K+rM6mX5sTf9azHvQ9wlX4N36h0eYzBA4Y4Qg==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.1.tgz", + "integrity": "sha512-BVFPuKjxkzjzKMmpc6KxUKICpVs6J2/KzA4HjtPp/UKvdZPe8dj8vIXuc9pGf8DA4XdkjCwvv8szCgzTWi02LQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -702,17 +702,17 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.0", - "@angular/core": "21.0.0", - "@angular/platform-browser": "21.0.0", + "@angular/common": "21.0.1", + "@angular/core": "21.0.1", + "@angular/platform-browser": "21.0.1", "@standard-schema/spec": "^1.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-21.0.0.tgz", - "integrity": "sha512-onJI3CzNSszcXK0/zVS66IDfaZpTVUdkduZTqth2w8CNaBkG6N/g9wleUVLwarx1+Vy4c4Fqr+gb85QkeGy2aQ==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-21.0.1.tgz", + "integrity": "sha512-+QohcgWbgrsPsHFhbie1ZQaNsnoBpuVK7479WZXPyFiw4PWEceNuF0hSr9yrSNEh/kvgCu9BfJSzVf7w5Yj39A==", "dev": true, "license": "MIT", "engines": { @@ -720,9 +720,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.0.tgz", - "integrity": "sha512-KQrANla4RBLhcGkwlndqsKzBwVFOWQr1640CfBVjj2oz4M3dW5hyMtXivBACvuwyUhYU/qJbqlDMBXl/OUSudQ==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.1.tgz", + "integrity": "sha512-68StH9HILKUqNhQKz6KKNHzpgk1n88CIusWlmJvnb0l6iWGf3ydq5lTMKAKiZQmSDAVP5unTGfNvIkh59GRyVg==", "license": "MIT", "peer": true, "dependencies": { @@ -732,9 +732,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "21.0.0", - "@angular/common": "21.0.0", - "@angular/core": "21.0.0" + "@angular/animations": "21.0.1", + "@angular/common": "21.0.1", + "@angular/core": "21.0.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -743,9 +743,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.0.0.tgz", - "integrity": "sha512-H7nfgQvtzl242Tjs34k20XQC3ZNssJCCvYkGTkVowR61khsX87OE5ggKqTSnLiqq1+OoR29hyvvqn5e9truS7w==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.0.1.tgz", + "integrity": "sha512-TzCKf3p1NBK1NYoPJXLScSjVeiQ52DaXf9gweNUGtCmX3EkVKf1sx4Ny1x4DxaTwB5XZn+O+L3nVLstPBj7UGA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -754,16 +754,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.0", - "@angular/compiler": "21.0.0", - "@angular/core": "21.0.0", - "@angular/platform-browser": "21.0.0" + "@angular/common": "21.0.1", + "@angular/compiler": "21.0.1", + "@angular/core": "21.0.1", + "@angular/platform-browser": "21.0.1" } }, "node_modules/@angular/router": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.0.tgz", - "integrity": "sha512-ARx1R2CmTgAezlMkUpV40V4T/IbXhL7dm4SuMVKbuEOsCKZC0TLOSSTsGYY7HKem45JHlJaByv819cJnabFgBg==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.1.tgz", + "integrity": "sha512-EnNbiScESZ0op9XS9qUNncWc1UcSYy90uCbDMVTTChikZt9b+e19OusFMf50zecb96VMMz+BzNY1see7Rmvx4g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -772,9 +772,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.0.0", - "@angular/core": "21.0.0", - "@angular/platform-browser": "21.0.0", + "@angular/common": "21.0.1", + "@angular/core": "21.0.1", + "@angular/platform-browser": "21.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1607,9 +1607,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.17.tgz", - "integrity": "sha512-LCC++2h8pLUSPY+EsZmrrJ1EOUu+5iClpEiDhhdw3zRJpPbABML/N5lmRuBHjxtKm9VnRcsUzioyD0sekFMF0A==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.19.tgz", + "integrity": "sha512-QW5/SM2ARltEhoKcmRI1LoLf3/C7dHGswwCnfLcoMgqurBT4f8GvwXMgAbK/FwcxthmJRK5MGTtddj0yQn0J9g==", "dev": true, "funding": [ { @@ -3455,44 +3455,6 @@ "@tybys/wasm-util": "^0.10.1" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@npmcli/agent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", @@ -3597,9 +3559,9 @@ } }, "node_modules/@npmcli/git/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -3669,9 +3631,9 @@ } }, "node_modules/@npmcli/package-json/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -3769,9 +3731,9 @@ } }, "node_modules/@npmcli/run-script/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -4761,14 +4723,14 @@ "license": "MIT" }, "node_modules/@schematics/angular": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.0.0.tgz", - "integrity": "sha512-50eEsBaT++Gwr+5FAhaKIzTUjpE1DJAwmE5QwtogbTnr2viZc8CsbFOfuMrokQbgdcXRvbkBDPXgO15STMcDRQ==", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.0.1.tgz", + "integrity": "sha512-m7Z/gykPxOyC5Gs9nkFkGwYTc5xLNLcVkjjZPcYszycwsWBohDREjQLZzRG86AauWFYy8mBUrTF9CD63ZqYHeQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.0.0", - "@angular-devkit/schematics": "21.0.0", + "@angular-devkit/core": "21.0.1", + "@angular-devkit/schematics": "21.0.1", "jsonc-parser": "3.3.1" }, "engines": { @@ -5028,17 +4990,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", - "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/type-utils": "8.47.0", - "@typescript-eslint/utils": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -5052,23 +5014,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.47.0", + "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", - "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4" }, "engines": { @@ -5084,14 +5046,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", - "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.47.0", - "@typescript-eslint/types": "^8.47.0", + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", "debug": "^4.3.4" }, "engines": { @@ -5106,14 +5068,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", - "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0" + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5124,9 +5086,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", - "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", "dev": true, "license": "MIT", "engines": { @@ -5141,15 +5103,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", - "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -5166,9 +5128,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", - "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", "dev": true, "license": "MIT", "peer": true, @@ -5181,21 +5143,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", - "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.47.0", - "@typescript-eslint/tsconfig-utils": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -5210,17 +5171,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", - "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0" + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5235,13 +5196,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", - "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/types": "8.48.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -5279,16 +5240,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.13.tgz", - "integrity": "sha512-zYtcnNIBm6yS7Gpr7nFTmq8ncowlMdOJkWLqYvhr/zweY6tFbDkDi8BPPOeHxEtK1rSI69H7Fd4+1sqvEGli6w==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.14.tgz", + "integrity": "sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.13", - "@vitest/utils": "4.0.13", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -5297,13 +5258,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.13.tgz", - "integrity": "sha512-eNCwzrI5djoauklwP1fuslHBjrbR8rqIVbvNlAnkq1OTa6XT+lX68mrtPirNM9TnR69XUPt4puBCx2Wexseylg==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.14.tgz", + "integrity": "sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.13", + "@vitest/spy": "4.0.14", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -5344,9 +5305,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.13.tgz", - "integrity": "sha512-ooqfze8URWbI2ozOeLDMh8YZxWDpGXoeY3VOgcDnsUxN0jPyPWSUvjPQWqDGCBks+opWlN1E4oP1UYl3C/2EQA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.14.tgz", + "integrity": "sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5357,13 +5318,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.13.tgz", - "integrity": "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.14.tgz", + "integrity": "sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.13", + "@vitest/utils": "4.0.14", "pathe": "^2.0.3" }, "funding": { @@ -5371,13 +5332,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.13.tgz", - "integrity": "sha512-hb7Usvyika1huG6G6l191qu1urNPsq1iFc2hmdzQY3F5/rTgqQnwwplyf8zoYHkpt7H6rw5UfIw6i/3qf9oSxQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.14.tgz", + "integrity": "sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.13", + "@vitest/pretty-format": "4.0.14", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -5396,9 +5357,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.13.tgz", - "integrity": "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.14.tgz", + "integrity": "sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==", "dev": true, "license": "MIT", "funding": { @@ -5406,13 +5367,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.13.tgz", - "integrity": "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.14.tgz", + "integrity": "sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.13", + "@vitest/pretty-format": "4.0.14", "tinyrainbow": "^3.0.3" }, "funding": { @@ -5824,9 +5785,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.30", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", - "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", + "version": "2.8.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", + "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5864,37 +5825,28 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/boolbase": { @@ -5920,6 +5872,7 @@ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -6114,9 +6067,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001756", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", - "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", "dev": true, "funding": [ { @@ -8092,9 +8045,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.259", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", - "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", + "version": "1.5.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", + "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", "dev": true, "license": "ISC" }, @@ -9110,36 +9063,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -9171,16 +9094,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -9244,6 +9157,7 @@ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -10931,6 +10845,7 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.12.0" } @@ -11858,9 +11773,9 @@ } }, "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -11946,22 +11861,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -11976,6 +11882,7 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8.6" }, @@ -12982,9 +12889,9 @@ } }, "node_modules/node-gyp/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -13146,9 +13053,9 @@ } }, "node_modules/npm-packlist/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -13202,9 +13109,9 @@ } }, "node_modules/npm-registry-fetch/node_modules/proc-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", - "integrity": "sha512-KG/XsTDN901PNfPfAMmj6N/Ywg9tM+bHK8pAz+27fS4N4Pcr+4zoYBOcGSBu6ceXYNPxkLpa4ohtfxV1XcLAfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { @@ -13338,6 +13245,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -13897,9 +13815,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz", + "integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==", "dev": true, "license": "MIT", "peer": true, @@ -14004,27 +13922,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -14378,17 +14275,6 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -14473,13 +14359,13 @@ } }, "node_modules/rollup-plugin-dts": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.2.3.tgz", - "integrity": "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.3.0.tgz", + "integrity": "sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA==", "dev": true, "license": "LGPL-3.0-only", "dependencies": { - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "engines": { "node": ">=16" @@ -14495,6 +14381,16 @@ "typescript": "^4.5 || ^5.0" } }, + "node_modules/rollup-plugin-dts/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -14512,30 +14408,6 @@ "node": ">= 18" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -15808,6 +15680,7 @@ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "is-number": "^7.0.0" }, @@ -16830,24 +16703,24 @@ } }, "node_modules/vitest": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.13.tgz", - "integrity": "sha512-QSD4I0fN6uZQfftryIXuqvqgBxTvJ3ZNkF6RWECd82YGAYAfhcppBLFXzXJHQAAhVFyYEuFTrq6h0hQqjB7jIQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.14.tgz", + "integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vitest/expect": "4.0.13", - "@vitest/mocker": "4.0.13", - "@vitest/pretty-format": "4.0.13", - "@vitest/runner": "4.0.13", - "@vitest/snapshot": "4.0.13", - "@vitest/spy": "4.0.13", - "@vitest/utils": "4.0.13", - "debug": "^4.4.3", + "@vitest/expect": "4.0.14", + "@vitest/mocker": "4.0.14", + "@vitest/pretty-format": "4.0.14", + "@vitest/runner": "4.0.14", + "@vitest/snapshot": "4.0.14", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", @@ -16870,12 +16743,11 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.13", - "@vitest/browser-preview": "4.0.13", - "@vitest/browser-webdriverio": "4.0.13", - "@vitest/ui": "4.0.13", + "@vitest/browser-playwright": "4.0.14", + "@vitest/browser-preview": "4.0.14", + "@vitest/browser-webdriverio": "4.0.14", + "@vitest/ui": "4.0.14", "happy-dom": "*", "jsdom": "*" }, @@ -16886,9 +16758,6 @@ "@opentelemetry/api": { "optional": true }, - "@types/debug": { - "optional": true - }, "@types/node": { "optional": true }, diff --git a/package.json b/package.json index 1d9cd5c..b3899e3 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@angular-eslint/builder": "^21.0.0", "@angular-eslint/eslint-plugin": "^21.0.0", "@angular-eslint/eslint-plugin-template": "^21.0.0", - "@angular-eslint/schematics": "21.0.0", + "@angular-eslint/schematics": "21.0.1", "@angular-eslint/template-parser": "^21.0.0", "@angular/build": "^21.0.0", "@angular/cli": "^21.0.0", diff --git a/projects/ngx-diff/package.json b/projects/ngx-diff/package.json index 55933e2..5e473aa 100644 --- a/projects/ngx-diff/package.json +++ b/projects/ngx-diff/package.json @@ -1,6 +1,7 @@ { "name": "ngx-diff", "version": "13.0.0", + "description": "An Angular component for viewing text diffs.", "peerDependencies": { "@angular/common": ">=21.0.0", "@angular/core": ">=21.0.0", @@ -16,7 +17,14 @@ "keywords": [ "Angular", "ng", - "diff" + "ngx", + "diff", + "difference", + "text", + "compare", + "component", + "text-diff", + "diff-viewer" ], "author": "Richard Russell", "license": "MIT", diff --git a/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.html b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.html new file mode 100644 index 0000000..918cdbb --- /dev/null +++ b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.html @@ -0,0 +1,5 @@ +
+
+
+
+
diff --git a/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.scss b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.scss new file mode 100644 index 0000000..d2118a2 --- /dev/null +++ b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.scss @@ -0,0 +1,30 @@ +.progress-bar-container { + position: relative; + width: 100%; + height: 4px; +} + +.progress-bar { + height: 4px; + width: 100%; + position: absolute; + top: 0; + overflow: hidden; + background-color: var(--ngx-diff-progress-background-color); +} + +.progress-bar-inner { + height: 100%; + width: 30%; + background-color: var(--ngx-diff-progress-foreground-color); + animation: move 1.5s infinite linear; +} + +@keyframes move { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(300%); + } +} diff --git a/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.spec.ts b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.spec.ts new file mode 100644 index 0000000..b0ec41c --- /dev/null +++ b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProgressBarComponent } from './progress-bar.component'; + +describe('ProgressBarComponent', () => { + let component: ProgressBarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProgressBarComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProgressBarComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.ts b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.ts new file mode 100644 index 0000000..bb9f089 --- /dev/null +++ b/projects/ngx-diff/src/lib/components/progress-bar/progress-bar.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngx-progress-bar', + imports: [], + templateUrl: './progress-bar.component.html', + styleUrl: './progress-bar.component.scss', +}) +export class ProgressBarComponent {} diff --git a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.html b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.html index f4c3b43..832715e 100644 --- a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.html +++ b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.html @@ -1,77 +1,88 @@ -
- @if (title()) { - {{ title() }}  - } - +++ {{ diffSummary().numLinesAdded }}  - --- {{ diffSummary().numLinesRemoved }} -
-@if (isContentEqual()) { -
-
There are no changes to display.
+@if (isCalculating()) { +
+ @if (processedDiff().title) { + {{ processedDiff().title }} + }
+ } @else { -
- -
- @for (lineDiff of beforeLines(); track lineDiff.id; let idx = $index) { -
-
{{ lineDiff.lineNumber | lineNumber }}
-
- } -
+
+ @if (processedDiff().title) { + {{ processedDiff().title }} + } + + +++ {{ processedDiff().diffSummary.numLinesAdded }} + + + --- {{ processedDiff().diffSummary.numLinesRemoved }} + +
+ @if (processedDiff().isContentEqual) { +
+
There are no changes to display.
-
-
+ } @else { +
+ +
@for (lineDiff of beforeLines(); track lineDiff.id; let idx = $index) {
-
{{ lineDiff.line }}
+
{{ lineDiff.lineNumber | lineNumber }}
} -
+
-
- -
- @for (lineDiff of afterLines(); track lineDiff.id; let idx = $index) { -
-
{{ lineDiff.lineNumber | lineNumber }}
+
+
+ @for (lineDiff of beforeLines(); track lineDiff.id; let idx = $index) { +
+
{{ lineDiff.line }}
+
+ } +
- } -
-
-
-
+
+ +
@for (lineDiff of afterLines(); track lineDiff.id; let idx = $index) {
-
{{ lineDiff.line }}
+
{{ lineDiff.lineNumber | lineNumber }}
} -
+
+
+
+
+ @for (lineDiff of afterLines(); track lineDiff.id; let idx = $index) { +
+
{{ lineDiff.line }}
+
+ } +
+
-
+ } } diff --git a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.spec.ts b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.spec.ts index 6430114..6493673 100644 --- a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.spec.ts +++ b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.spec.ts @@ -1,24 +1,199 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Diff, DiffOp } from 'diff-match-patch-ts'; import { SideBySideDiffComponent } from './side-by-side-diff.component'; +import { DiffMatchPatchService } from '../../services/diff-match-patch/diff-match-patch.service'; +import { LineSelectEvent } from '../../common/line-select-event'; +import { LineDiffType } from '../../common/line-diff-type'; -describe('SideBySideDiffComponent', () => { +class DiffMatchPatchServiceMock { + computeLineDiff(before: string, after: string): Promise { + const diffs: Diff[] = []; + if (before === after) { + diffs.push([DiffOp.Equal, before]); + } else { + diffs.push([DiffOp.Delete, before]); + diffs.push([DiffOp.Insert, after]); + } + return Promise.resolve(diffs); + } +} + +describe('SideBySideDiffComponent with Vitest', () => { let component: SideBySideDiffComponent; let fixture: ComponentFixture; + let dmpMock: DiffMatchPatchServiceMock; beforeEach(async () => { + vi.useFakeTimers(); + + dmpMock = new DiffMatchPatchServiceMock(); + await TestBed.configureTestingModule({ imports: [SideBySideDiffComponent], + providers: [{ provide: DiffMatchPatchService, useValue: dmpMock }], }).compileComponents(); fixture = TestBed.createComponent(SideBySideDiffComponent); component = fixture.componentInstance; + fixture.componentRef.setInput('title', 'my-diff.ts'); fixture.componentRef.setInput('before', 'a'); fixture.componentRef.setInput('after', 'b'); fixture.detectChanges(); + + await vi.runAllTimersAsync(); + await fixture.whenStable(); + fixture.detectChanges(); + }); + + afterEach(() => { + vi.useRealTimers(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set title', () => { + const titleEl = fixture.nativeElement.querySelector('.sbs-diff-title-bar'); + expect(titleEl.textContent).toContain('my-diff.ts'); + }); + + it('should handle identical content', async () => { + const text = 'a\nb\nc'; + fixture.componentRef.setInput('before', text); + fixture.componentRef.setInput('after', text); + + fixture.detectChanges(); + await vi.runAllTimersAsync(); + fixture.detectChanges(); + + expect(component.isCalculating()).toBe(false); + expect(component.processedDiff().isContentEqual).toBe(true); + expect(component.beforeLines().length).toBe(0); + expect(component.afterLines().length).toBe(0); + }); + + it('should handle different content', async () => { + const before = 'a\nb'; + const after = 'a\nc'; + vi.spyOn(dmpMock, 'computeLineDiff').mockReturnValue( + Promise.resolve([ + [DiffOp.Equal, 'a\n'], + [DiffOp.Delete, 'b'], + [DiffOp.Insert, 'c'], + ]), + ); + + fixture.componentRef.setInput('before', before); + fixture.componentRef.setInput('after', after); + + fixture.detectChanges(); + await vi.runAllTimersAsync(); + fixture.detectChanges(); + + expect(component.isCalculating()).toBe(false); + expect(component.processedDiff().isContentEqual).toBe(false); + expect(component.beforeLines().length).toBe(3); + expect(component.afterLines().length).toBe(3); + + // Line 1: equal + expect(component.beforeLines()[0].type).toBe(LineDiffType.Equal); + expect(component.beforeLines()[0].line).toBe('a'); + expect(component.afterLines()[0].type).toBe(LineDiffType.Equal); + expect(component.afterLines()[0].line).toBe('a'); + + // Line 2: delete + expect(component.beforeLines()[1].type).toBe(LineDiffType.Delete); + expect(component.beforeLines()[1].line).toBe('b'); + expect(component.afterLines()[1].type).toBe(LineDiffType.Delete); + expect(component.afterLines()[1].line).toBe(null); + + // Line 3: insert + expect(component.beforeLines()[2].type).toBe(LineDiffType.Insert); + expect(component.beforeLines()[2].line).toBe(null); + expect(component.afterLines()[2].type).toBe(LineDiffType.Insert); + expect(component.afterLines()[2].line).toBe('c'); + }); + + it('should emit line select event', async () => { + const emitSpy = vi.spyOn(component.selectedLineChange, 'emit'); + const before = 'a'; + const after = 'b'; + fixture.componentRef.setInput('before', before); + fixture.componentRef.setInput('after', after); + + fixture.detectChanges(); + await vi.runAllTimersAsync(); + fixture.detectChanges(); + + component.selectLine(0); + + const expectedEvent: LineSelectEvent = { + index: 0, + type: LineDiffType.Delete, + lineNumberInOldText: 1, + lineNumberInNewText: null, + line: 'a', + }; + expect(emitSpy).toHaveBeenCalledWith(expect.objectContaining(expectedEvent)); + }); + + it('should expand placeholder on click', async () => { + const before = 'delete-before\nline1\nline2\nline3\nline4\nline5\ndelete-after'; + const after = 'insert-before\nline1\nline2\nline3\nline4\nline5\ninsert-after'; + + vi.spyOn(dmpMock, 'computeLineDiff').mockReturnValue( + Promise.resolve([ + [DiffOp.Delete, 'delete-before'], + [DiffOp.Insert, 'insert-before'], + [DiffOp.Equal, '\nline1\nline2\nline3\nline4\nline5'], // 6 lines when split + [DiffOp.Delete, '\ndelete-after'], + [DiffOp.Insert, '\ninsert-after'], + ]), + ); + + fixture.componentRef.setInput('before', before); + fixture.componentRef.setInput('after', after); + fixture.componentRef.setInput('lineContextSize', 2); + fixture.detectChanges(); + await vi.runAllTimersAsync(); + fixture.detectChanges(); + + const placeholderIndex = component + .beforeLines() + .findIndex((l) => l.type === LineDiffType.Placeholder); + expect(placeholderIndex, 'placeholder should be created').toBeGreaterThan(-1); + + const placeholderLine = component.beforeLines()[placeholderIndex]; + expect(placeholderLine.line).toContain('hidden lines'); + const beforeLineCount = component.beforeLines().length; + + component.selectLine(placeholderIndex); + fixture.detectChanges(); + + expect(component.beforeLines().length, 'number of lines should grow').toBeGreaterThan( + beforeLineCount, + ); + const newPlaceholderIndex = component + .beforeLines() + .findIndex((l) => l.type === LineDiffType.Placeholder); + + expect(newPlaceholderIndex, 'placeholder should be gone').toBe(-1); + }); + + it('should set dynamic line number width', async () => { + const before = 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj'; // 10 lines + const after = 'a\nb\nc'; + fixture.componentRef.setInput('before', before); + fixture.componentRef.setInput('after', after); + fixture.componentRef.setInput('isDynamicLineNumberWidthEnabled', true); + fixture.detectChanges(); + await vi.runAllTimersAsync(); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + // JSDOM doesn't compute styles, so we check if the style property is set. + expect(element.style.getPropertyValue('--ngx-diff-line-number-width')).toBeTruthy(); + }); }); diff --git a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.ts b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.ts index 32dabbe..02df9f0 100644 --- a/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.ts +++ b/projects/ngx-diff/src/lib/components/side-by-side-diff/side-by-side-diff.component.ts @@ -21,6 +21,9 @@ import { LineDiffType } from '../../common/line-diff-type'; import { NgClass } from '@angular/common'; import { LineSelectEvent } from '../../common/line-select-event'; import { StyleCalculatorService } from '../../services/style-calculator/style-calculator.service'; +import { BehaviorSubject, debounceTime, startWith, switchMap } from 'rxjs'; +import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { ProgressBarComponent } from '../progress-bar/progress-bar.component'; interface IDiffCalculation { beforeLineNumber: number; @@ -50,17 +53,11 @@ type LineDiffResult = { }; }; -const transformToString = (value: string | number | boolean | undefined) => { - if (typeof value === 'number' || typeof value === 'boolean') { - return value.toString(); - } - - return value ?? ''; -}; +const transformToString = (value: string | number | boolean | undefined) => value?.toString() ?? ''; @Component({ selector: 'ngx-side-by-side-diff', - imports: [NgClass, LineNumberPipe], + imports: [NgClass, LineNumberPipe, ProgressBarComponent], templateUrl: './side-by-side-diff.component.html', styleUrl: './side-by-side-diff.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -86,6 +83,12 @@ export class SideBySideDiffComponent implements AfterViewInit { public readonly before = input.required(); public readonly after = input.required(); + protected readonly diffData = computed(() => ({ + title: this.title(), + before: transformToString(this.before()), + after: transformToString(this.after()), + })); + /** * The number of lines of context to provide either side of a DiffOp.Insert or DiffOp.Delete diff. * Context is taken from a DiffOp.Equal section. @@ -94,31 +97,43 @@ export class SideBySideDiffComponent implements AfterViewInit { public readonly selectedLineChange = output(); - public readonly isContentEqual = computed(() => this.lineDiffResult().isContentEqual); - public readonly diffSummary = computed(() => this.lineDiffResult().diffSummary); + private readonly isCalculatingSubject = new BehaviorSubject(false); + public readonly isCalculating = toSignal( + this.isCalculatingSubject.asObservable().pipe(debounceTime(50)), + ); - public readonly beforeLines = signal([]); // computed(() => this.lineDiffResult().beforeLines); - public readonly afterLines = signal([]); // computed(() => this.lineDiffResult().afterLines); + public readonly beforeLines = signal([]); + public readonly afterLines = signal([]); public selectedLineIndex?: number; - private readonly beforeText = computed(() => transformToString(this.before())); - private readonly afterText = computed(() => transformToString(this.after())); - - private readonly lineDiffs = computed(() => { - return this.dmp.computeLineDiff(this.beforeText(), this.afterText()); - }); - - private readonly lineDiffResult = computed(() => { - return this.calculateLineDiffs(this.lineDiffs()); - }); + private readonly lineDiffs = toSignal( + toObservable(this.diffData).pipe( + takeUntilDestroyed(), + debounceTime(50), + switchMap(async ({ title, before, after }) => { + this.isCalculatingSubject.next(true); + const diffs = await this.dmp.computeLineDiff(before, after); + return { title, diffs }; + }), + startWith({ title: undefined, diffs: [] }), + ), + { requireSync: true }, + ); + + public readonly processedDiff = computed(() => ({ + ...this.calculateLineDiffs(this.lineDiffs().diffs), + title: this.lineDiffs().title, + })); public constructor() { effect(() => { - this.beforeLines.set(this.lineDiffResult().beforeLines); - }); - effect(() => { - this.afterLines.set(this.lineDiffResult().afterLines); + this.isCalculatingSubject.next(false); + + const { beforeLines, afterLines } = this.processedDiff(); + + this.beforeLines.set(beforeLines); + this.afterLines.set(afterLines); }); } @@ -129,14 +144,14 @@ export class SideBySideDiffComponent implements AfterViewInit { return; } - const lineDiffResult = this.lineDiffResult(); + const { beforeLines, afterLines } = this.processedDiff(); - let maxLineNumber = lineDiffResult.beforeLines.reduce( + let maxLineNumber = beforeLines.reduce( (maxSoFar, entry) => Math.max(maxSoFar, entry.lineNumber ?? 0), 0, ); - maxLineNumber = lineDiffResult.afterLines.reduce( + maxLineNumber = afterLines.reduce( (maxSoFar, entry) => Math.max(maxSoFar, entry.lineNumber ?? 0), maxLineNumber, ); diff --git a/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.html b/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.html index 4c4fa31..0b78826 100644 --- a/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.html +++ b/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.html @@ -1,47 +1,58 @@ -
- @if (title()) { - {{ title() }}  - } - +++ {{ diffSummary().numLinesAdded }}  - --- {{ diffSummary().numLinesRemoved }} -
-@if (isContentEqual()) { -
-
There are no changes to display.
+@if (isCalculating()) { +
+ @if (processedDiff().title) { + {{ processedDiff().title }} + }
+ } @else { -
-
- @for (lineDiff of calculatedDiff(); track lineDiff.id; let idx = $index) { -
-
{{ lineDiff.lineNumberInOldText | lineNumber }}
-
{{ lineDiff.lineNumberInNewText | lineNumber }}
-
- } -
+
+ @if (processedDiff().title) { + {{ processedDiff().title }} + } + + +++ {{ processedDiff().diffSummary.numLinesAdded }} + + + --- {{ processedDiff().diffSummary.numLinesRemoved }} + +
+ @if (processedDiff().isContentEqual) { +
+
There are no changes to display.
-
-
- @for (lineDiff of calculatedDiff(); track lineDiff.id) { + } @else { +
+
+ @for (lineDiff of calculatedDiff(); track lineDiff.id; let idx = $index) {
-
{{ lineDiff.line }}
+
{{ lineDiff.lineNumberInOldText | lineNumber }}
+
{{ lineDiff.lineNumberInNewText | lineNumber }}
} -
+
+
+
+
+ @for (lineDiff of calculatedDiff(); track lineDiff.id) { +
+
{{ lineDiff.line }}
+
+ } +
+
-
+ } } diff --git a/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.spec.ts b/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.spec.ts index 0d74def..7b42400 100644 --- a/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.spec.ts +++ b/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.spec.ts @@ -10,14 +10,13 @@ import { DiffMatchPatchService } from '../../services/diff-match-patch/diff-matc class DiffMatchPatchServiceMock { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars, no-underscore-dangle, id-blacklist, id-match - public computeLineDiff(_oldText: string, _newText: string): Diff[] { - return [ + public computeLineDiff = (_oldText: string, _newText: string): Promise => + Promise.resolve([ [DiffOp.Equal, 'Diff One A\r\nDiff One B\r\n'], [DiffOp.Insert, 'Diff Two A\r\nDiff Two B\r\n'], [DiffOp.Delete, 'Diff Three A\r\nDiff Three B'], [DiffOp.Equal, 'Diff Four A\r\nDiff Four B\r\n'], - ]; - } + ]); } describe('UnifiedDiffComponent', () => { @@ -25,11 +24,12 @@ describe('UnifiedDiffComponent', () => { let fixture: ComponentFixture; beforeEach(async () => { + vi.useFakeTimers(); + await TestBed.configureTestingModule({ imports: [UnifiedDiffComponent, LineNumberPipe], providers: [{ provide: DiffMatchPatchService, useClass: DiffMatchPatchServiceMock }], }).compileComponents(); - fixture = TestBed.createComponent(UnifiedDiffComponent); component = fixture.componentInstance; fixture.componentRef.setInput('before', 'a'); @@ -37,47 +37,61 @@ describe('UnifiedDiffComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => { + vi.useRealTimers(); }); - it('should have 8 line diffs', () => { - expect(component.calculatedDiff().length).toBe(8); - }); + describe('after diff calculation', () => { + beforeEach(async () => { + vi.advanceTimersByTime(100); + await fixture.whenStable(); + fixture.detectChanges(); + }); - it('should have correct line numbers', () => { - const leftLineNumbers = component.calculatedDiff().map((x) => x.lineNumberInOldText); - expect(leftLineNumbers).toEqual([1, 2, null, null, 3, 4, 5, 6]); + it('should create', () => { + expect(component).toBeTruthy(); + }); - const rightLineNumbers = component.calculatedDiff().map((x) => x.lineNumberInNewText); - expect(rightLineNumbers).toEqual([1, 2, 3, 4, null, null, 5, 6]); - }); + it('should have 8 line diffs', () => { + expect(component.calculatedDiff().length).toBe(8); + }); - it('should have correct class annotations', () => { - const classes = component.calculatedDiff().map((x) => x.type); - expect(classes).toEqual([ - LineDiffType.Equal, - LineDiffType.Equal, - LineDiffType.Insert, - LineDiffType.Insert, - LineDiffType.Delete, - LineDiffType.Delete, - LineDiffType.Equal, - LineDiffType.Equal, - ]); - }); + it('should have correct line numbers', () => { + const leftLineNumbers = component.calculatedDiff().map((x) => x.lineNumberInOldText); + expect(leftLineNumbers).toEqual([1, 2, null, null, 3, 4, 5, 6]); - it('should have correct line contents', () => { - const contents = component.calculatedDiff().map((x) => x.line); - expect(contents).toEqual([ - 'Diff One A', - 'Diff One B', - 'Diff Two A', - 'Diff Two B', - 'Diff Three A', - 'Diff Three B', - 'Diff Four A', - 'Diff Four B', - ]); + const rightLineNumbers = component.calculatedDiff().map((x) => x.lineNumberInNewText); + expect(rightLineNumbers).toEqual([1, 2, 3, 4, null, null, 5, 6]); + }); + + it('should have correct class annotations', () => { + const classes = component.calculatedDiff().map((x) => x.type); + const expected = [ + LineDiffType.Equal, + LineDiffType.Equal, + LineDiffType.Insert, + LineDiffType.Insert, + LineDiffType.Delete, + LineDiffType.Delete, + LineDiffType.Equal, + LineDiffType.Equal, + ]; + expect(classes).toEqual(expected); + }); + + it('should have correct line contents', () => { + const contents = component.calculatedDiff().map((x) => x.line); + const expected = [ + 'Diff One A', + 'Diff One B', + 'Diff Two A', + 'Diff Two B', + 'Diff Three A', + 'Diff Three B', + 'Diff Four A', + 'Diff Four B', + ]; + expect(contents).toEqual(expected); + }); }); }); diff --git a/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.ts b/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.ts index 9eedb99..5d2f2e6 100644 --- a/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.ts +++ b/projects/ngx-diff/src/lib/components/unified-diff/unified-diff.component.ts @@ -23,6 +23,9 @@ import { DiffMatchPatchService } from '../../services/diff-match-patch/diff-matc import { LineNumberPipe } from '../../pipes/line-number/line-number.pipe'; import { NgClass } from '@angular/common'; import { StyleCalculatorService } from '../../services/style-calculator/style-calculator.service'; +import { BehaviorSubject, debounceTime, startWith, switchMap } from 'rxjs'; +import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { ProgressBarComponent } from '../progress-bar/progress-bar.component'; type LineDiff = { id: string; @@ -43,17 +46,11 @@ type LineDiffResult = { }; }; -const transformToString = (value: string | number | boolean | undefined) => { - if (typeof value === 'number' || typeof value === 'boolean') { - return value.toString(); - } - - return value ?? ''; -}; +const transformToString = (value: string | number | boolean | undefined) => value?.toString() ?? ''; @Component({ selector: 'ngx-unified-diff', - imports: [NgClass, LineNumberPipe], + imports: [NgClass, LineNumberPipe, ProgressBarComponent], templateUrl: './unified-diff.component.html', styleUrl: './unified-diff.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -79,6 +76,12 @@ export class UnifiedDiffComponent implements AfterViewInit { public readonly before = input.required(); public readonly after = input.required(); + protected readonly diffData = computed(() => ({ + title: this.title(), + before: transformToString(this.before()), + after: transformToString(this.after()), + })); + /** * The number of lines of context to provide either side of a DiffOp.Insert or DiffOp.Delete diff. * Context is taken from a DiffOp.Equal section. @@ -89,24 +92,38 @@ export class UnifiedDiffComponent implements AfterViewInit { public selectedLine?: LineDiff; - public readonly isContentEqual = computed(() => this.lineDiffResult().isContentEqual); + // This needs to be a signal, rather than computed(..) to support alterations when a placeholder is expanded. public readonly calculatedDiff = signal([]); - public readonly diffSummary = computed(() => this.lineDiffResult().diffSummary); - - protected readonly beforeString = computed(() => transformToString(this.before())); - protected readonly afterString = computed(() => transformToString(this.after())); - - protected readonly diffs = computed(() => { - return this.dmp.computeLineDiff(this.beforeString(), this.afterString()); - }); - private readonly lineDiffResult = computed(() => { - return UnifiedDiffComponent.calculateLineDiff(this.diffs(), this.lineContextSize()); - }); + private readonly isCalculatingSubject = new BehaviorSubject(false); + + protected readonly isCalculating = toSignal( + this.isCalculatingSubject.asObservable().pipe(debounceTime(50)), + ); + + protected readonly diffs = toSignal( + toObservable(this.diffData).pipe( + takeUntilDestroyed(), + debounceTime(50), + switchMap(async ({ title, before, after }) => { + this.isCalculatingSubject.next(true); + const diffs = await this.dmp.computeLineDiff(before, after); + return { title, diffs }; + }), + startWith({ title: undefined, diffs: [] }), + ), + { requireSync: true }, + ); + + protected readonly processedDiff = computed(() => ({ + ...UnifiedDiffComponent.calculateLineDiff(this.diffs().diffs, this.lineContextSize()), + title: this.diffs().title, + })); public constructor() { effect(() => { - this.calculatedDiff.set(this.lineDiffResult().calculatedDiff); + this.isCalculatingSubject.next(false); + this.calculatedDiff.set(this.processedDiff().calculatedDiff); }); } @@ -117,7 +134,7 @@ export class UnifiedDiffComponent implements AfterViewInit { return; } - const maxLineNumber = this.lineDiffResult().calculatedDiff.reduce( + const maxLineNumber = this.processedDiff().calculatedDiff.reduce( (maxLineNumber, entry) => Math.max( maxLineNumber, diff --git a/projects/ngx-diff/src/lib/services/diff-match-patch/diff-match-patch.service.ts b/projects/ngx-diff/src/lib/services/diff-match-patch/diff-match-patch.service.ts index 2a332a6..1ed4f33 100644 --- a/projects/ngx-diff/src/lib/services/diff-match-patch/diff-match-patch.service.ts +++ b/projects/ngx-diff/src/lib/services/diff-match-patch/diff-match-patch.service.ts @@ -1,14 +1,131 @@ import { type Diff, DiffMatchPatch } from 'diff-match-patch-ts'; -import { Injectable } from '@angular/core'; +import { inject, Injectable, InjectionToken, OnDestroy } from '@angular/core'; + +export interface IDiffWebWorkerFactory { + createWorker(): Worker | undefined; +} + +interface DiffWorkerSuccessResponse { + id: number; + status: 'success'; + diffs: Diff[]; +} + +interface DiffWorkerErrorResponse { + id: number; + status: 'error'; + error: { message: string }; +} + +export const NGX_DIFF_WEB_WORKER_FACTORY = new InjectionToken('NGX_DIFF_WEB_WORKER_FACTORY'); @Injectable({ providedIn: 'root', }) -export class DiffMatchPatchService { +export class DiffMatchPatchService implements OnDestroy { private readonly dmp = new DiffMatchPatch(); - public computeLineDiff(text1: string, text2: string): Diff[] { - return this.dmp.diff_lineMode(text1, text2); + private readonly promises = new Map< + number, + { resolve: (value: Diff[]) => void; reject: (reason?: unknown) => void } + >(); + + private readonly factory = inject(NGX_DIFF_WEB_WORKER_FACTORY, { + optional: true, + }); + + private worker?: Worker; + private messageId = 0; + + public computeLineDiff(text1: string, text2: string): Promise { + if (this.factory && this.isPotentiallyLongComputation(text1, text2)) { + const worker = this.getOrCreateWorker(); + + if (worker) { + return new Promise((resolve, reject) => { + const id = this.messageId++; + this.promises.set(id, { resolve, reject }); + + try { + worker.postMessage({ id, before: text1, after: text2 }); + } catch (error) { + this.promises.delete(id); + reject(error); + } + }); + } + } + + return new Promise((resolve) => resolve(this.dmp.diff_lineMode(text1, text2))); + } + + public ngOnDestroy(): void { + if (this.worker) { + const error = new Error('DiffMatchPatchService is being destroyed.'); + for (const promise of this.promises.values()) { + promise.reject(error); + } + this.promises.clear(); + + this.worker.terminate(); + this.worker = undefined; + } + } + + private getOrCreateWorker(): Worker | undefined { + if (this.worker) { + return this.worker; + } + + const worker = this.factory?.createWorker(); + if (worker) { + this.worker = worker; + this.worker.onmessage = this.onWorkerMessage.bind(this); + this.worker.onerror = this.onWorkerError.bind(this); + } + return this.worker; + } + + private onWorkerMessage({ + data, + }: MessageEvent): void { + const promise = this.promises.get(data.id); + if (!promise) { + console.error('Received a message from web worker with an unknown id.', data); + return; + } + + if (data.status === 'success') { + promise.resolve(data.diffs); + } else if (data.status === 'error') { + console.error('Web worker error:', data.error); + promise.reject(new Error(data.error.message)); + } + this.promises.delete(data.id); + } + + private onWorkerError(error: ErrorEvent): void { + for (const promise of this.promises.values()) { + promise.reject(error); + } + this.promises.clear(); + + if (this.worker) { + this.worker.terminate(); + this.worker = undefined; + } + } + + private isPotentiallyLongComputation(text1: string, text2: string): boolean { + const numLines1 = this.countNewLines(text1); + const numLines2 = this.countNewLines(text2); + + return numLines1 + numLines2 > 10000; + } + + private countNewLines(input: string): number { + const matches = input.match(/\n/g); + return matches ? matches.length : 0; } } diff --git a/projects/ngx-diff/styles/default-theme.css b/projects/ngx-diff/styles/default-theme.css index c7ce9bd..8e28db6 100644 --- a/projects/ngx-diff/styles/default-theme.css +++ b/projects/ngx-diff/styles/default-theme.css @@ -26,6 +26,9 @@ --ngx-diff-light-mix-percentage: 4%; --ngx-diff-heavy-mix-percentage: 10%; + --ngx-diff-progress-background-color: #dfdfdf; + --ngx-diff-progress-foreground-color: #aaaaaa; + --ngx-diff-inserted-background-color: var(--ngx-diff-insert-color); --ngx-diff-deleted-background-color: var(--ngx-diff-delete-color); --ngx-diff-equal-background-color: var(--ngx-diff-equal-color); @@ -73,4 +76,7 @@ --ngx-diff-mix-color: #fff; --ngx-diff-light-mix-percentage: 4%; --ngx-diff-heavy-mix-percentage: 10%; + + --ngx-diff-progress-background-color: #354a54; + --ngx-diff-progress-foreground-color: #636363; } diff --git a/src/app/app.component.html b/src/app/app.component.html index cf86277..10c6e74 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -34,6 +34,7 @@ (selectedLineChange)="selectedLineChange($event)" />
+ +
{ + let service: DiffWebWorkerFactoryService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DiffWebWorkerFactoryService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/diff-web-worker-factory/diff-web-worker-factory.service.ts b/src/app/services/diff-web-worker-factory/diff-web-worker-factory.service.ts new file mode 100644 index 0000000..eaa97d3 --- /dev/null +++ b/src/app/services/diff-web-worker-factory/diff-web-worker-factory.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { IDiffWebWorkerFactory } from 'ngx-diff'; + +@Injectable() +export class DiffWebWorkerFactoryService implements IDiffWebWorkerFactory { + public createWorker(): Worker | undefined { + if (typeof Worker !== 'undefined') { + return new Worker(new URL('../../web-workers/diff-web-worker.worker', import.meta.url)); + } else { + return undefined; + } + } +} diff --git a/src/app/web-workers/diff-web-worker.worker.ts b/src/app/web-workers/diff-web-worker.worker.ts new file mode 100644 index 0000000..85f1dc2 --- /dev/null +++ b/src/app/web-workers/diff-web-worker.worker.ts @@ -0,0 +1,25 @@ +/// + +import { DiffMatchPatch } from 'diff-match-patch-ts'; + +addEventListener('message', ({ data }) => { + try { + if (typeof data.before !== 'string' || typeof data.after !== 'string') { + throw new TypeError('Input data for diffing must be strings.'); + } + + const dmp = new DiffMatchPatch(); + console.time('dmp'); + const diffs = dmp.diff_lineMode(data.before, data.after); + console.timeEnd('dmp'); + console.log(`${data.before.length} ${data.after.length} length`); + + postMessage({ id: data.id, status: 'success', diffs }); + } catch (error: any) { + postMessage({ + id: data.id, + status: 'error', + error: { message: error.message, stack: error.stack }, + }); + } +}); diff --git a/src/main.ts b/src/main.ts index 7c7f48a..70d39cf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,8 @@ import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; import { AppRoutingModule } from './app/app-routing.module'; import { AppComponent } from './app/app.component'; import { environment } from './environments/environment'; +import { NGX_DIFF_WEB_WORKER_FACTORY } from 'ngx-diff'; +import { DiffWebWorkerFactoryService } from './app/services/diff-web-worker-factory/diff-web-worker-factory.service'; if (environment.production) { enableProdMode(); @@ -13,5 +15,6 @@ bootstrapApplication(AppComponent, { providers: [ importProvidersFrom(BrowserModule, AppRoutingModule), provideZonelessChangeDetection(), + { provide: NGX_DIFF_WEB_WORKER_FACTORY, useClass: DiffWebWorkerFactoryService }, ], }).catch((err) => console.error(err)); diff --git a/tsconfig.worker.json b/tsconfig.worker.json new file mode 100644 index 0000000..0ed426a --- /dev/null +++ b/tsconfig.worker.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/worker", + "lib": [ + "es2018", + "webworker" + ], + "types": [] + }, + "include": [ + "src/**/*.worker.ts" + ] +}