378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
"use strict";
|
|
|
|
const gulp = require("gulp");
|
|
const sass = require("gulp-sass")(require("sass"));
|
|
const less = require("gulp-less");
|
|
const concat = require("gulp-concat");
|
|
const cleanCSS = require("gulp-clean-css");
|
|
const htmlmin = require("gulp-htmlmin");
|
|
const terser = require("gulp-terser");
|
|
const gulpif = require("gulp-if");
|
|
const merge2 = require("merge2");
|
|
|
|
const copyconfig = require("./copyconfig.json");
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const { Transform, Writable } = require("stream");
|
|
|
|
// Node pipeline (promise-based)
|
|
let pipeline;
|
|
try {
|
|
({ pipeline } = require("stream/promises"));
|
|
} catch {
|
|
pipeline = require("stream").promises.pipeline;
|
|
}
|
|
|
|
// --------------------------
|
|
// Config loaders
|
|
// --------------------------
|
|
function loadBundleConfig() {
|
|
delete require.cache[require.resolve("./bdlconfig.json")];
|
|
return require("./bdlconfig.json");
|
|
}
|
|
|
|
const terserOptions = {
|
|
parse: { ecma: 2020 },
|
|
compress: { ecma: 2020 },
|
|
mangle: true,
|
|
output: { ecma: 2020 },
|
|
};
|
|
|
|
const regex = {
|
|
css: /\.css$/i,
|
|
html: /\.(html|htm)$/i,
|
|
js: /\.js$/i,
|
|
};
|
|
|
|
// --------------------------
|
|
// Helpers
|
|
// --------------------------
|
|
function safeUnlink(filePath) {
|
|
try {
|
|
if (fs.existsSync(filePath)) {
|
|
fs.unlinkSync(filePath);
|
|
console.log("Deleted:", filePath);
|
|
}
|
|
} catch (err) {
|
|
console.warn("Could not delete file:", filePath, "-", err.message);
|
|
}
|
|
}
|
|
|
|
function handleTerserError(err) {
|
|
console.error("========================================");
|
|
console.error("TERSER ERROR:", err && err.message ? err.message : err);
|
|
if (err && (err.filename || err.fileName)) console.error("File:", err.filename || err.fileName);
|
|
if (err && (err.line != null || err.col != null)) console.error("Line:", err.line, "Col:", err.col);
|
|
console.error(String(err));
|
|
console.error("========================================");
|
|
this.emit("end");
|
|
}
|
|
|
|
/**
|
|
* Safe, non-hanging replacement for gulp-preservetime.
|
|
* Use AFTER gulp.dest().
|
|
*/
|
|
function preservetimeSafe() {
|
|
return new Transform({
|
|
objectMode: true,
|
|
transform(file, _enc, cb) {
|
|
try {
|
|
if (!file || !file.path || !file.stat) return cb(null, file);
|
|
if (typeof file.isDirectory === "function" && file.isDirectory()) return cb(null, file);
|
|
|
|
const atime =
|
|
file.stat.atime instanceof Date ? file.stat.atime : new Date(file.stat.atimeMs ?? Date.now());
|
|
const mtime =
|
|
file.stat.mtime instanceof Date ? file.stat.mtime : new Date(file.stat.mtimeMs ?? Date.now());
|
|
|
|
fs.utimes(file.path, atime, mtime, () => cb(null, file));
|
|
} catch (_e) {
|
|
cb(null, file);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function logSink(prefix) {
|
|
return new Writable({
|
|
objectMode: true,
|
|
write(file, _enc, cb) {
|
|
try {
|
|
if (file && file.path) {
|
|
const rel = path.relative(process.cwd(), file.path);
|
|
console.log(prefix + rel);
|
|
}
|
|
} catch (_e) { }
|
|
cb();
|
|
},
|
|
});
|
|
}
|
|
|
|
function getBundles(regexPattern) {
|
|
const bundleconfig = loadBundleConfig();
|
|
return bundleconfig.filter((bundle) => regexPattern.test(bundle.outputFileName));
|
|
}
|
|
|
|
function normalizeRel(p) {
|
|
return String(p || "").replace(/\//g, "|").replace(/\\/g, "|");
|
|
}
|
|
|
|
// Normalize slashes for Windows friendliness (outputFileName uses forward slashes)
|
|
function normalizePath(p) {
|
|
return String(p || "").replace(/\//g, path.sep);
|
|
}
|
|
|
|
// Make watch list more complete for SCSS: include likely include roots.
|
|
// (Safe: only affects watch mode, not build output.)
|
|
function inferScssWatchGlobs(bundle) {
|
|
const inputs = (bundle.inputFiles || []).slice();
|
|
const scss = inputs.filter((f) => /\.s[ac]ss$/i.test(f));
|
|
if (scss.length === 0) return [];
|
|
|
|
const dirSet = new Set();
|
|
for (const f of scss) {
|
|
const d = path.dirname(normalizePath(f));
|
|
if (d && d !== ".") dirSet.add(d);
|
|
}
|
|
|
|
// Watch all scss/sass partials and imports under those folders.
|
|
// This catches changes to partials that aren't explicitly listed in bdlconfig.json.
|
|
return Array.from(dirSet).map((d) => path.join(d, "**", "*.s[ac]ss"));
|
|
}
|
|
|
|
// --------------------------
|
|
// COPY (separate task)
|
|
// --------------------------
|
|
gulp.task("copy", async () => {
|
|
const jobs = [];
|
|
for (const cpy of copyconfig) {
|
|
jobs.push(
|
|
pipeline(gulp.src(cpy.src, { allowEmpty: true }), gulp.dest(cpy.dest), preservetimeSafe())
|
|
);
|
|
}
|
|
await Promise.all(jobs);
|
|
});
|
|
|
|
// --------------------------
|
|
// JS
|
|
// --------------------------
|
|
gulp.task("min:js", async () => {
|
|
for (const bundle of getBundles(regex.js)) {
|
|
const minify = typeof (bundle.minify || {}).enabled === "boolean" ? bundle.minify.enabled : true;
|
|
|
|
const inputs = (bundle.inputFiles || []).slice();
|
|
const toMinifyFiles = (bundle.inputFiles_tominify || []).slice();
|
|
|
|
const outNorm = normalizePath(bundle.outputFileName);
|
|
const outputDir = path.dirname(outNorm);
|
|
const outputFile = path.basename(outNorm);
|
|
|
|
console.log("Bundle:", bundle.outputFileName, "| Minify:", minify, "| Inputs:", inputs.length + toMinifyFiles.length);
|
|
|
|
if (minify === true) {
|
|
const allInputs = inputs.concat(toMinifyFiles);
|
|
const nonMinFile = outputFile.includes(".min.") ? outputFile.replace(".min.", ".") : outputFile;
|
|
const minFile = outputFile.includes(".min.") ? outputFile : outputFile.replace(/\.js$/i, ".min.js");
|
|
|
|
safeUnlink(path.join(outputDir, nonMinFile));
|
|
safeUnlink(path.join(outputDir, minFile));
|
|
|
|
await pipeline(
|
|
gulp.src(allInputs, { base: ".", allowEmpty: false }),
|
|
concat(nonMinFile),
|
|
gulp.dest(outputDir),
|
|
logSink("Non-Minified: ")
|
|
);
|
|
|
|
await pipeline(
|
|
gulp.src(allInputs, { base: ".", allowEmpty: false }),
|
|
concat(minFile),
|
|
terser(terserOptions).on("error", handleTerserError),
|
|
gulp.dest(outputDir),
|
|
logSink("Minified: ")
|
|
);
|
|
} else {
|
|
safeUnlink(path.join(outputDir, outputFile));
|
|
|
|
// Build source streams separately to avoid anymatch index confusion
|
|
// when negated globs and singular paths are mixed in one gulp.src call
|
|
const srcStreams = [];
|
|
if (inputs.length > 0) {
|
|
srcStreams.push(gulp.src(inputs, { base: ".", allowEmpty: false }));
|
|
}
|
|
if (toMinifyFiles.length > 0) {
|
|
srcStreams.push(
|
|
gulp.src(toMinifyFiles, { base: ".", allowEmpty: false })
|
|
.pipe(terser(terserOptions).on("error", handleTerserError))
|
|
);
|
|
}
|
|
|
|
const merged = srcStreams.length === 1 ? srcStreams[0] : merge2(srcStreams);
|
|
|
|
await pipeline(
|
|
merged,
|
|
concat(outputFile),
|
|
gulp.dest(outputDir),
|
|
logSink("Bundled: ")
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
// --------------------------
|
|
// CSS source builder
|
|
// IMPORTANT: the intermediate concat names MUST NOT start with "_" (gulp-sass ignores those)
|
|
// --------------------------
|
|
function getCssSourceAndSteps(bundle) {
|
|
const inputs = (bundle.inputFiles || []).slice();
|
|
|
|
const scssFiles = inputs.filter((f) => /\.s[ac]ss$/i.test(f));
|
|
const lessFiles = inputs.filter((f) => /\.less$/i.test(f));
|
|
const cssFiles = inputs.filter((f) => /\.css$/i.test(f) && !/\.s[ac]ss$/i.test(f));
|
|
|
|
const srcOpts = { base: ".", allowEmpty: false };
|
|
|
|
// Make Sass FAIL the build (no sass.logError swallowing)
|
|
if (scssFiles.length && !lessFiles.length && !cssFiles.length) {
|
|
return {
|
|
src: gulp.src(scssFiles, srcOpts),
|
|
steps: [concat("bundle.scss"), sass()],
|
|
};
|
|
}
|
|
|
|
if (lessFiles.length && !scssFiles.length && !cssFiles.length) {
|
|
return {
|
|
src: gulp.src(lessFiles, srcOpts),
|
|
steps: [concat("bundle.less"), less()],
|
|
};
|
|
}
|
|
|
|
if (cssFiles.length && !scssFiles.length && !lessFiles.length) {
|
|
return {
|
|
src: gulp.src(cssFiles, srcOpts),
|
|
steps: [],
|
|
};
|
|
}
|
|
|
|
throw new Error(`Mixed CSS types in bundle ${bundle.outputFileName}. Split into separate bundles.`);
|
|
}
|
|
|
|
// --------------------------
|
|
// CSS (min:css + min:scss alias)
|
|
// --------------------------
|
|
async function minCssImpl() {
|
|
for (const bundle of getBundles(regex.css)) {
|
|
const minify = typeof (bundle.minify || {}).enabled === "boolean" ? bundle.minify.enabled : true;
|
|
|
|
const outNorm = normalizePath(bundle.outputFileName);
|
|
const outputDir = path.dirname(outNorm);
|
|
const outputBase = path.basename(outNorm);
|
|
|
|
console.log(
|
|
"Bundle:",
|
|
bundle.outputFileName,
|
|
"| Minify:",
|
|
minify,
|
|
"| Inputs:",
|
|
(bundle.inputFiles || []).length
|
|
);
|
|
|
|
if (minify === true) {
|
|
const nonMinFile = outputBase.includes(".min.") ? outputBase.replace(".min.", ".") : outputBase;
|
|
const minFile = outputBase.includes(".min.")
|
|
? outputBase
|
|
: outputBase.replace(/\.css$/i, ".min.css");
|
|
|
|
safeUnlink(path.join(outputDir, nonMinFile));
|
|
safeUnlink(path.join(outputDir, minFile));
|
|
|
|
// Non-minified
|
|
{
|
|
const spec = getCssSourceAndSteps(bundle);
|
|
await pipeline(
|
|
spec.src,
|
|
...spec.steps,
|
|
concat(nonMinFile),
|
|
gulp.dest(outputDir),
|
|
logSink("Non-Minified: ")
|
|
);
|
|
}
|
|
|
|
// Minified (fresh stream)
|
|
{
|
|
const spec = getCssSourceAndSteps(bundle);
|
|
await pipeline(
|
|
spec.src,
|
|
...spec.steps,
|
|
concat(minFile),
|
|
// cleanCSS level 2 + single-line stats
|
|
cleanCSS({ level: 2, debug: true }, (details) => {
|
|
const name = details.name || path.basename(minFile);
|
|
console.log(`${name}: ${details.stats.originalSize} -> ${details.stats.minifiedSize}`);
|
|
}),
|
|
gulp.dest(outputDir),
|
|
logSink("Minified: ")
|
|
);
|
|
}
|
|
} else {
|
|
safeUnlink(path.join(outputDir, outputBase));
|
|
|
|
const spec = getCssSourceAndSteps(bundle);
|
|
await pipeline(
|
|
spec.src,
|
|
...spec.steps,
|
|
concat(outputBase),
|
|
gulp.dest(outputDir),
|
|
logSink("Bundled: ")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
gulp.task("min:css", minCssImpl);
|
|
gulp.task("min:scss", minCssImpl);
|
|
|
|
// --------------------------
|
|
// HTML
|
|
// --------------------------
|
|
gulp.task("min:html", async () => {
|
|
for (const bundle of getBundles(regex.html)) {
|
|
const minify = typeof (bundle.minify || {}).enabled === "boolean" ? bundle.minify.enabled : true;
|
|
|
|
safeUnlink(bundle.outputFileName);
|
|
|
|
await pipeline(
|
|
gulp.src(bundle.inputFiles || [], { base: ".", allowEmpty: false }),
|
|
concat(bundle.outputFileName),
|
|
htmlmin({ collapseWhitespace: true, minifyCSS: minify, minifyJS: minify }),
|
|
gulp.dest("."),
|
|
logSink("Bundled: ")
|
|
);
|
|
}
|
|
});
|
|
|
|
// --------------------------
|
|
// Aggregate / Watch
|
|
// --------------------------
|
|
gulp.task("min", gulp.parallel("min:js", "min:css", "min:html"));
|
|
gulp.task("all", gulp.series("copy", "min"));
|
|
|
|
gulp.task("watch", function () {
|
|
getBundles(regex.js).forEach((bundle) => {
|
|
const watchFiles = []
|
|
.concat(bundle.inputFiles || [])
|
|
.concat(bundle.inputFiles_tominify || []);
|
|
gulp.watch(watchFiles, gulp.series("min:js"));
|
|
});
|
|
|
|
getBundles(regex.css).forEach((bundle) => {
|
|
const watchFiles = [].concat(bundle.inputFiles || []);
|
|
const extraScssGlobs = inferScssWatchGlobs(bundle);
|
|
gulp.watch(watchFiles.concat(extraScssGlobs), gulp.series("min:css"));
|
|
});
|
|
|
|
getBundles(regex.html).forEach((bundle) => {
|
|
gulp.watch(bundle.inputFiles || [], gulp.series("min:html"));
|
|
});
|
|
});
|