chore(deps): drop three unmaintained dependencies (unorm, find-root, jsonminify) (#7992)

* chore(deps): drop three unmaintained dependencies from core

Remove dependencies whose upstreams are effectively abandoned, replacing
each with a maintained alternative or native API. No behaviour change for
users; reduces the production dependency surface.

- unorm (last publish 2019): replace `UNorm.nfc(s)` in contentcollector
  with native `String.prototype.normalize('NFC')`, available in every
  supported Node and browser. Also drop it from Minify's LIBRARY_WHITELIST.
- find-root (last publish 2017): inline a ~10-line equivalent in
  AbsolutePaths.findEtherpadRoot(), mirroring find-root's semantics
  (closest ancestor containing package.json, throw if none).
- jsonminify (last publish 2021): swap settings parsing to jsonc-parser
  (already used by the admin workspace, actively maintained). The old
  `jsonminify(str).replace(',]', ']').replace(',}', '}')` had two bugs that
  jsonc-parser's allowTrailingComma fixes: String#replace only swapped the
  FIRST trailing comma of each kind, and the blind replace corrupted ',]' /
  ',}' byte sequences inside string values (e.g. URLs).

Added a regression test in settings.ts covering multiple trailing commas
and ',]'/',}' inside string values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: drop stale $unorm bundle entry from tar.json

The unorm removal left `$unorm/lib/unorm.js` listed in the ace2_inner.js
client bundle manifest. With unorm uninstalled, getTar() would point at a
node_modules asset that no longer exists, producing 404s when loading that
bundle. Nothing imports unorm anymore (contentcollector now uses native
String.prototype.normalize), so the entry is dead and removed.

Caught by Qodo review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: update container loadSettings helper to jsonc-parser

tests/container/loadSettings.js is a standalone helper (separate from
node/utils/Settings.ts) that parsed settings.json.docker with jsonminify
directly. Removing jsonminify from dependencies broke the container test
suite with MODULE_NOT_FOUND. Switch it to jsonc-parser to match Settings.ts.

Verified loadSettings() parses settings.json.docker and applies the
container ip/port overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-06-22 09:53:27 +01:00 committed by GitHub
parent 01d0b08a4e
commit 7168f14f0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 101 additions and 49 deletions

37
pnpm-lock.yaml generated
View file

@ -229,9 +229,6 @@ importers:
express-session:
specifier: ^1.19.0
version: 1.19.0
find-root:
specifier: 1.1.0
version: 1.1.0
formidable:
specifier: ^3.5.4
version: 3.5.4
@ -253,9 +250,9 @@ importers:
jsdom:
specifier: ^29.1.1
version: 29.1.1(@noble/hashes@1.8.0)
jsonminify:
specifier: 0.4.2
version: 0.4.2
jsonc-parser:
specifier: ^3.3.1
version: 3.3.1
jsonwebtoken:
specifier: ^9.0.3
version: 9.0.3
@ -373,9 +370,6 @@ importers:
undici:
specifier: ^8.5.0
version: 8.5.0
unorm:
specifier: 1.6.0
version: 1.6.0
wtfnode:
specifier: ^0.10.1
version: 0.10.1
@ -416,9 +410,6 @@ importers:
'@types/jsdom':
specifier: ^28.0.3
version: 28.0.3
'@types/jsonminify':
specifier: ^0.4.3
version: 0.4.3
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
@ -1847,9 +1838,6 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/jsonminify@0.4.3':
resolution: {integrity: sha512-+oz7EbPz1Nwmn/sr3UztgXpRhdFpvFrjGi5ictEYxUri5ZvQMTcdTi36MTfD/gCb1A5xhJKdH8Hwz2uz5k6s9A==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
@ -3326,9 +3314,6 @@ packages:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'}
find-root@1.1.0:
resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@ -3904,10 +3889,6 @@ packages:
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
jsonminify@0.4.2:
resolution: {integrity: sha512-mEtP5ECD0293D+s45JhDutqF5mFCkWY8ClrPFxjSFR2KUoantofky7noSzyKnAnD9Gd8pXHZSUd5bgzLDUBbfA==}
engines: {node: '>=0.8.0', npm: '>=1.1.0'}
jsonschema-draft4@1.0.0:
resolution: {integrity: sha512-sBV3UnQPRiyCTD6uzY/Oao2Yohv6KKgQq7zjPwjFHeR6scg/QSXnzDxdugsGaLQDmFUrUlTbMYdEE+72PizhGA==}
@ -5509,10 +5490,6 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unorm@1.6.0:
resolution: {integrity: sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==}
engines: {node: '>= 0.4.0'}
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
@ -7120,8 +7097,6 @@ snapshots:
'@types/json5@0.0.29': {}
'@types/jsonminify@0.4.3': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
@ -8774,8 +8749,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
find-root@1.1.0: {}
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@ -9430,8 +9403,6 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jsonminify@0.4.2: {}
jsonschema-draft4@1.0.0: {}
jsonschema@1.2.4: {}
@ -11181,8 +11152,6 @@ snapshots:
universalify@2.0.1: {}
unorm@1.6.0: {}
unpipe@1.0.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.2):

View file

@ -19,6 +19,7 @@
* limitations under the License.
*/
import log4js from 'log4js';
import fs from 'fs';
import path from 'path';
import _ from 'underscore';
@ -30,6 +31,25 @@ const absPathLogger = log4js.getLogger('AbsolutePaths');
*/
let etherpadRoot: string|null = null;
/**
* Walks up the directory tree from `start`, returning the closest ancestor
* directory (including `start` itself) that contains a package.json. Replaces
* the unmaintained `find-root` package, mirroring its semantics: it throws if
* no package.json is found before reaching the filesystem root.
*
* @param {string} start - The directory to start searching from.
* @return {string} The closest ancestor directory containing a package.json.
*/
const findRoot = (start: string): string => {
let dir = start;
for (;;) {
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
const parent = path.dirname(dir);
if (parent === dir) throw new Error('package.json not found in path');
dir = parent;
}
};
/**
* If stringArray's last elements are exactly equal to lastDesiredElements,
* returns a copy in which those last elements are popped, or false otherwise.
@ -79,7 +99,6 @@ export const findEtherpadRoot = () => {
return etherpadRoot;
}
const findRoot = require('find-root');
const foundRoot = findRoot(__dirname);
const splitFoundRoot = foundRoot.split(path.sep);

View file

@ -43,7 +43,6 @@ const LIBRARY_WHITELIST = [
'split-grid',
'tinycon',
'underscore',
'unorm',
];
// What follows is a terrible hack to avoid loop-back within the server.

View file

@ -35,7 +35,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {argv} from './Cli'
import jsonminify from 'jsonminify';
import {parse as parseJsonc, printParseErrorCode, ParseError} from 'jsonc-parser';
import log4js from 'log4js';
import {createHash} from 'node:crypto';
import randomString from './randomstring';
@ -116,9 +116,18 @@ const parseSettings = (settingsFilename: string, isSettings: boolean) => {
}
try {
settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}');
// jsonc-parser tolerates comments and trailing commas, so settings files
// can stay annotated. Unlike the old jsonminify + naive ',]'/',}' string
// replace, it fixes *every* trailing comma (not just the first of each
// kind) and never mangles those sequences when they appear inside strings.
const errors: ParseError[] = [];
const settings = parseJsonc(settingsStr, errors, {allowTrailingComma: true});
const settings = JSON.parse(settingsStr);
if (errors.length > 0) {
const {error, offset} = errors[0];
throw new Error(`${printParseErrorCode(error)} at offset ${offset}`);
}
if (settings === undefined) throw new Error('file is empty or not valid JSON');
logger.info(`${settingsType} loaded from: ${settingsFilename}`);

View file

@ -67,7 +67,6 @@
, "skiplist.js"
, "colorutils.js"
, "undomodule.js"
, "$unorm/lib/unorm.js"
, "contentcollector.js"
, "changesettracker.js"
, "linestylefilter.js"

View file

@ -42,7 +42,6 @@
"express": "^5.2.1",
"express-rate-limit": "^8.5.1",
"express-session": "^1.19.0",
"find-root": "1.1.0",
"formidable": "^3.5.4",
"html-to-docx": "^1.8.0",
"htmlparser2": "^12.0.0",
@ -50,7 +49,7 @@
"jose": "^6.2.3",
"js-cookie": "^3.0.8",
"jsdom": "^29.1.1",
"jsonminify": "0.4.2",
"jsonc-parser": "^3.3.1",
"jsonwebtoken": "^9.0.3",
"jwt-decode": "^4.0.0",
"languages4translatewiki": "0.1.3",
@ -90,7 +89,6 @@
"ueberdb2": "6.1.13",
"underscore": "1.13.8",
"undici": "^8.5.0",
"unorm": "1.6.0",
"wtfnode": "^0.10.1"
},
"bin": {
@ -110,7 +108,6 @@
"@types/jquery": "^4.0.1",
"@types/js-cookie": "^3.0.6",
"@types/jsdom": "^28.0.3",
"@types/jsonminify": "^0.4.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/mime-types": "^3.0.1",
"@types/mocha": "^10.0.9",

View file

@ -31,12 +31,13 @@ import Op from "./Op";
const _MAX_LIST_LEVEL = 16;
import AttributeMap from './AttributeMap';
import UNorm from 'unorm';
import {subattribution} from './Changeset';
import {SmartOpAssembler} from "./SmartOpAssembler";
const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s);
// NFC-normalize via the native String API (replaces the unmaintained `unorm`
// polyfill; String.prototype.normalize is available in every supported runtime).
const sanitizeUnicode = (s: string) => s.normalize('NFC');
const tagName = (n) => n.tagName && n.tagName.toLowerCase();
// supportedElems are Supported natively within Etherpad and don't require a plugin
const supportedElems = new Set([

View file

@ -278,4 +278,62 @@ describe(__filename, function () {
assert.strictEqual(over!.privacy.pluginCatalog, false);
});
});
// Regression test for the jsonminify -> jsonc-parser migration.
// The old parser was `jsonminify(str).replace(',]', ']').replace(',}', '}')`,
// which had two correctness bugs that jsonc-parser (allowTrailingComma) fixes:
// 1. String#replace with a string needle only swaps the FIRST match, so a
// file with more than one trailing comma of the same kind stayed invalid.
// 2. The blind ',]' / ',}' replace also corrupted those byte sequences when
// they appeared *inside* a string value (e.g. a URL or literal text).
describe('JSONC settings parsing (jsonminify -> jsonc-parser)', function () {
const fs = require('fs');
const os = require('os');
let tmpFile: string;
const writeTmp = (contents: string) => {
tmpFile = path.join(os.tmpdir(), `ep-settings-jsonc-${process.pid}.json`);
fs.writeFileSync(tmpFile, contents);
return tmpFile;
};
afterEach(function () {
if (tmpFile) { try { fs.unlinkSync(tmpFile); } catch (e) { /* ignore */ } }
});
it('strips comments and tolerates multiple trailing commas', function () {
const file = writeTmp(`// leading line comment
/* block comment */
{
"list": [
"a",
"b",
],
"nested": [
[1, 2,],
[3, 4,],
],
"obj": {
"x": 1,
"y": 2,
},
}`);
const s: any = exportedForTestingOnly.parseSettings(file, true);
assert.deepEqual(s.list, ['a', 'b']);
assert.deepEqual(s.nested, [[1, 2], [3, 4]]);
assert.deepEqual(s.obj, {x: 1, y: 2});
});
it('does not corrupt ",]" / ",}" sequences inside string values', function () {
const file = writeTmp(`{
"url": "http://example.com/a,]b,}c",
"text": "trailing-comma-like ,] and ,} must survive"
}`);
const s: any = exportedForTestingOnly.parseSettings(file, true);
// The old replace() would have stripped the comma and produced
// "http://example.com/a]b}c" here.
assert.strictEqual(s.url, 'http://example.com/a,]b,}c');
assert.strictEqual(s.text, 'trailing-comma-like ,] and ,} must survive');
});
});
});

View file

@ -13,15 +13,16 @@
*/
const fs = require('fs');
const jsonminify = require('jsonminify');
const {parse: parseJsonc} = require('jsonc-parser');
function loadSettings() {
let settingsStr = fs.readFileSync(`${__dirname}/../../../settings.json.docker`).toString();
// try to parse the settings
try {
if (settingsStr) {
settingsStr = jsonminify(settingsStr).replace(',]', ']').replace(',}', '}');
const settings = JSON.parse(settingsStr);
// jsonc-parser tolerates the comments and trailing commas in the docker
// settings file, matching node/utils/Settings.ts.
const settings = parseJsonc(settingsStr, [], {allowTrailingComma: true});
// custom settings for running in a container
settings.ip = 'localhost';