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' import twig from 'gulp-twig' import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { execSync } from 'child_process' import dotenv from 'dotenv' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) // Загружаем переменные окружения из .env.local const envPath = path.join(__dirname, '.env.local') if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }) console.log('📝 Загружены настройки из .env.local') } else { console.log('⚠️ Файл .env.local не найден. Запустите: gulp configInit') } const baseUrl = '.' const pathCss = '..' const searchId = Date.now() // Конфигурация для разных шаблонизаторов const settings = { templateEngine: process.env.TEMPLATE_ENGINE || 'pug', // 'pug' или 'twig' dataPath: 'src/data', // путь к JSON файлам с данными https: { enabled: process.env.HTTPS === 'true', keyPath: 'ssl/server.key', certPath: 'ssl/server.crt' }, server: { port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, host: process.env.HOST || 'localhost', open: process.env.OPEN_BROWSER === 'true', notify: process.env.BROWSER_SYNC_NOTIFY === 'true' } } // Выводим текущие настройки при запуске console.log('⚙️ Текущие настройки:') console.log(` Шаблонизатор: ${settings.templateEngine}`) console.log(` HTTPS: ${settings.https.enabled ? 'включен' : 'выключен'}`) console.log(` Порт: ${settings.server.port}`) console.log(` Хост: ${settings.server.host}`) console.log(` Автооткрытие браузера: ${settings.server.open ? 'да' : 'нет'}`) console.log(` Уведомления: ${settings.server.notify ? 'да' : 'нет'}`) // Функция для загрузки данных из JSON файлов const loadData = () => { const dataPath = path.join(__dirname, settings.dataPath) let data = {} if (fs.existsSync(dataPath)) { const files = fs.readdirSync(dataPath).filter(file => file.endsWith('.json')) files.forEach(file => { const fileName = path.basename(file, '.json') const filePath = path.join(dataPath, file) try { const fileContent = fs.readFileSync(filePath, 'utf8') data[fileName] = JSON.parse(fileContent) } catch (error) { console.error(`Ошибка загрузки данных из ${file}:`, error.message) } }) } else { console.log(`📁 Папка с данными не найдена: ${dataPath}`) console.log('💡 Запустите: gulp data для создания примера данных') } return data } 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 }) .pipe(newer('dist')) .pipe( imagemin( [ optipng({ optimizationLevel: 5 }), mozjpeg({ quality: 85, progressive: true }), svgo({ plugins: [ { name: 'removeViewBox', active: false }, { name: 'removeXMLNS', active: false }, { name: 'removeUnknownsAndDefaults', active: false } ] }) ], { silent: true } ) ) .pipe(gulp.dest('dist')) } const webp = () => { return gulp .src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false }) .pipe(newer({ dest: 'dist', ext: '.webp' })) .pipe(gulpWebp({ quality: 85 })) .pipe(gulp.dest('dist')) } const avif = () => { return gulp .src('src/img/**/*.{png,jpg,jpeg}', { base: 'src', encoding: false }) .pipe(newer({ dest: 'dist', ext: '.avif' })) .pipe(imagemin([imageminAvif({ quality: 60 })], { silent: true })) .pipe(rename((path) => (path.extname = '.avif'))) .pipe(gulp.dest('dist')) } 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' // Проверяем, существует ли файл спрайта if (!fs.existsSync(spritePath)) { console.log('⚠️ Спрайт не найден, пропускаем инъекцию') return Promise.resolve() } return gulp .src('dist/*.html') .pipe( inject(gulp.src(spritePath), { transform: (_, file) => { return file.contents.toString() } }) ) .pipe(gulp.dest('dist')) } // Инъекция спрайта только в конкретный файл const injectSpriteToFile = (htmlFile) => { const spritePath = 'dist/img/sprite.svg' if (!fs.existsSync(spritePath) || !fs.existsSync(htmlFile)) { return Promise.resolve() } return gulp .src(htmlFile) .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('JS Common Error:', err.toString()) this.emit('end') } }) ) .pipe(order(['_utils.js', '*.js'])) .pipe(sourcemap.init()) .pipe(concat('script.js')) .pipe(sourcemap.write('.')) .pipe(gulp.dest('dist/js')) } const jsCommonMin = () => { return gulp .src('src/js/common/*.js') .pipe( plumber({ errorHandler(err) { console.error('JS Common Min Error:', err.toString()) this.emit('end') } }) ) .pipe(order(['_utils.js', '*.js'])) .pipe(concat('script.min.js')) .pipe(uglify.default()) .pipe(gulp.dest('dist/js')) } const jsVendor = () => { return gulp .src('src/js/vendor/*.js') .pipe( plumber({ errorHandler(err) { console.error('JS Vendor Error:', err.toString()) this.emit('end') } }) ) .pipe(concat('vendor.js')) .pipe(gulp.dest('dist/js')) } const jsVendorMin = () => { return gulp .src('src/js/vendor/*.js') .pipe( plumber({ errorHandler(err) { console.error('JS Vendor Min Error:', err.toString()) this.emit('end') } }) ) .pipe(concat('vendor.min.js')) .pipe(uglify.default()) .pipe(gulp.dest('dist/js')) } const js = gulp.parallel(jsCommon, jsCommonMin, jsVendor, jsVendorMin) // Простая обработка Pug шаблонов const htmlPug = () => { const data = loadData() return gulp .src('src/pug/pages/**/*.pug') .pipe(plumber({ errorHandler(err) { console.error('Pug Error:', err.toString()) this.emit('end') } })) .pipe( pug({ pretty: true, basedir: 'src/pug', locals: { baseUrl, searchId, getSrcset: getImageSrcset, getMime: getImageMime, ...data } }) ) .pipe(gulp.dest('dist')) } // Простая обработка Twig шаблонов const htmlTwig = () => { const data = loadData() return gulp .src('src/twig/pages/**/*.twig') .pipe(plumber({ errorHandler(err) { console.error('Twig Error:', err.toString()) this.emit('end') } })) .pipe( twig({ base: 'src/twig', data: { baseUrl, searchId, getSrcset: getImageSrcset, getMime: getImageMime, ...data }, functions: [ { name: 'getSrcset', func: getImageSrcset }, { name: 'getMime', func: getImageMime } ] }) ) .pipe(gulp.dest('dist')) } // Pug с инкрементальной сборкой const htmlPugIncremental = () => { const data = loadData() return gulp .src('src/pug/pages/**/*.pug') .pipe(plumber({ errorHandler(err) { console.error('Pug Error:', err.toString()) this.emit('end') } })) .pipe(newer({ dest: 'dist', ext: '.html' })) .pipe( pug({ pretty: true, basedir: 'src/pug', locals: { baseUrl, searchId, getSrcset: getImageSrcset, getMime: getImageMime, ...data } }) ) .pipe(gulp.dest('dist')) .on('end', () => { console.log('📄 Обработаны только измененные Pug файлы') }) } // Twig с инкрементальной сборкой const htmlTwigIncremental = () => { const data = loadData() return gulp .src('src/twig/pages/**/*.twig') .pipe(plumber({ errorHandler(err) { console.error('Twig Error:', err.toString()) this.emit('end') } })) .pipe(newer({ dest: 'dist', ext: '.html' })) .pipe( twig({ base: 'src/twig', data: { baseUrl, searchId, getSrcset: getImageSrcset, getMime: getImageMime, ...data }, functions: [ { name: 'getSrcset', func: getImageSrcset }, { name: 'getMime', func: getImageMime } ] }) ) .pipe(gulp.dest('dist')) .on('end', () => { console.log('📄 Обработаны только измененные Twig файлы') }) } // Универсальная функция для обработки HTML const html = (incremental = false) => { if (settings.templateEngine === 'twig') { return incremental ? htmlTwigIncremental() : htmlTwig() } else { return incremental ? htmlPugIncremental() : htmlPug() } } // Создание SSL сертификатов для разработки const createSSLCerts = (done) => { const sslDir = path.join(__dirname, 'ssl') const keyPath = path.join(sslDir, 'server.key') const certPath = path.join(sslDir, 'server.crt') if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { console.log('✅ SSL сертификаты уже существуют') done() return } if (!fs.existsSync(sslDir)) { fs.mkdirSync(sslDir, { recursive: true }) } try { console.log('🔐 Создание SSL сертификатов для разработки...') execSync(`openssl genrsa -out ${keyPath} 2048`, { stdio: 'pipe' }) const certCommand = `openssl req -new -x509 -key ${keyPath} -out ${certPath} -days 365 -subj "/C=RU/ST=Local/L=Local/O=Development/OU=Dev/CN=localhost"` execSync(certCommand, { stdio: 'pipe' }) console.log('✅ SSL сертификаты успешно созданы') console.log('⚠️ Это сертификаты только для разработки!') } catch (error) { console.error('❌ Ошибка создания SSL сертификатов:') console.error('Убедитесь что OpenSSL установлен в системе') } done() } // Создание конфигурационного файла const createHTTPSConfig = (done) => { const configPath = path.join(__dirname, '.env.local') if (!fs.existsSync(configPath)) { const envContent = `# Конфигурация для разработки # Этот файл автоматически загружается при запуске gulp # ===== ОСНОВНЫЕ НАСТРОЙКИ ===== # Включить HTTPS (true/false) # После изменения нужно перезапустить gulp HTTPS=false # Шаблонизатор (pug/twig) # После изменения нужно перезапустить gulp TEMPLATE_ENGINE=pug # ===== НАСТРОЙКИ СЕРВЕРА ===== # Порт для разработки PORT=3000 # Хост для разработки (localhost, 127.0.0.1, 0.0.0.0 для доступа извне) HOST=localhost # Автоматически открывать браузер при запуске (true/false) OPEN_BROWSER=false # Показывать уведомления BrowserSync (true/false) BROWSER_SYNC_NOTIFY=false ` fs.writeFileSync(configPath, envContent) console.log('✅ Создан файл конфигурации: .env.local') console.log('📝 Теперь вы можете настроить параметры в .env.local') } else { console.log('ℹ️ Файл .env.local уже существует') } done() } // Функция для перезагрузки конфигурации const reloadConfig = (done) => { const envPath = path.join(__dirname, '.env.local') if (fs.existsSync(envPath)) { // Очищаем старые значения delete process.env.TEMPLATE_ENGINE delete process.env.HTTPS delete process.env.PORT delete process.env.HOST delete process.env.OPEN_BROWSER delete process.env.BROWSER_SYNC_NOTIFY // Загружаем новые dotenv.config({ path: envPath, override: true }) // Обновляем настройки settings.templateEngine = process.env.TEMPLATE_ENGINE || 'pug' settings.https.enabled = process.env.HTTPS === 'true' settings.server.port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000 settings.server.host = process.env.HOST || 'localhost' settings.server.open = process.env.OPEN_BROWSER === 'true' settings.server.notify = process.env.BROWSER_SYNC_NOTIFY === 'true' console.log('🔄 Конфигурация перезагружена!') console.log('⚙️ Новые настройки:') console.log(` Шаблонизатор: ${settings.templateEngine}`) console.log(` HTTPS: ${settings.https.enabled ? 'включен' : 'выключен'}`) console.log(` Порт: ${settings.server.port}`) console.log(` Хост: ${settings.server.host}`) console.log('⚠️ Для применения HTTPS и смены шаблонизатора нужен перезапуск!') } else { console.log('❌ Файл .env.local не найден') } done() } // Задача для создания примера структуры данных const createDataExample = (done) => { const dataDir = path.join(__dirname, 'src/data') if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }) } const exampleData = { site: { title: 'Мой сайт', description: 'Описание сайта', author: 'Автор' }, navigation: [ { title: 'Главная', url: '/' }, { title: 'О нас', url: '/about' }, { title: 'Контакты', url: '/contacts' } ] } fs.writeFileSync( path.join(dataDir, 'global.json'), JSON.stringify(exampleData, null, 2) ) console.log('✅ Создан пример файла данных: src/data/global.json') done() } const refresh = (done) => { sync.reload() done() } // Обработка конкретного Pug файла const processSinglePugFile = (filePath) => { const data = loadData() const relativePath = path.relative('src/pug/pages', filePath) const outputPath = path.join('dist', relativePath.replace('.pug', '.html')) console.log(`📄 Обработка файла: ${relativePath}`) return gulp .src(filePath) .pipe(plumber({ errorHandler(err) { console.error('Pug Error:', err.toString()) this.emit('end') } })) .pipe( pug({ pretty: true, basedir: 'src/pug', locals: { baseUrl, searchId, getSrcset: getImageSrcset, getMime: getImageMime, ...data } }) ) .pipe(rename(path.basename(outputPath))) .pipe(gulp.dest(path.dirname(outputPath))) } // Обработка конкретного Twig файла const processSingleTwigFile = (filePath) => { const data = loadData() const relativePath = path.relative('src/twig/pages', filePath) const outputPath = path.join('dist', relativePath.replace('.twig', '.html')) console.log(`📄 Обработка файла: ${relativePath}`) return gulp .src(filePath) .pipe(plumber({ errorHandler(err) { console.error('Twig Error:', err.toString()) this.emit('end') } })) .pipe( twig({ base: 'src/twig', data: { baseUrl, searchId, getSrcset: getImageSrcset, getMime: getImageMime, ...data }, functions: [ { name: 'getSrcset', func: getImageSrcset }, { name: 'getMime', func: getImageMime } ] }) ) .pipe(rename(path.basename(outputPath))) .pipe(gulp.dest(path.dirname(outputPath))) } // Сервер с поддержкой HTTPS const server = () => { let serverConfig = { server: 'dist/', notify: settings.server.notify, open: settings.server.open, cors: true, ui: false, port: settings.server.port, host: settings.server.host } if (settings.https.enabled) { const keyPath = path.join(__dirname, settings.https.keyPath) const certPath = path.join(__dirname, settings.https.certPath) if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { serverConfig.https = { key: keyPath, cert: certPath } console.log('🔒 Запуск сервера с HTTPS') console.log(`📍 https://${serverConfig.host}:${serverConfig.port}`) } else { console.log('⚠️ HTTPS включен, но сертификаты не найдены') console.log('Запустите: gulp ssl для создания сертификатов') } } else { console.log('🌐 Запуск HTTP сервера') console.log(`📍 http://${serverConfig.host}:${serverConfig.port}`) } sync.init(serverConfig) // Умное наблюдение за страницами if (settings.templateEngine === 'twig') { // Отслеживание изменений в страницах Twig const twigWatcher = gulp.watch('src/twig/pages/**/*.twig') twigWatcher.on('change', (filePath) => { console.log(`🔄 Изменен файл: ${path.relative(__dirname, filePath)}`) return gulp.series( () => processSingleTwigFile(filePath), refresh )() }) // Наблюдение за общими файлами - полная пересборка gulp.watch(['src/twig/**/*.twig', '!src/twig/pages/**/*.twig'], gulp.series(() => html(false), injectSprite, refresh)) } else { // Отслеживание изменений в страницах Pug const pugWatcher = gulp.watch('src/pug/pages/**/*.pug') pugWatcher.on('change', (filePath) => { console.log(`🔄 Изменен файл: ${path.relative(__dirname, filePath)}`) return gulp.series( () => processSinglePugFile(filePath), refresh )() }) // Наблюдение за общими файлами - полная пересборка gulp.watch(['src/pug/**/*.pug', '!src/pug/pages/**/*.pug'], gulp.series(() => html(false), injectSprite, refresh)) } // Наблюдение за данными - полная пересборка gulp.watch('src/data/**/*.json', gulp.series(() => html(false), injectSprite, refresh)) // Наблюдение за спрайтами - полная пересборка с инъекцией gulp.watch('src/icons/**/*.svg', gulp.series(sprite, () => html(false), injectSprite, refresh)) // Остальные файлы gulp.watch('src/scss/**/*.scss', gulp.series(css)) gulp.watch('src/js/**/*.js', gulp.series(js, refresh)) gulp.watch('src/img/**/*.{png,jpg,jpeg,svg}', gulp.series(processImages, refresh)) } // Экспорт задач export const build = gulp.series( clean, copy, css, js, processImages, sprite, () => html(false), injectSprite ) export const dev = gulp.series(build, server) export const data = createDataExample export const ssl = createSSLCerts export const configInit = createHTTPSConfig export const configReload = reloadConfig // HTTPS команды export const devSSL = gulp.series( (done) => { settings.https.enabled = true done() }, ssl, build, server ) // Команды с выбором шаблонизатора export const buildPug = gulp.series( (done) => { settings.templateEngine = 'pug'; done() }, build ) export const buildTwig = gulp.series( (done) => { settings.templateEngine = 'twig'; done() }, build ) export const devPug = gulp.series( (done) => { settings.templateEngine = 'pug'; done() }, build, server ) export const devTwig = gulp.series( (done) => { settings.templateEngine = 'twig'; done() }, build, server ) export const devPugSSL = gulp.series( (done) => { settings.templateEngine = 'pug' settings.https.enabled = true done() }, ssl, build, server ) export const devTwigSSL = gulp.series( (done) => { settings.templateEngine = 'twig' settings.https.enabled = true done() }, ssl, build, server )