searxng/client/simple/vite.config.js
2025-03-09 22:07:12 +01:00

355 lines
10 KiB
JavaScript

/**
* CONFIG: https://vite.dev/config/
*/
import { resolve, relative } from "node:path";
import { Buffer } from 'buffer';
import path from 'path';
import { defineConfig } from "vite";
import stylelint from "vite-plugin-stylelint";
import { viteStaticCopy } from "vite-plugin-static-copy";
import { plg_svg2png } from "./tools/plg.js";
import { plg_svg2svg } from "./tools/plg.js";
import fs from 'node:fs/promises';
import crypto from 'node:crypto';
const ROOT = "../.."; // root of the git reposetory
const PATH = {
dist: resolve(ROOT, "searx/static/themes/simple"),
// dist: resolve(ROOT, "client/simple/dist"),
src: "src",
modules: "node_modules",
brand: "src/brand",
static: resolve(ROOT, "client/simple/static"),
leaflet: resolve(ROOT, "client/simple/node_modules/leaflet/dist"),
templates: resolve(ROOT, "searx/templates/simple"),
};
const svg2svg_opts = {
plugins: [
{ name: "preset-default" },
"sortAttrs",
"convertStyleToAttrs",
]
};
const svg2svg_favicon_opts = {
plugins: [
{ name: "preset-default" },
"sortAttrs",
]
};
function AddSearxNGHashes(options = {}) {
const {
fileName = "hashes.json",
exclude = [],
include_without_hashes = []
} = options;
let outDir = null;
// Helper: recursively get all files (not directories) within `dir`.
async function getAllFiles(dir) {
let entries = await fs.readdir(dir, { withFileTypes: true });
let files = [];
for (const entry of entries) {
const fullPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
files = files.concat(await getAllFiles(fullPath));
} else {
files.push(fullPath);
}
}
// Separate out `.map` files so they end up last
const mapFiles = files.filter((file) => file.endsWith(".map"));
const otherFiles = files.filter((file) => !file.endsWith(".map"));
return [...otherFiles, ...mapFiles];
}
function replacePathsInBuffer(body, mapping) {
// Convert the Buffer to a string (assuming UTF-8)
let content = body.toString("utf-8");
// Perform replacements
for (const logicalPath of Object.keys(mapping)) {
const hashedPath = mapping[logicalPath];
content = content.replaceAll(logicalPath, hashedPath);
}
// Convert the modified string back to a Buffer
return Buffer.from(content, "utf-8");
}
return {
name: "recursive-hash-manifest-plugin",
apply: "build",
// Capture the final "outDir" from the resolved Vite config
configResolved(config) {
outDir = config.build.outDir;
},
// "closeBundle" is called after everything (including other async tasks) is done writing
async closeBundle() {
// Check if the outDir is set (from configResolved)
if (outDir === null) {
return
}
// Get a list of every file in the output directory
let allFiles = await getAllFiles(outDir);
// Optionally exclude certain files
const exclusionSet = new Set([...exclude, fileName]);
allFiles = allFiles.filter((filePath) => {
const relPath = relative(outDir, filePath);
return !exclusionSet.has(relPath);
});
// Compute a hash for each file
const assets = {};
const var_mapping = {}
const hash_override = {}
for (const filePath of allFiles) {
const relPath = relative(outDir, filePath);
// Get the shortHash
let shortHash;
if (include_without_hashes.includes(relative(outDir, filePath))) {
shortHash = "";
} else if (Object.prototype.hasOwnProperty.call(hash_override, filePath)) {
shortHash = hash_override[filePath];
} else {
const fileBuf = await fs.readFile(filePath);
const hashSum = crypto.createHash("sha256").update(fileBuf).digest("hex");
shortHash = "." + hashSum.slice(0, 8);
hash_override[filePath + ".map"] = shortHash;
}
// Prepare to build a new file path
const dirName = path.dirname(filePath);
let newFilePath;
let varPath = null;
// Special handling for *.js.map
if (filePath.endsWith(".js.map")) {
const baseName = path.basename(filePath, ".js.map");
newFilePath = path.join(dirName, `${baseName}${shortHash}.js.map`);
}
// Special handling for *.css.map
else if (filePath.endsWith(".css.map")) {
const baseName = path.basename(filePath, ".css.map");
newFilePath = path.join(dirName, `${baseName}${shortHash}.css.map`);
}
// Otherwise, rename as usual
else {
const extName = path.extname(filePath);
const baseName = path.basename(filePath, extName);
newFilePath = path.join(dirName, `${baseName}${shortHash}${extName}`);
//
varPath = `${baseName}.SEARXNG_HASH${extName}`;
var_mapping[varPath] = `${baseName}${shortHash}${extName}`;
if (filePath.endsWith(".js")) {
var_mapping[`//# sourceMappingURL=${baseName}${extName}.map`] = `//# sourceMappingURL=${baseName}${shortHash}${extName}.map`;
}
}
// New relative path
const newRelPath = relative(outDir, newFilePath);
assets[relPath] = newRelPath;
}
// Step 2: Once the manifest is all set, read back files that might reference others
// and replace placeholders with hashed paths.
for (const filePath of allFiles) {
const extName = path.extname(filePath);
if (![".css", ".js", ".html"].includes(extName)) {
continue;
}
const originalBuf = await fs.readFile(filePath);
const replacedBuf = replacePathsInBuffer(originalBuf, var_mapping);
await fs.writeFile(filePath, replacedBuf);
}
// Step 3: rename the original files to their hashed filenames
for (const filePath of allFiles) {
const relPath = path.relative(outDir, filePath);
const newRelPath = assets[relPath];
const newFilePath = path.join(outDir, newRelPath);
await fs.rename(filePath, newFilePath);
}
// Write out `assets.json`
const assetsPath = resolve(outDir, fileName);
await fs.writeFile(assetsPath, JSON.stringify(assets, null, 2), "utf-8");
},
};
}
export default defineConfig({
root: PATH.src,
mode: "production",
// mode: "development",
// FIXME: missing CCS sourcemaps!!
// see: https://github.com/vitejs/vite/discussions/13845#discussioncomment-11992084
//
// what I have tried so far (see config below):
//
// - build.sourcemap
// - esbuild.sourcemap
// - css.preprocessorOptions.less.sourceMap
css: {
devSourcemap: true,
preprocessorOptions: {
less: {
// FIXME: missing CCS sourcemaps!!
sourceMap: {
outputSourceFiles: true,
sourceMapURL: (name) => { const s = name.split('/'); return s[s.length - 1] + '.map'; },
},
// env: 'development',
// relativeUrls: true,
// javascriptEnabled: true,
},
},
}, // end: css
esbuild : {
// FIXME: missing CCS sourcemaps!!
sourcemap: true
},
build: {
manifest: "manifest.json",
emptyOutDir: true,
assetsDir: "",
outDir: PATH.dist,
// FIXME: missing CCS sourcemaps!!
sourcemap: true,
// https://vite.dev/config/build-options.html#build-cssminify
cssMinify: true,
// cssMinify: "esbuild",
minify: "esbuild",
rollupOptions: {
input: {
// build CSS files
"css/searxng.min.css": PATH.src + "/less/style-ltr.less",
"css/searxng-rtl.min.css": PATH.src + "/less/style-rtl.less",
"css/rss.min.css": PATH.src + "/less/rss.less",
// build JS files
"js/searxng.head.min": PATH.src + "/js/searxng.head.js",
"js/searxng.min": PATH.src + "/js/searxng.js",
},
// file naming conventions / pathnames are relative to outDir (PATH.dist)
output: {
entryFileNames: "[name].js",
chunkFileNames: "[name].js",
assetFileNames: "[name].[ext]",
// Vite does not support "rollupOptions.output.sourcemap".
// Please use "build.sourcemap" instead.
// sourcemap: true,
},
},
}, // end: build
plugins: [
stylelint({
build: true,
emitWarningAsError: true,
fix: true,
}),
// Leaflet
viteStaticCopy({
targets: [
{ src: PATH.leaflet + "/leaflet.{js,js.map}", dest: PATH.dist + "/js" },
{ src: PATH.leaflet + "/images/*.png", dest: PATH.dist + "/css/images/" },
{ src: PATH.leaflet + "/*.{css,css.map}", dest: PATH.dist + "/css" },
{ src: PATH.static + "/**/*", dest: PATH.dist },
]
}),
// -- svg images
plg_svg2svg(
[
{ src: PATH.src + "/svg/empty_favicon.svg", dest: PATH.dist + "/img/empty_favicon.svg" },
{ src: PATH.src + "/svg/select-dark.svg", dest: PATH.dist + "/img/select-dark.svg" },
{ src: PATH.src + "/svg/select-light.svg", dest: PATH.dist + "/img/select-light.svg" },
],
svg2svg_opts,
),
// SearXNG brand (static)
plg_svg2png(
[
{ src: PATH.brand + "/searxng-wordmark.svg", dest: PATH.dist + "/img/favicon.png" },
{ src: PATH.brand + "/searxng.svg", dest: PATH.dist + "/img/searxng.png" },
],
),
// -- svg
plg_svg2svg(
[
{ src: PATH.brand + "/searxng.svg", dest: PATH.dist + "/img/searxng.svg" },
{ src: PATH.brand + "/img_load_error.svg", dest: PATH.dist + "/img/img_load_error.svg" },
],
svg2svg_opts,
),
// -- favicon
plg_svg2svg(
[ { src: PATH.brand + "/searxng-wordmark.svg", dest: PATH.dist + "/img/favicon.svg" } ],
svg2svg_favicon_opts,
),
// -- simple templates
plg_svg2svg(
[
{ src: PATH.brand + "/searxng-wordmark.svg", dest: PATH.templates + "/searxng-wordmark.min.svg" },
],
svg2svg_opts
),
// -- create assets.json and add hashes to files
AddSearxNGHashes({
fileName: "assets.json",
exclude: [
".gitattributes",
"manifest.json"
],
include_without_hashes: [
"css/images/layers-2x.png",
"css/images/layers.png",
"css/images/marker-icon-2x.png",
"css/images/marker-icon.png",
"css/images/marker-shadow.png",
]
}),
] // end: plugins
});