Replace deprecated and unmaintained packages

- Removed mkdirp in favor of native fs.mkdir({ recursive: true })
- Replaced ecstatic (unmaintained since 2021) with sirv
- Replaced jsonpath (security vulnerability) with jsonpath-plus
  - Added jsonpathQuery/jsonpathApply helpers in app/common/util.mjs
- Updated sharp: 0.32.6 → 0.34.5
- Updated puppeteer-core: 23.8.0 → 24.37.3

Vulnerabilities reduced from 40 to 1 (only remaining: axios in
threads-api transitive dependency).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Isenhower 2026-02-15 11:05:49 -08:00
parent e6004154b9
commit 8780463c66
11 changed files with 687 additions and 539 deletions

View File

@ -1,6 +1,5 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import mkdirp from 'mkdirp';
export default class ValueCache export default class ValueCache
{ {
constructor(key) { constructor(key) {
@ -48,7 +47,7 @@ export default class ValueCache
let cachedAt = new Date; let cachedAt = new Date;
let serialized = JSON.stringify({ expires, data, cachedAt }, undefined, 2); let serialized = JSON.stringify({ expires, data, cachedAt }, undefined, 2);
await mkdirp(path.dirname(this.path)); await fs.mkdir(path.dirname(this.path), { recursive: true });
await fs.writeFile(this.path, serialized); await fs.writeFile(this.path, serialized);
} }
} }

View File

@ -1,4 +1,5 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { JSONPath } from 'jsonpath-plus';
export function getTopOfCurrentHour(date = null) { export function getTopOfCurrentHour(date = null) {
date ??= new Date; date ??= new Date;
@ -88,6 +89,16 @@ export function getXRankSeasonId(id) {
* @param {number} expiresIn - Seconds until expiry * @param {number} expiresIn - Seconds until expiry
* @returns {number} Timestamp to expire the cache (5 minutes early) * @returns {number} Timestamp to expire the cache (5 minutes early)
*/ */
export function jsonpathQuery(data, path) {
return JSONPath({ path, json: data });
}
export function jsonpathApply(data, path, fn) {
JSONPath({ path, json: data, resultType: 'all', callback: (result) => {
result.parent[result.parentProperty] = fn(result.value);
}});
}
export function calculateCacheExpiry(expiresIn) { export function calculateCacheExpiry(expiresIn) {
let expires = Date.now() + expiresIn * 1000; let expires = Date.now() + expiresIn * 1000;

View File

@ -1,6 +1,5 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import mkdirp from 'mkdirp';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import prefixedConsole from '../common/prefixedConsole.mjs'; import prefixedConsole from '../common/prefixedConsole.mjs';
import { normalizeSplatnetResourcePath } from '../common/util.mjs'; import { normalizeSplatnetResourcePath } from '../common/util.mjs';
@ -70,7 +69,7 @@ export default class ImageProcessor
throw new Error(`Invalid image response code: ${result.status}`); throw new Error(`Invalid image response code: ${result.status}`);
} }
await mkdirp(path.dirname(this.localPath(destination))); await fs.mkdir(path.dirname(this.localPath(destination)), { recursive: true });
await fs.writeFile(this.localPath(destination), result.body); await fs.writeFile(this.localPath(destination), result.body);
} catch (e) { } catch (e) {
this.console.error(`Image download failed for ${destination}`, e); this.console.error(`Image download failed for ${destination}`, e);

View File

@ -1,7 +1,6 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import mkdirp from 'mkdirp'; import { jsonpathQuery } from '../common/util.mjs';
import jsonpath from 'jsonpath';
import get from 'lodash/get.js'; import get from 'lodash/get.js';
import set from 'lodash/set.js'; import set from 'lodash/set.js';
import pLimit from 'p-limit'; import pLimit from 'p-limit';
@ -41,7 +40,7 @@ export class LocalizationProcessor {
*dataIterations(data) { *dataIterations(data) {
for (let ruleset of this.rulesetIterations()) { for (let ruleset of this.rulesetIterations()) {
for (let node of jsonpath.query(data, ruleset.node)) { for (let node of jsonpathQuery(data, ruleset.node)) {
if (!node) continue; if (!node) continue;
let id = get(node, ruleset.id); let id = get(node, ruleset.id);
@ -92,7 +91,7 @@ export class LocalizationProcessor {
data = JSON.stringify(data, undefined, space); data = JSON.stringify(data, undefined, space);
await mkdirp(path.dirname(this.filename)); await fs.mkdir(path.dirname(this.filename), { recursive: true });
await fs.writeFile(this.filename, data); await fs.writeFile(this.filename, data);
} }

View File

@ -1,8 +1,6 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { Console } from 'node:console'; import { Console } from 'node:console';
import mkdirp from 'mkdirp';
import jsonpath from 'jsonpath';
import ical from 'ical-generator'; import ical from 'ical-generator';
import pFilter from 'p-filter'; import pFilter from 'p-filter';
import prefixedConsole from '../../common/prefixedConsole.mjs'; import prefixedConsole from '../../common/prefixedConsole.mjs';
@ -11,7 +9,7 @@ import ImageProcessor from '../ImageProcessor.mjs';
import NsoClient from '../../splatnet/NsoClient.mjs'; import NsoClient from '../../splatnet/NsoClient.mjs';
import { locales, regionalLocales, defaultLocale } from '../../../src/common/i18n.mjs'; import { locales, regionalLocales, defaultLocale } from '../../../src/common/i18n.mjs';
import { LocalizationProcessor } from '../LocalizationProcessor.mjs'; import { LocalizationProcessor } from '../LocalizationProcessor.mjs';
import { deriveId, getDateParts, getTopOfCurrentHour } from '../../common/util.mjs'; import { deriveId, getDateParts, getTopOfCurrentHour, jsonpathQuery, jsonpathApply } from '../../common/util.mjs';
export default class DataUpdater export default class DataUpdater
{ {
name = null; name = null;
@ -120,7 +118,7 @@ export default class DataUpdater
deriveIds(data) { deriveIds(data) {
for (let expression of this.derivedIds) { for (let expression of this.derivedIds) {
jsonpath.apply(data, expression, deriveId); jsonpathApply(data, expression, deriveId);
} }
} }
@ -157,14 +155,14 @@ export default class DataUpdater
// This JSONPath library is completely synchronous, so we have to // This JSONPath library is completely synchronous, so we have to
// build a mapping here after transforming all URLs. // build a mapping here after transforming all URLs.
let mapping = {}; let mapping = {};
for (let url of jsonpath.query(data, expression)) { for (let url of jsonpathQuery(data, expression)) {
let [path, publicUrl] = await this.imageProcessor.process(url); let [path, publicUrl] = await this.imageProcessor.process(url);
mapping[url] = publicUrl; mapping[url] = publicUrl;
images[publicUrl] = path; images[publicUrl] = path;
} }
// Now apply the URL transformations // Now apply the URL transformations
jsonpath.apply(data, expression, url => mapping[url]); jsonpathApply(data, expression, url => mapping[url]);
} }
await ImageProcessor.onIdle(); await ImageProcessor.onIdle();
@ -217,7 +215,7 @@ export default class DataUpdater
} }
async writeFile(file, data) { async writeFile(file, data) {
await mkdirp(path.dirname(file)); await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, data); await fs.writeFile(file, data);
} }

View File

@ -1,7 +1,6 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import jsonpath from 'jsonpath';
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import { getFestId } from '../../common/util.mjs'; import { getFestId, jsonpathQuery, jsonpathApply } from '../../common/util.mjs';
import ValueCache from '../../common/ValueCache.mjs'; import ValueCache from '../../common/ValueCache.mjs';
import { regionTokens } from '../../splatnet/NsoClient.mjs'; import { regionTokens } from '../../splatnet/NsoClient.mjs';
import FestivalRankingUpdater from './FestivalRankingUpdater.mjs'; import FestivalRankingUpdater from './FestivalRankingUpdater.mjs';
@ -70,7 +69,7 @@ export default class FestivalUpdater extends DataUpdater
let data = await this.splatnet(locale).getFestRecordDataPage(cursor); let data = await this.splatnet(locale).getFestRecordDataPage(cursor);
// Grab the nodes from the current page // Grab the nodes from the current page
result.data.festRecords.nodes.push(...jsonpath.query(data, '$..festRecords.edges.*.node')); result.data.festRecords.nodes.push(...jsonpathQuery(data, '$..festRecords.edges.*.node'));
// Update the cursor and next page indicator // Update the cursor and next page indicator
cursor = data.data.festRecords.pageInfo.endCursor; cursor = data.data.festRecords.pageInfo.endCursor;
@ -103,7 +102,7 @@ export default class FestivalUpdater extends DataUpdater
} }
deriveFestivalIds(data) { deriveFestivalIds(data) {
jsonpath.apply(data, '$..nodes.*', node => ({ jsonpathApply(data, '$..nodes.*', node => ({
'__splatoon3ink_id': getFestId(node.id), '__splatoon3ink_id': getFestId(node.id),
...node, ...node,
})); }));

View File

@ -1,5 +1,5 @@
import http from 'http'; import http from 'http';
import ecstatic from 'ecstatic'; import sirv from 'sirv';
export default class HttpServer export default class HttpServer
{ {
@ -16,7 +16,7 @@ export default class HttpServer
return resolve(); return resolve();
} }
const handler = ecstatic({ root: './dist' }); const handler = sirv('./dist');
this.#server = http.createServer(handler); this.#server = http.createServer(handler);
this.#server.on('listening', () => resolve()); this.#server.on('listening', () => resolve());
this.#server.listen(); this.#server.listen();

View File

@ -1,5 +1,4 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import mkdirp from 'mkdirp';
import Client from './Client.mjs'; import Client from './Client.mjs';
export default class FileWriter extends Client { export default class FileWriter extends Client {
@ -9,7 +8,7 @@ export default class FileWriter extends Client {
dir = 'temp'; dir = 'temp';
async send(status, generator) { async send(status, generator) {
await mkdirp(this.dir); await fs.mkdir(this.dir, { recursive: true });
if (status.media?.length > 0) { if (status.media?.length > 0) {
let imgFilename = `${this.dir}/${generator.key}.png`; let imgFilename = `${this.dir}/${generator.key}.png`;

View File

@ -1,5 +1,4 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import mkdirp from 'mkdirp';
import Client from './Client.mjs'; import Client from './Client.mjs';
export default class ImageWriter extends Client { export default class ImageWriter extends Client {
@ -13,7 +12,7 @@ export default class ImageWriter extends Client {
return; return;
} }
await mkdirp(this.dir); await fs.mkdir(this.dir, { recursive: true });
let imgFilename = `${this.dir}/${generator.key}.png`; let imgFilename = `${this.dir}/${generator.key}.png`;
await fs.writeFile(imgFilename, status.media[0].file); await fs.writeFile(imgFilename, status.media[0].file);

1164
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,20 +34,19 @@
"console-stamp": "^3.0.6", "console-stamp": "^3.0.6",
"cron": "^2.1.0", "cron": "^2.1.0",
"dotenv": "^16.0.2", "dotenv": "^16.0.2",
"ecstatic": "^4.1.4",
"ical-generator": "^3.6.0", "ical-generator": "^3.6.0",
"jsonpath": "^1.1.1", "jsonpath-plus": "^10.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"masto": "^7.10.1", "masto": "^7.10.1",
"mkdirp": "^1.0.4",
"nxapi": "^1.6.1-next.170", "nxapi": "^1.6.1-next.170",
"p-filter": "^4.1.0", "p-filter": "^4.1.0",
"p-limit": "^6.1.0", "p-limit": "^6.1.0",
"p-queue": "^8.0.1", "p-queue": "^8.0.1",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"puppeteer-core": "^23.8.0", "puppeteer-core": "^24.37.3",
"s3-sync-client": "^4.3.1", "s3-sync-client": "^4.3.1",
"sharp": "^0.32.0", "sharp": "^0.34.5",
"sirv": "^3.0.2",
"threads-api": "^1.4.0", "threads-api": "^1.4.0",
"twitter-api-v2": "^1.29.0", "twitter-api-v2": "^1.29.0",
"vue": "^3.5.28", "vue": "^3.5.28",