Initial Commit after switching from SVN to git
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
"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"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user