import autoprefixer from 'autoprefixer' import concat from 'gulp-concat' import csso from 'gulp-csso' import { deleteAsync } from 'del' import gulp from 'gulp' import gulpWebp from 'gulp-webp' import imagemin, { mozjpeg, optipng, svgo } from 'gulp-imagemin' import imageminAvif from 'imagemin-avif' import order from 'gulp-order' import plumber from 'gulp-plumber' import postcss from 'gulp-postcss' import pug from 'gulp-pug' import rename from 'gulp-rename' import uglify from 'gulp-uglify-es' import * as dartSass from 'sass' import gulpSass from 'gulp-sass' import sourcemap from 'gulp-sourcemaps' import svgstore from 'gulp-svgstore' import sync from 'browser-sync' import { nanoid } from 'nanoid' import postcssUrl from 'postcss-url' import inject from 'gulp-inject' import newer from 'gulp-newer' // Added gulp-newer const baseUrl = '.' const pathCss = '..' const searchId = Date.now() const getImageSrcset = (path, format, params) => { return [1, 2] .map((dpr) => `${baseUrl}${path}@${dpr}x.${format}${params} ${dpr}x`) .join(', ') } const getImageMime = (format) => { switch (format) { case 'avif': return 'image/avif' case 'webp': return 'image/webp' case 'jpeg': case 'jpg': return 'image/jpeg' case 'png': return 'image/png' default: return null } } const sass = gulpSass(dartSass) const clean = async () => { return await deleteAsync(['dist']) } const copy = () => { return gulp .src( [ 'src/fonts/**/*.{woff,woff2}', 'src/img/**/*.{webm,webp,avif,jpg,jpeg,png,svg}', 'src/favicon/**/*', 'src/favicon.ico', 'src/settings.js', 'src/urls.json', 'src/robots.txt' ], { base: 'src', encoding: false } ) .pipe(gulp.dest('dist')) } const css = () => { return gulp .src('src/scss/index.scss') .pipe(plumber()) .pipe(sourcemap.init()) .pipe(sass.sync().on('error', sass.logError)) .pipe( postcss([ postcssUrl({ url: (asset) => { if (asset.url.startsWith('/')) { return `${pathCss}${asset.url}?v=${searchId}` } return asset.url } }), autoprefixer({ remove: false }) ]) ) .pipe(rename('style.css')) .pipe(sourcemap.write('.')) .pipe(gulp.dest('dist/css')) .pipe(sync.stream()) .pipe(csso()) .pipe(rename('style.min.css')) .pipe(gulp.dest('dist/css')) } const images = () => { return gulp .src('src/img/**/*.{png,jpg,jpeg,svg}', { base: 'src', encoding: false }) // Process all images in subfolders, keep base for dest path .pipe(newer('dist')) // Process only newer files, compare with base 'dist' (e.g. dist/img/...) .pipe( imagemin( [ optipng({ optimizationLevel: 3 }), mozjpeg({ quality: 80, progressive: true }), // Quality for JPEG svgo({ plugins: [{ name: 'removeUnknownsAndDefaults', active: false }] }) ], { silent: true } ) ) .pipe(gulp.dest('dist')) // Output to dist, maintaining folder structure from base } const webp = () => { return gulp .src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false }) // Process all images in subfolders .pipe(newer({ dest: 'dist', ext: '.webp' })) // Process only newer, check against .webp extension in dist .pipe(gulpWebp({ quality: 80 })) // Quality for WebP .pipe(gulp.dest('dist')) // Output to dist, maintaining folder structure } const avif = () => { return gulp .src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false }) // Process all images in subfolders .pipe(newer({ dest: 'dist', ext: '.avif' })) // Process only newer, check against .avif extension in dist .pipe(imagemin([imageminAvif({ quality: 50 })], { silent: true })) // Quality for AVIF .pipe(rename((path) => (path.extname = '.avif'))) .pipe(gulp.dest('dist')) // Output to dist, maintaining folder structure } const processImages = gulp.parallel(images, webp, avif) const sprite = () => { return gulp .src('src/icons/**/*.svg') .pipe(svgstore({ inlineSvg: true })) .pipe(rename(`sprite.svg`)) .pipe(gulp.dest('dist/img')) } const injectSprite = () => { const spritePath = 'dist/img/sprite.svg' return gulp .src('dist/*.html') .pipe( inject(gulp.src(spritePath), { transform: (_, file) => { return file.contents.toString() } }) ) .pipe(gulp.dest('dist')) } const jsCommon = () => { return gulp .src('src/js/common/*.js') .pipe( plumber({ errorHandler(err) { console.error(err.toString()) this.emit('end') } }) ) .pipe(order(['_utils.js', '*.js'])) .pipe(concat(`script.js`)) .pipe(gulp.dest('dist/js')) .pipe(uglify.default()) .pipe(rename(`script.min.js`)) .pipe(gulp.dest('dist/js')) } const jsVendor = () => { return gulp .src('src/js/vendor/*.js') .pipe( plumber({ errorHandler(err) { console.error(err.toString()) this.emit('end') } }) ) .pipe(concat(`vendor.js`)) .pipe(gulp.dest('dist/js')) .pipe(uglify.default()) .pipe(rename(`script.min.js`)) .pipe(gulp.dest('dist/js')) } const html = () => { return gulp .src('src/pug/pages/**/*.pug') .pipe(plumber()) .pipe( pug({ pretty: true, basedir: 'src/pug', locals: { baseUrl, searchId, getSrcset: getImageSrcset, getMime: getImageMime } }) ) .pipe(gulp.dest('dist')) } const refresh = (done) => { sync.reload() done() } const server = () => { sync.init({ server: 'dist/', notify: false, open: false, cors: true, ui: false }) gulp.watch('src/pug/**/*.{pug,js}', gulp.series(html, injectSprite, refresh)) gulp.watch( 'src/icons/**/*.svg', gulp.series(sprite, html, injectSprite, refresh) ) gulp.watch('src/scss/**/*.scss', gulp.series(css)) gulp.watch('src/js/**/*.js', gulp.series(jsVendor, jsCommon, refresh)) // Watch for source image changes and run all image processing tasks. // gulp-newer within each task will ensure only necessary files are processed. gulp.watch('src/img/**/*.{png,jpg,jpeg,svg}', gulp.series(processImages, refresh)) } export const build = gulp.series( clean, copy, css, jsVendor, jsCommon, processImages, sprite, html, injectSprite ) export const dev = gulp.series(build, server)