From da530d645fa9f9dd77890097dae8e32ac356a7b4 Mon Sep 17 00:00:00 2001 From: tdurieux Date: Tue, 8 Sep 2026 16:31:59 +0200 Subject: [PATCH] fix: clear dependency vulnerabilities without overrides --- gulpfile.js | 44 +++++---------- package-lock.json | 119 ++++++++++++++------------------------- package.json | 7 +-- test/asset-build.test.js | 78 +++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 113 deletions(-) create mode 100644 test/asset-build.test.js diff --git a/gulpfile.js b/gulpfile.js index b1ba846..72cd3f3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,11 +1,11 @@ -const { src, dest, parallel } = require("gulp"); +const { src, dest, parallel, series } = require("gulp"); const uglify = require("gulp-uglify"); const concat = require("gulp-concat"); -var order = require("gulp-order"); +const order = require("ordered-read-streams"); +const { pipeline } = require("node:stream"); const cleanCss = require("gulp-clean-css"); const crypto = require("crypto"); const fs = require("fs"); -const path = require("path"); const coreJsFiles = [ "public/script/external/angular.min.js", @@ -60,38 +60,26 @@ function hashFile(filePath) { return crypto.createHash("md5").update(content).digest("hex").slice(0, 10); } +// Gulp 5 does not preserve array order. Read each asset in its declared order +// so libraries precede their plugins and application code, and CSS keeps its cascade. +function orderedSrc(files) { + return order(files.map(file => src(file))); +} + function buildCoreJs(cb) { - src(coreJsFiles) - .pipe(order(coreJsFiles, { base: "./" })) - .pipe(concat("core.min.js")) - .pipe(uglify()) - .pipe(dest("public/script")) - .on("end", cb); + pipeline(orderedSrc(coreJsFiles), concat("core.min.js"), uglify(), dest("public/script"), cb); } function buildVendorJs(cb) { - src(vendorJsFiles) - .pipe(order(vendorJsFiles, { base: "./" })) - .pipe(concat("vendor.min.js")) - .pipe(uglify()) - .pipe(dest("public/script")) - .on("end", cb); + pipeline(orderedSrc(vendorJsFiles), concat("vendor.min.js"), uglify(), dest("public/script"), cb); } function buildMermaidJs(cb) { - src(mermaidFiles) - .pipe(concat("mermaid.min.js")) - .pipe(dest("public/script")) - .on("end", cb); + pipeline(src(mermaidFiles), concat("mermaid.min.js"), dest("public/script"), cb); } function buildCss(cb) { - src(cssFiles) - .pipe(order(cssFiles, { base: "./" })) - .pipe(concat("all.min.css")) - .pipe(cleanCss()) - .pipe(dest("public/css")) - .on("end", cb); + pipeline(orderedSrc(cssFiles), concat("all.min.css"), cleanCss(), dest("public/css"), cb); } function writeManifest(cb) { @@ -119,8 +107,4 @@ function writeManifest(cb) { const buildAssets = parallel(buildCoreJs, buildVendorJs, buildMermaidJs, buildCss); -exports.default = function (cb) { - buildAssets(function () { - writeManifest(cb); - }); -}; +exports.default = series(buildAssets, writeManifest); diff --git a/package-lock.json b/package-lock.json index 2eee0db..39aafdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "rate-limit-redis": "^4.2.0", "re2js": "^2.8.6", "redis": "^4.6.13", - "sanitize-html": "^2.17.2", + "sanitize-html": "^2.17.7", "ts-custom-error": "^3.3.1", "unzip-stream": "^0.3.1" }, @@ -70,12 +70,12 @@ "gulp": "^5.0.0", "gulp-clean-css": "^4.3.0", "gulp-concat": "^2.6.1", - "gulp-order": "^1.2.0", "gulp-uglify": "^3.0.2", "http-proxy-middleware": "^3.0.5", "knip": "^5.1.0", "mocha": "^12.0.0", "nodemon": "^3.1.0", + "ordered-read-streams": "^2.0.0", "ts-node": "^10.9.2", "typescript": "^5.4.3", "typescript-eslint": "^8.58.2" @@ -10931,32 +10931,6 @@ "node": ">= 0.10" } }, - "node_modules/gulp-order": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gulp-order/-/gulp-order-1.2.0.tgz", - "integrity": "sha512-RduLhxbEye1NT6fnvev3792VUsZA8F+5zRFczAgpOSELz587Fv1Nn7l4ZiaiQzFPV7aECh1WRc2dPWjjcxCa0g==", - "dev": true, - "dependencies": { - "minimatch": "~3.0.4", - "stable": "~0.1.8", - "through": "~2.3.8" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/gulp-order/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/gulp-uglify": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gulp-uglify/-/gulp-uglify-3.0.2.tgz", @@ -12610,15 +12584,16 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://factory.endorlabs.com/v1/namespaces/endor-labs.package-firewall/firewall/npm/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -12943,6 +12918,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ordered-read-streams": { + "version": "2.0.0", + "resolved": "https://factory.endorlabs.com/v1/namespaces/endor-labs.package-firewall/firewall/npm/ordered-read-streams/-/ordered-read-streams-2.0.0.tgz", + "integrity": "sha512-YT9wHHV9mB/qadhWnBsC57JKhAMA22/aR+RwZRgcf4K4Q7IIfmSsnYGxgiu9LVZP3wddRAm5pfYkzkmBb+HuwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/oxc-resolver": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", @@ -13292,9 +13280,9 @@ } }, "node_modules/postcss": { - "version": "8.5.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", - "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "version": "8.5.28", + "resolved": "https://factory.endorlabs.com/v1/namespaces/endor-labs.package-firewall/firewall/npm/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "funding": [ { "type": "opencollective", @@ -13309,8 +13297,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -14254,13 +14243,6 @@ "memory-pager": "^1.0.2" } }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "dev": true - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -21595,7 +21577,7 @@ "requires": { "cron-parser": "4.9.0", "ioredis": "5.10.1", - "msgpackr": "^1.11.12", + "msgpackr": "1.11.12", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1" @@ -23314,28 +23296,6 @@ } } }, - "gulp-order": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gulp-order/-/gulp-order-1.2.0.tgz", - "integrity": "sha512-RduLhxbEye1NT6fnvev3792VUsZA8F+5zRFczAgpOSELz587Fv1Nn7l4ZiaiQzFPV7aECh1WRc2dPWjjcxCa0g==", - "dev": true, - "requires": { - "minimatch": "~3.0.4", - "stable": "~0.1.8", - "through": "~2.3.8" - }, - "dependencies": { - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "gulp-uglify": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/gulp-uglify/-/gulp-uglify-3.0.2.tgz", @@ -24480,9 +24440,9 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==" + "version": "3.3.18", + "resolved": "https://factory.endorlabs.com/v1/namespaces/endor-labs.package-firewall/firewall/npm/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==" }, "natural-compare": { "version": "1.4.0", @@ -24702,6 +24662,15 @@ "wcwidth": "^1.0.1" } }, + "ordered-read-streams": { + "version": "2.0.0", + "resolved": "https://factory.endorlabs.com/v1/namespaces/endor-labs.package-firewall/firewall/npm/ordered-read-streams/-/ordered-read-streams-2.0.0.tgz", + "integrity": "sha512-YT9wHHV9mB/qadhWnBsC57JKhAMA22/aR+RwZRgcf4K4Q7IIfmSsnYGxgiu9LVZP3wddRAm5pfYkzkmBb+HuwA==", + "dev": true, + "requires": { + "streamx": "^2.12.5" + } + }, "oxc-resolver": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", @@ -24945,11 +24914,11 @@ } }, "postcss": { - "version": "8.5.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", - "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "version": "8.5.28", + "resolved": "https://factory.endorlabs.com/v1/namespaces/endor-labs.package-firewall/firewall/npm/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "requires": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } @@ -25580,12 +25549,6 @@ "memory-pager": "^1.0.2" } }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, "standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", diff --git a/package.json b/package.json index e7bab5b..e5e92ce 100644 --- a/package.json +++ b/package.json @@ -67,13 +67,10 @@ "rate-limit-redis": "^4.2.0", "re2js": "^2.8.6", "redis": "^4.6.13", - "sanitize-html": "^2.17.2", + "sanitize-html": "^2.17.7", "ts-custom-error": "^3.3.1", "unzip-stream": "^0.3.1" }, - "overrides": { - "msgpackr": "^1.11.12" - }, "devDependencies": { "@eslint/js": "^10.0.1", "@types/archiver": "^5.3.4", @@ -97,12 +94,12 @@ "gulp": "^5.0.0", "gulp-clean-css": "^4.3.0", "gulp-concat": "^2.6.1", - "gulp-order": "^1.2.0", "gulp-uglify": "^3.0.2", "http-proxy-middleware": "^3.0.5", "knip": "^5.1.0", "mocha": "^12.0.0", "nodemon": "^3.1.0", + "ordered-read-streams": "^2.0.0", "ts-node": "^10.9.2", "typescript": "^5.4.3", "typescript-eslint": "^8.58.2" diff --git a/test/asset-build.test.js b/test/asset-build.test.js new file mode 100644 index 0000000..8526f47 --- /dev/null +++ b/test/asset-build.test.js @@ -0,0 +1,78 @@ +const { expect } = require("chai"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const vm = require("node:vm"); +const { spawnSync } = require("node:child_process"); +const process = require("node:process"); + +const gulpfile = path.resolve(__dirname, "../gulpfile.js"); +const source = fs.readFileSync(gulpfile, "utf8"); +const groups = Object.fromEntries( + [...source.matchAll(/const (\w+Files) = \[([\s\S]*?)\];/g)].map(match => [ + match[1], [...match[2].matchAll(/"([^"]+)"/g)].map(file => file[1]), + ]) +); + +describe("asset build", function () { + this.timeout(15000); + let directory; + + beforeEach(function () { + directory = fs.mkdtempSync(path.join(os.tmpdir(), "anonymous-assets-")); + for (const file of Object.values(groups).flat()) { + const target = path.join(directory, file); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, file.endsWith(".css") + ? `.cascade { color: ${file.endsWith("/style.css") ? "red" : "blue"}; }` + : `globalThis.assetOrder.push(${JSON.stringify(file)});`); + } + }); + + afterEach(function () { + fs.rmSync(directory, { recursive: true, force: true }); + }); + + function build() { + return spawnSync(process.execPath, [ + "-e", + `require(${JSON.stringify(gulpfile)}).default(error => { + if (error) { console.error(error); process.exitCode = 1; } + });`, + ], { cwd: directory, encoding: "utf8", timeout: 10000 }); + } + + it("preserves script dependencies and CSS precedence and hashes completed assets", function () { + const result = build(); + expect(result.status, result.stderr).to.equal(0); + for (const [bundle, group] of [["core", "coreJsFiles"], ["vendor", "vendorJsFiles"], ["mermaid", "mermaidFiles"]]) { + const context = { assetOrder: [] }; + vm.runInNewContext(fs.readFileSync(path.join(directory, `public/script/${bundle}.min.js`), "utf8"), context); + expect(context.assetOrder).to.deep.equal(groups[group]); + } + expect(fs.readFileSync(path.join(directory, "public/css/all.min.css"), "utf8")).to.equal(".cascade{color:#00f}".repeat(groups.cssFiles.length - 1) + ".cascade{color:red}"); + const manifest = JSON.parse(fs.readFileSync(path.join(directory, "public/asset-manifest.json"), "utf8")); + expect(Object.keys(manifest)).to.have.length(4); + for (const [name, hashed] of Object.entries(manifest)) { + const content = fs.readFileSync(path.join(directory, "public", name.endsWith(".css") ? "css" : "script", name)); + const hash = require("node:crypto").createHash("md5").update(content).digest("hex").slice(0, 10); + expect(hashed).to.equal(name.replace(".", `.${hash}.`)); + } + }); + + it("fails on missing input without publishing a manifest", function () { + fs.unlinkSync(path.join(directory, groups.coreJsFiles[0])); + const result = build(); + expect(result.status).not.to.equal(0); + expect(result.stderr).to.include("File not found"); + expect(fs.existsSync(path.join(directory, "public/asset-manifest.json"))).to.equal(false); + }); + + it("fails on invalid JavaScript without publishing a manifest", function () { + fs.writeFileSync(path.join(directory, groups.vendorJsFiles[0]), "function {"); + const result = build(); + expect(result.status).not.to.equal(0); + expect(result.stderr).to.include("uglify"); + expect(fs.existsSync(path.join(directory, "public/asset-manifest.json"))).to.equal(false); + }); +});