mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-04 11:57:16 +00:00
Bootstrap 5.1.3, Font Awesome 6.0.0, Socket.IO 4.7.2 and Chart.js 4.4.0 are now served from static/vendor/ at the versions the CDNs served, so every page works on a LAN with no internet. Only map tiles still need it. The Content-Security-Policy drops jsDelivr and cdnjs. Every vendored package, the map stack and login fonts included, is pinned in scripts/web-vendor/package.json and its lockfile. sync.mjs copies the listed files into static/, strips the source-map comment, empties each directory it owns first, and regenerates the version table in static/vendor/README.md; it reproduces the previously vendored files byte for byte. Font Awesome ships only its woff2 fonts, which every supported browser takes before the .ttf. Dependabot watches that directory: security fixes as advisories land, grouped minor and patch releases monthly, majors one per PR. Dependabot can't regenerate the vendored files, so vendor-refresh.yml adds them to its PRs as a second commit and dispatches the tests, since a GITHUB_TOKEN push doesn't trigger them. It runs on pull_request_target, so it uses the base branch's sync.mjs, spells out its commands instead of running the PR's npm scripts, and installs with install scripts disabled. A new CI job re-runs the sync and fails if anything under static/ changes. Tests now check that no template loads a script, stylesheet or font from another host, that every vendored file a template names exists, and that the CSP allows no CDN host. In Chrome, 12 pages load with no external request, failed load or console error.
145 lines
6.2 KiB
JavaScript
145 lines
6.2 KiB
JavaScript
// Copy the web viewer's front-end libraries from node_modules into
|
|
// modules/web_viewer/static/, at the versions pinned in package.json.
|
|
//
|
|
// cd scripts/web-vendor && npm run vendor
|
|
//
|
|
// Every file is the unmodified npm build except that a trailing
|
|
// sourceMappingURL comment is removed, since the source maps aren't vendored.
|
|
// Each destination directory is owned by this script: it is emptied before the
|
|
// copy, so a file dropped upstream doesn't linger. The version table in
|
|
// static/vendor/README.md is regenerated from the installed packages.
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const STATIC = path.resolve(HERE, '../../modules/web_viewer/static');
|
|
const README = path.join(STATIC, 'vendor/README.md');
|
|
|
|
// package -> { destination file under static/: source file in the package }
|
|
const LIBRARIES = {
|
|
bootstrap: {
|
|
'vendor/bootstrap/bootstrap.min.css': 'dist/css/bootstrap.min.css',
|
|
'vendor/bootstrap/bootstrap.bundle.min.js': 'dist/js/bootstrap.bundle.min.js',
|
|
'vendor/bootstrap/LICENSE': 'LICENSE',
|
|
},
|
|
// The CSS lists a .ttf after each .woff2; every browser the viewer supports
|
|
// takes the woff2, so the .ttf files are left out.
|
|
'@fortawesome/fontawesome-free': {
|
|
'vendor/fontawesome/css/all.min.css': 'css/all.min.css',
|
|
'vendor/fontawesome/webfonts/fa-brands-400.woff2': 'webfonts/fa-brands-400.woff2',
|
|
'vendor/fontawesome/webfonts/fa-regular-400.woff2': 'webfonts/fa-regular-400.woff2',
|
|
'vendor/fontawesome/webfonts/fa-solid-900.woff2': 'webfonts/fa-solid-900.woff2',
|
|
'vendor/fontawesome/webfonts/fa-v4compatibility.woff2': 'webfonts/fa-v4compatibility.woff2',
|
|
'vendor/fontawesome/LICENSE.txt': 'LICENSE.txt',
|
|
},
|
|
'socket.io-client': {
|
|
'vendor/socket.io/socket.io.min.js': 'dist/socket.io.min.js',
|
|
'vendor/socket.io/LICENSE': 'LICENSE',
|
|
},
|
|
// chart.umd.js is already minified; there is no .min.js in the package.
|
|
'chart.js': {
|
|
'vendor/chart.js/chart.umd.js': 'dist/chart.umd.js',
|
|
'vendor/chart.js/LICENSE.md': 'LICENSE.md',
|
|
},
|
|
leaflet: {
|
|
'vendor/leaflet/leaflet.js': 'dist/leaflet.js',
|
|
'vendor/leaflet/leaflet.css': 'dist/leaflet.css',
|
|
'vendor/leaflet/images/layers.png': 'dist/images/layers.png',
|
|
'vendor/leaflet/images/layers-2x.png': 'dist/images/layers-2x.png',
|
|
'vendor/leaflet/images/marker-icon.png': 'dist/images/marker-icon.png',
|
|
'vendor/leaflet/images/marker-icon-2x.png': 'dist/images/marker-icon-2x.png',
|
|
'vendor/leaflet/images/marker-shadow.png': 'dist/images/marker-shadow.png',
|
|
'vendor/leaflet/LICENSE': 'LICENSE',
|
|
},
|
|
'maplibre-gl': {
|
|
'vendor/maplibre-gl/maplibre-gl.js': 'dist/maplibre-gl.js',
|
|
'vendor/maplibre-gl/maplibre-gl.css': 'dist/maplibre-gl.css',
|
|
'vendor/maplibre-gl/LICENSE.txt': 'LICENSE.txt',
|
|
},
|
|
'@maplibre/maplibre-gl-leaflet': {
|
|
'vendor/maplibre-gl-leaflet/leaflet-maplibre-gl.js': 'leaflet-maplibre-gl.js',
|
|
'vendor/maplibre-gl-leaflet/LICENSE': 'LICENSE',
|
|
},
|
|
'vis-network': {
|
|
'vendor/vis-network/vis-network.min.js': 'standalone/umd/vis-network.min.js',
|
|
'vendor/vis-network/vis-network.min.css': 'styles/vis-network.min.css',
|
|
'vendor/vis-network/LICENSE-MIT': 'LICENSE-MIT',
|
|
},
|
|
'js-sha256': {
|
|
'vendor/js-sha256/sha256.min.js': 'build/sha256.min.js',
|
|
'vendor/js-sha256/LICENSE.txt': 'LICENSE.txt',
|
|
},
|
|
// The login page's fonts: variable, Latin subset.
|
|
'@fontsource-variable/dm-sans': {
|
|
'fonts/dm-sans-latin-opsz-normal.woff2': 'files/dm-sans-latin-opsz-normal.woff2',
|
|
'fonts/LICENSE-DM-Sans.txt': 'LICENSE',
|
|
},
|
|
'@fontsource-variable/outfit': {
|
|
'fonts/outfit-latin-wght-normal.woff2': 'files/outfit-latin-wght-normal.woff2',
|
|
'fonts/LICENSE-Outfit.txt': 'LICENSE',
|
|
},
|
|
};
|
|
|
|
const SOURCE_MAP_COMMENT = /(?:\/\/# sourceMappingURL=\S+|\/\*# sourceMappingURL=\S+ \*\/)\s*$/;
|
|
|
|
function packageDir(name) {
|
|
return path.join(HERE, 'node_modules', name);
|
|
}
|
|
|
|
function readManifest(name) {
|
|
return JSON.parse(fs.readFileSync(path.join(packageDir(name), 'package.json'), 'utf8'));
|
|
}
|
|
|
|
function topDir(dest) {
|
|
const parts = dest.split('/');
|
|
return parts[0] === 'vendor' ? `vendor/${parts[1]}` : parts[0];
|
|
}
|
|
|
|
const pinned = JSON.parse(fs.readFileSync(path.join(HERE, 'package.json'), 'utf8')).dependencies;
|
|
for (const name of Object.keys(LIBRARIES)) {
|
|
if (!(name in pinned)) throw new Error(`${name} is not a dependency in package.json`);
|
|
const installed = readManifest(name).version;
|
|
if (installed !== pinned[name]) {
|
|
throw new Error(`${name} ${installed} is installed but package.json pins ${pinned[name]}; run npm ci`);
|
|
}
|
|
}
|
|
for (const name of Object.keys(pinned)) {
|
|
if (!(name in LIBRARIES)) throw new Error(`${name} is in package.json but has no files listed in sync.mjs`);
|
|
}
|
|
|
|
const owned = new Set(Object.values(LIBRARIES).flatMap((files) => Object.keys(files).map(topDir)));
|
|
for (const dir of owned) fs.rmSync(path.join(STATIC, dir), { recursive: true, force: true });
|
|
|
|
for (const [name, files] of Object.entries(LIBRARIES)) {
|
|
for (const [dest, src] of Object.entries(files)) {
|
|
const out = path.join(STATIC, dest);
|
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
const from = path.join(packageDir(name), src);
|
|
if (/\.(js|css)$/.test(src)) {
|
|
fs.writeFileSync(out, fs.readFileSync(from, 'utf8').replace(SOURCE_MAP_COMMENT, ''));
|
|
} else {
|
|
fs.copyFileSync(from, out);
|
|
}
|
|
}
|
|
}
|
|
|
|
const rows = Object.entries(LIBRARIES).map(([name, files]) => {
|
|
const { version, license } = readManifest(name);
|
|
const dirs = [...new Set(Object.keys(files).map(topDir))].map((d) => `\`${d}/\``).join(', ');
|
|
return `| \`${name}\` | ${version} | ${license} | ${dirs} |`;
|
|
});
|
|
const table = [
|
|
'<!-- vendor-table:start (generated by scripts/web-vendor/sync.mjs) -->',
|
|
'| Package | Version | License | Served from `static/` |',
|
|
'|---|---|---|---|',
|
|
...rows,
|
|
'<!-- vendor-table:end -->',
|
|
].join('\n');
|
|
const readme = fs.readFileSync(README, 'utf8');
|
|
const marked = /<!-- vendor-table:start[^>]*-->[\s\S]*?<!-- vendor-table:end -->/;
|
|
if (!marked.test(readme)) throw new Error(`${README} has no vendor-table markers`);
|
|
fs.writeFileSync(README, readme.replace(marked, table));
|
|
|
|
console.log(`Vendored ${Object.keys(LIBRARIES).length} packages into ${path.relative(process.cwd(), STATIC)}/`);
|