update packages to Fix npm audit vulnerabilities (#35810)

* Fix npm audit vulnerabilities and replace image-webpack-loader with sharp

* remove dead webp config and restore SVG optimization via svgoMinify

* fix import order

* removed unused gif from testing, and add missing peer dependency

* make sure only svgs are parsed

* scope svgoMinify to src- SVGs via loader rule

* remove redundant npm overrides per review feedback

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Pablo Vélez
2026-05-22 17:57:53 +02:00
committed by GitHub
parent 834a86b5e3
commit b0185d9817
9 changed files with 8697 additions and 6222 deletions
@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify that webpack-bundled static image assets are not broken by
* the image-minimizer-webpack-plugin (sharp) migration.
*
* Sharp runs at build time and compresses PNG/JPEG/SVG assets bundled into the
* app. If it mis-encodes a file the asset will either 404, return a wrong
* content-type, or decode to a zero-width image in the browser.
*
* This test catches that by reloading the page and asserting:
* - no static asset request returns 4xx/5xx
* - no <img> in the DOM has naturalWidth === 0 (failed to decode)
*/
test('app loads without any broken image assets on the main channel view', {tag: '@image_assets'}, async ({pw}) => {
const {user} = await pw.initSetup();
const {page, channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto();
await channelsPage.toBeVisible();
// # Collect image/font load errors on reload so the response listener is
// active before any requests fire.
const failedImageUrls: string[] = [];
page.on('response', (response) => {
const url = response.url();
const isImage = /\.(png|jpg|jpeg|svg|gif|woff2|woff)(\?|$)/.test(url);
if (isImage && response.status() >= 400) {
failedImageUrls.push(`${response.status()} ${url}`);
}
});
await page.reload();
await channelsPage.toBeVisible();
// * No image/font requests should return 4xx or 5xx
expect(failedImageUrls, `Failed asset requests:\n${failedImageUrls.join('\n')}`).toHaveLength(0);
// * No <img> element should have naturalWidth === 0 (means the file was
// served but the browser could not decode it — typical of a sharp corruption)
const brokenImages = await page.evaluate(() => {
return Array.from(document.querySelectorAll('img'))
.filter((img) => img.complete && img.naturalWidth === 0 && Boolean(img.src))
.map((img) => img.src);
});
expect(brokenImages, `Broken <img> elements found:\n${brokenImages.join('\n')}`).toHaveLength(0);
});
@@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@mattermost/playwright-lib';
/**
* @objective Verify that the pdfjs cmaps path fix in webpack.config.js works.
*
* Context: PR #35810 changed the copy-webpack-plugin entry for pdfjs cmaps from
* a fragile relative path to one resolved via require.resolve():
*
* Before: {from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'}
* After: {from: path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps'), to: 'cmaps'}
*
* The old path broke with npm workspace hoisting — pdfjs-dist gets installed at
* the root node_modules, not channels/node_modules, so the relative path resolves
* to nothing and copy-webpack-plugin silently copies zero files.
*
* A 404 on identity-h means the cmaps directory was not copied to /static/cmaps/.
*/
test('pdfjs cmaps are copied to /static/cmaps/ and served correctly', {tag: '@pdf_preview'}, async ({pw}) => {
const {user} = await pw.initSetup();
const {page} = await pw.testBrowser.login(user);
const baseUrl = new URL(page.url()).origin;
// # identity-h is a standard CMap that pdfjs requests for non-Latin PDFs.
// Its presence confirms copy-webpack-plugin found and copied the cmaps directory.
const cmapUrl = `${baseUrl}/static/cmaps/identity-h`;
const response = await page.request.get(cmapUrl);
// * 200 = require.resolve() found the right path and cmaps were copied.
// * 404 = the path in webpack.config.js is wrong or copy-webpack-plugin skipped it.
expect(
response.status(),
`CMap not found at ${cmapUrl} — pdfjs cmaps were not copied to /static/cmaps/. ` +
'Check the copy-webpack-plugin entry in webpack.config.js.',
).toBe(200);
const body = await response.body();
expect(body.length, 'identity-h CMap file must not be empty').toBeGreaterThan(0);
});
+1
View File
@@ -25,6 +25,7 @@ const config = {
'^mattermost-redux/test/(.*)$':
'<rootDir>/src/packages/mattermost-redux/test/$1',
'^mattermost-redux/(.*)$': '<rootDir>/src/packages/mattermost-redux/src/$1',
'^pdfjs-dist/.*': '<rootDir>/src/tests/pdfjs_mock.ts',
'^.+\\.(jpg|jpeg|png|apng|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/src/tests/image_url_mock.json',
'^.+\\.(css|less|scss)$': 'identity-obj-proxy',
+6 -6
View File
@@ -50,7 +50,7 @@
"katex": "0.16.21",
"localforage": "1.10.0",
"localforage-observable": "2.1.1",
"lodash": "4.17.23",
"lodash": "4.18.1",
"luxon": "3.6.1",
"mark.js": "8.11.1",
"marked": "github:mattermost/marked#e4a8785014b26ba9f637c1fdab23e340961c6a03",
@@ -59,7 +59,7 @@
"monaco-editor": "0.52.2",
"monaco-editor-webpack-plugin": "7.1.0",
"p-queue": "7.3.0",
"pdfjs-dist": "4.4.168",
"pdfjs-dist": "4.10.38",
"process": "0.11.10",
"prop-types": "15.8.1",
"react": "18.2.0",
@@ -134,7 +134,7 @@
"@types/tinycolor2": "1.4.6",
"@types/zen-observable": "0.8.7",
"babel-plugin-styled-components": "2.1.4",
"copy-webpack-plugin": "11.0.0",
"copy-webpack-plugin": "14.0.0",
"emoji-datasource": "6.1.1",
"emoji-datasource-apple": "6.1.1",
"emoji-datasource-google": "6.1.1",
@@ -143,9 +143,9 @@
"html-loader": "5.1.0",
"html-webpack-plugin": "5.5.0",
"identity-obj-proxy": "3.0.0",
"image-webpack-loader": "8.1.0",
"imagemin-gifsicle": "7.0.0",
"imagemin-mozjpeg": "9.0.0",
"image-minimizer-webpack-plugin": "5.0.0",
"svgo": "2.8.2",
"sharp": "0.34.5",
"jest": "30.1.3",
"jest-canvas-mock": "2.5.0",
"jest-cli": "30.1.3",
@@ -1,21 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import React from 'react';
// All pdfjs-dist/* imports map to the same file via moduleNameMapper.
// Use the imported reference directly — separate jest.mock() calls for different
// pdfjs paths resolve to the same key and silently overwrite each other.
import PDFPreview from 'components/pdf_preview';
import type {Props} from 'components/pdf_preview';
import {render, renderWithContext, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
const mockGetDocument = jest.fn();
jest.mock('pdfjs-dist/legacy/build/pdf.mjs', () => ({
getDocument: (params: unknown) => mockGetDocument(params),
}));
jest.mock('pdfjs-dist/build/pdf.worker.min.mjs', () => ({}));
const mockGetDocument = pdfjsLib.getDocument as jest.Mock;
describe('component/PDFPreview', () => {
const requiredProps: Props = {
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// pdfjs-dist 4.x ships ESM-only builds that use import.meta.url, which Jest's
// CommonJS runtime cannot parse. This mock replaces all pdfjs-dist imports in
// tests so that components which transitively depend on pdf_preview.tsx compile
// without requiring the native pdfjs binary pipeline.
//
// This is intentional: running real pdfjs in jsdom would require mocking canvas,
// workers, and CMap fetches anyway — the mock is the right level of abstraction
// for unit tests. For pdf_preview.tsx component tests, extend this mock as needed.
export const GlobalWorkerOptions = {workerSrc: ''};
export const getDocument = jest.fn(() => ({
promise: Promise.resolve({
numPages: 1,
getPage: jest.fn(() =>
Promise.resolve({
getViewport: jest.fn(() => ({width: 100, height: 100, scale: 1})),
render: jest.fn(() => ({promise: Promise.resolve(), cancel: jest.fn()})),
cleanup: jest.fn(),
}),
),
destroy: jest.fn(),
}),
destroy: jest.fn(),
}));
export const version = '4.10.38';
+45 -9
View File
@@ -9,6 +9,7 @@ const url = require('url');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const webpack = require('webpack');
@@ -106,14 +107,6 @@ var config = {
{
test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/,
type: 'asset/resource',
use: [
// Skip image optimizations during development to speed up build time
!DEV && {
loader: 'image-webpack-loader',
options: {},
},
],
},
{
test: /\.apng$/,
@@ -205,7 +198,7 @@ var config = {
{from: 'src/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff', to: 'fonts'},
{from: 'src/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff2', to: 'fonts'},
{from: 'src/fonts/open-sans-v18-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-600.woff', to: 'fonts'},
{from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'},
{from: path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps'), to: 'cmaps'},
],
}),
@@ -423,6 +416,49 @@ if (DEV) {
// Production mode configuration
config.mode = 'production';
config.devtool = 'source-map';
// Optimize SVGs from src/ at module-build time so vendor SVG fonts (e.g. font-awesome)
// are never passed to svgo. Vendor SVGs get content-hashed names at emit time, making
// them indistinguishable from src/ assets in the optimization.minimizer phase.
config.module.rules.push({
test: /\.svg$/i,
include: [path.resolve(__dirname, 'src')],
enforce: 'pre',
loader: ImageMinimizerPlugin.loader,
options: {
minimizer: {
implementation: ImageMinimizerPlugin.svgoMinify,
options: {
encodeOptions: {
multipass: true,
plugins: ['preset-default'],
},
},
},
},
});
// Optimize images in production builds.
// GIFs are excluded from sharp: animated GIFs (e.g. Customize-Your-Experience.gif)
// lose frames or grow in size when re-encoded by sharp.
config.optimization = {
...config.optimization,
minimizer: [
'...',
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
filter: (source, sourcePath) => !sourcePath.endsWith('.gif'),
options: {
encodeOptions: {
jpeg: {mozjpeg: true},
png: {},
},
},
},
}),
],
};
}
const env = {};
+8492 -6196
View File
File diff suppressed because it is too large Load Diff
+22 -4
View File
@@ -47,13 +47,13 @@
"eslint-plugin-react": "7.34.0",
"eslint-plugin-react-hooks": "4.6.0",
"mini-css-extract-plugin": "2.7.5",
"patch-package": "8.0.0",
"patch-package": "8.0.1",
"sass": "1.80.5",
"sass-loader": "16.0.2",
"strip-ansi": "7.1.0",
"style-loader": "4.0.0",
"typescript-eslint-language-service": "5.0.5",
"webpack": "5.103.0",
"webpack": "5.106.2",
"webpack-cli": "6.0.1",
"webpack-dev-server": "5.2.2"
},
@@ -62,7 +62,7 @@
"jest": "30.1.3"
},
"@giphy/react-components": {
"styled-components": "5.3.6"
"styled-components": "5.3.7"
},
"@types/redux-mock-store": {
"redux": "5.0.1"
@@ -82,7 +82,25 @@
},
"redux-persist": {
"redux": "5.0.1"
}
},
"ajv@>=7.0.0-alpha.0 <8.18.0": "8.20.0",
"bn.js@>=5.0.0 <5.2.3": "5.2.3",
"bn.js@<4.12.3": "4.12.3",
"brace-expansion@<1.1.13": "1.1.14",
"brace-expansion@>=2.0.0 <2.0.3": "2.1.0",
"fast-xml-parser@>=4.0.0-beta.0 <4.5.4": "4.5.5",
"flatted": "3.4.2",
"follow-redirects": "1.16.0",
"immutable": "4.3.8",
"js-yaml": "4.1.1",
"node-forge": "1.4.0",
"path-to-regexp@<0.1.13": "0.1.13",
"picomatch@<2.3.2": "2.3.2",
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
"postcss": "8.5.13",
"rollup@<2.80.0": "2.80.0",
"yaml@<1.10.3": "1.10.3",
"yaml@>=2.0.0 <2.8.3": "2.8.4"
},
"workspaces": [
"channels",