From 80eb251bdf69cf9716a8baa721d223f9bcb498c0 Mon Sep 17 00:00:00 2001 From: Raniro Coelho Date: Tue, 17 Mar 2026 11:10:57 -0300 Subject: [PATCH] feat: add non-interactive setup flags for automation --- README.md | 7 + _gulp/combineTasks.ts | 110 ---------------- _gulp/gulpSetup.ts | 8 +- _gulp/modules/buildEnvironment.ts | 40 ------ _gulp/modules/fileSetup.ts | 28 ++-- _gulp/modules/gulpfileGenerator.ts | 113 +--------------- _gulp/modules/gulpfileSections.test.ts | 33 +++++ .../modules/gulpfileTemplateSnapshots.test.ts | 65 +++++++++ _gulp/modules/pathConfig.test.ts | 67 ++++++++++ _gulp/modules/pathConfig.ts | 43 ++++++ _gulp/modules/scaffoldIntegration.test.ts | 61 +++++++++ _gulp/modules/setupCliOptions.test.ts | 40 ++++++ _gulp/modules/setupCliOptions.ts | 77 +++++++++++ _gulp/modules/setupQuestions.test.ts | 31 +++++ _gulp/modules/setupQuestions.ts | 9 +- _gulp/modules/userChoicesValidation.test.ts | 31 +++++ _gulp/modules/userChoicesValidation.ts | 33 +++++ _gulp/tasks/cleanTask.ts | 7 - _gulp/tasks/imagesTask.ts | 10 -- _gulp/tasks/markupTask.ts | 13 -- _gulp/tasks/scriptTask.ts | 32 ----- _gulp/tasks/styleTask.ts | 22 ---- .../__snapshots__/js-sass-html.gulpfile.snap | 104 +++++++++++++++ .../__snapshots__/js-sass-pug.gulpfile.snap | 105 +++++++++++++++ .../__snapshots__/js-scss-html.gulpfile.snap | 104 +++++++++++++++ .../__snapshots__/js-scss-pug.gulpfile.snap | 105 +++++++++++++++ .../__snapshots__/ts-sass-html.gulpfile.snap | 104 +++++++++++++++ .../__snapshots__/ts-sass-pug.gulpfile.snap | 105 +++++++++++++++ .../__snapshots__/ts-scss-html.gulpfile.snap | 104 +++++++++++++++ .../__snapshots__/ts-scss-pug.gulpfile.snap | 105 +++++++++++++++ _gulp/templates/gulpfile/index.ts | 27 ++++ _gulp/templates/gulpfile/sections.ts | 124 ++++++++++++++++++ _gulp/templates/gulpfile/types.ts | 14 ++ 33 files changed, 1518 insertions(+), 363 deletions(-) delete mode 100644 _gulp/combineTasks.ts delete mode 100644 _gulp/modules/buildEnvironment.ts create mode 100644 _gulp/modules/gulpfileSections.test.ts create mode 100644 _gulp/modules/gulpfileTemplateSnapshots.test.ts create mode 100644 _gulp/modules/pathConfig.test.ts create mode 100644 _gulp/modules/pathConfig.ts create mode 100644 _gulp/modules/scaffoldIntegration.test.ts create mode 100644 _gulp/modules/setupCliOptions.test.ts create mode 100644 _gulp/modules/setupCliOptions.ts create mode 100644 _gulp/modules/setupQuestions.test.ts create mode 100644 _gulp/modules/userChoicesValidation.test.ts create mode 100644 _gulp/modules/userChoicesValidation.ts delete mode 100644 _gulp/tasks/cleanTask.ts delete mode 100644 _gulp/tasks/imagesTask.ts delete mode 100644 _gulp/tasks/markupTask.ts delete mode 100644 _gulp/tasks/scriptTask.ts delete mode 100644 _gulp/tasks/styleTask.ts create mode 100644 _gulp/templates/gulpfile/__snapshots__/js-sass-html.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/__snapshots__/js-sass-pug.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/__snapshots__/js-scss-html.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/__snapshots__/js-scss-pug.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/__snapshots__/ts-sass-html.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/__snapshots__/ts-sass-pug.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/__snapshots__/ts-scss-html.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/__snapshots__/ts-scss-pug.gulpfile.snap create mode 100644 _gulp/templates/gulpfile/index.ts create mode 100644 _gulp/templates/gulpfile/sections.ts create mode 100644 _gulp/templates/gulpfile/types.ts diff --git a/README.md b/README.md index 8f8f96b..015db4c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ Build for production: npm run build ``` +Non-interactive setup (for automation): + +```bash +npm run init -- --script ts --style scss --markup pug --normalize --reset +``` + Quality checks: ```bash @@ -81,6 +87,7 @@ This repository now includes: - Unit tests for filesystem utilities (`npm test`) - CI workflow for typecheck + tests (GitHub Actions) - Contributing and security documentation +- A single runtime architecture: setup always generates one `gulpfile.js` from user choices (no parallel dynamic task runtime) --- diff --git a/_gulp/combineTasks.ts b/_gulp/combineTasks.ts deleted file mode 100644 index a8fb77b..0000000 --- a/_gulp/combineTasks.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { readFile, writeFile } from 'fs/promises'; -import { parallel, series, TaskFunction } from 'gulp'; -import { browserSyncServe, createWatchTask } from './modules/buildEnvironment'; -import { copyVendorCSS, createProjectFiles, createProjectStructure } from './modules/fileSetup'; -import { confirmProjectDeletion, promptUser } from './modules/setupQuestions'; -import { UserChoices } from './types'; -import { deleteDirectory, fileExists } from './utils/fileSystem'; -import { logger } from './utils/logger'; - -async function loadUserChoices(): Promise { - try { - const data = await readFile('_gulp/user-choices.json', 'utf8'); - return JSON.parse(data); - } catch (error) { - logger.error('Failed to load user choices. Please run setup again.'); - process.exit(1); - } -} - -async function createGulpTasks() { - const choices = await loadUserChoices(); - - try { - const { styleTask } = await import('./tasks/styleTask'); - const { scriptTask } = await import('./tasks/scriptTask'); - const { markupTask } = await import('./tasks/markupTask'); - const { imagesTask } = await import('./tasks/imagesTask'); - const { cleanTask } = await import('./tasks/cleanTask'); - - const watchTask = createWatchTask(choices, { - styleTask: styleTask(choices) as TaskFunction, - scriptTask: scriptTask(choices) as TaskFunction, - markupTask: markupTask(choices) as TaskFunction, - imagesTask: imagesTask() as TaskFunction - }); - - const defaultTask: TaskFunction = (done) => { - return series( - cleanTask(), - parallel( - styleTask(choices), - scriptTask(choices), - markupTask(choices), - imagesTask() - ), - browserSyncServe, - watchTask - )(done); - }; - - const buildTask: TaskFunction = (done) => { - return series( - cleanTask(), - parallel( - styleTask(choices), - scriptTask(choices), - markupTask(choices), - imagesTask() - ) - )(done); - }; - - return { defaultTask, buildTask }; - } catch (error) { - logger.error(`Failed to create Gulp tasks: ${(error as Error).message}`); - process.exit(1); - } -} - -export async function setup(): Promise { - try { - const projectExists = fileExists('src') || fileExists('dist'); - if (projectExists) { - const shouldDelete = await confirmProjectDeletion(); - if (!shouldDelete) { - logger.info('Project setup canceled. Exiting...'); - return; - } - deleteDirectory('src'); - deleteDirectory('dist'); - } - - const choices: UserChoices = await promptUser(); - - await writeFile('_gulp/user-choices.json', JSON.stringify(choices, null, 2)); - - createProjectStructure(choices); - createProjectFiles(choices); - copyVendorCSS(choices); - - logger.success('Setup complete. Gulpfile has been generated.'); - logger.info('Starting development server...'); - - const { defaultTask } = await createGulpTasks(); - defaultTask((err?: Error | null) => { - if (err) { - logger.error(`Development server failed: ${err.message}`); - } else { - logger.success('Development server started'); - } - }); - } catch (error: unknown) { - logger.error(`An error occurred during setup: ${(error as Error).message}`); - } -} - -setup().catch(error => { - logger.error(`An error occurred: ${(error as Error).message}`); - process.exit(1); -}); \ No newline at end of file diff --git a/_gulp/gulpSetup.ts b/_gulp/gulpSetup.ts index 70b45a1..ff78f91 100644 --- a/_gulp/gulpSetup.ts +++ b/_gulp/gulpSetup.ts @@ -3,6 +3,8 @@ import { writeFile } from 'fs/promises'; import { copyVendorCSS, createProjectFiles, createProjectStructure } from './modules/fileSetup'; import { generateGulpfile } from './modules/gulpfileGenerator'; import { confirmProjectDeletion, promptUser } from './modules/setupQuestions'; +import { parseSetupOptions } from './modules/setupCliOptions'; +import { assertUserChoices } from './modules/userChoicesValidation'; import { UserChoices } from './types'; import { deleteDirectory, fileExists } from './utils/fileSystem'; import { logger } from './utils/logger'; @@ -20,7 +22,9 @@ async function setup(): Promise { deleteDirectory('dist'); } - const choices: UserChoices = await promptUser(); + const parsedOptions = parseSetupOptions(process.argv.slice(2)); + const rawChoices = parsedOptions.shouldPrompt ? await promptUser() : parsedOptions.choices; + const choices: UserChoices = assertUserChoices(rawChoices); await writeFile('_gulp/user-choices.json', JSON.stringify(choices, null, 2)); @@ -32,7 +36,7 @@ async function setup(): Promise { logger.success('Setup complete. Gulpfile has been generated.'); logger.info('Starting development server...'); - exec('yarn start', (error, stdout, stderr) => { + exec('npm start', (error, stdout, stderr) => { if (error) { logger.error(`Error: ${error.message}`); return; diff --git a/_gulp/modules/buildEnvironment.ts b/_gulp/modules/buildEnvironment.ts deleted file mode 100644 index bba6b05..0000000 --- a/_gulp/modules/buildEnvironment.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { create as createBrowserSync } from 'browser-sync'; -import { series, TaskFunction, watch } from 'gulp'; -import { UserChoices } from '../types'; - -const bs = createBrowserSync(); - -export function browserSyncServe(cb: () => void): void { - bs.init({ - server: { baseDir: 'dist/' }, - open: false, - notify: false - }); - cb(); -} - -export function browserSyncReload(cb: () => void): void { - bs.reload(); - cb(); -} - -interface Tasks { - styleTask: TaskFunction; - scriptTask: TaskFunction; - markupTask: TaskFunction; - imagesTask: TaskFunction; -} - -export function createWatchTask(choices: UserChoices, tasks: Tasks) { - return function watchTask(): void { - const stylePath = `src/${choices.style === 'Sass' ? 'sass' : 'scss'}/**/*.${choices.style === 'Sass' ? 'sass' : 'scss'}`; - const scriptPath = `src/${choices.script === 'TypeScript' ? 'ts' : 'js'}/**/*.${choices.script === 'TypeScript' ? 'ts' : 'js'}`; - const markupPath = `src/${choices.markup === 'Pug' ? 'pug' : 'html'}/**/*.${choices.markup === 'Pug' ? 'pug' : 'html'}`; - const imgPath = 'src/img/**/*'; - - watch(stylePath, series(tasks.styleTask, browserSyncReload)); - watch(scriptPath, series(tasks.scriptTask, browserSyncReload)); - watch(markupPath, series(tasks.markupTask, browserSyncReload)); - watch(imgPath, series(tasks.imagesTask, browserSyncReload)); - }; -} \ No newline at end of file diff --git a/_gulp/modules/fileSetup.ts b/_gulp/modules/fileSetup.ts index 18fbc10..2b6e2c2 100644 --- a/_gulp/modules/fileSetup.ts +++ b/_gulp/modules/fileSetup.ts @@ -1,12 +1,15 @@ +import { resolveProjectPaths } from './pathConfig'; import { UserChoices } from '../types'; import { copyFile, createDirectory, writeFile } from '../utils/fileSystem'; export function createProjectStructure(choices: UserChoices): void { + const paths = resolveProjectPaths(choices); + const dirs = [ 'src', - `src/${choices.script === 'JavaScript' ? 'js' : 'ts'}`, - `src/${choices.style === 'Sass' ? 'sass/base' : 'scss/base'}`, // Updated path for base - `src/${choices.markup === 'HTML' ? 'html' : 'pug'}`, + `src/${paths.scriptFolder}`, + `src/${paths.styleBaseFolder}`, + `src/${paths.markupFolder}`, 'src/img', 'dist/css' ]; @@ -15,35 +18,34 @@ export function createProjectStructure(choices: UserChoices): void { } export function createProjectFiles(choices: UserChoices): void { - const scriptExt = choices.script === 'JavaScript' ? 'js' : 'ts'; - const styleExt = choices.style === 'Sass' ? 'sass' : 'scss'; + const paths = resolveProjectPaths(choices); const scriptContent = choices.script === 'JavaScript' ? 'console.log("Hello, World!");' : 'console.log("Hello, TypeScript!");'; - writeFile(`src/${scriptExt}/main.${scriptExt}`, scriptContent); + writeFile(`src/${paths.scriptFolder}/main.${paths.scriptExtension}`, scriptContent); let styleContent = `// Main ${choices.style} file\n`; if (choices.addNormalize) styleContent += choices.style === 'Sass' ? "@import 'base/normalize'\n" : "@import 'base/normalize';\n"; if (choices.addReset) styleContent += choices.style === 'Sass' ? "@import 'base/reset'\n" : "@import 'base/reset';\n"; - writeFile(`src/${styleExt}/main.${styleExt}`, styleContent); + writeFile(`src/${paths.styleFolder}/main.${paths.styleExtension}`, styleContent); const markupContent = choices.markup === 'HTML' ? getHtmlTemplate() : getPugTemplate(); - const markupFilePath = choices.markup === 'HTML' ? 'src/html/index.html' : 'src/pug/index.pug'; - - writeFile(markupFilePath, markupContent); + writeFile(`src/${paths.markupFolder}/index.${paths.markupExtension}`, markupContent); writeFile('src/favicon.ico', ''); } export function copyVendorCSS(choices: UserChoices): void { + const paths = resolveProjectPaths(choices); + const vendorFiles = [ - { type: 'Normalize', src: `_gulp/vendors/normalize.${choices.style === 'Sass' ? 'sass' : 'scss'}` }, - { type: 'Reset', src: `_gulp/vendors/reset.${choices.style === 'Sass' ? 'sass' : 'scss'}` } + { type: 'Normalize', src: `_gulp/vendors/normalize.${paths.styleExtension}` }, + { type: 'Reset', src: `_gulp/vendors/reset.${paths.styleExtension}` } ]; vendorFiles.forEach(file => { if (choices[`add${file.type}` as keyof UserChoices]) { - const dest = `src/${choices.style === 'Sass' ? 'sass/base' : 'scss/base'}/_${file.type.toLowerCase()}.${choices.style === 'Sass' ? 'sass' : 'scss'}`; // Save in base folder + const dest = `src/${paths.styleBaseFolder}/_${file.type.toLowerCase()}.${paths.styleExtension}`; copyFile(file.src, dest); } }); diff --git a/_gulp/modules/gulpfileGenerator.ts b/_gulp/modules/gulpfileGenerator.ts index 5b8f8d2..00e869a 100644 --- a/_gulp/modules/gulpfileGenerator.ts +++ b/_gulp/modules/gulpfileGenerator.ts @@ -1,115 +1,8 @@ import { UserChoices } from '../types'; import { writeFile } from '../utils/fileSystem'; +import { buildGulpfileTemplate } from '../templates/gulpfile'; export function generateGulpfile(choices: UserChoices): void { - const gulpfileContent = ` -const { src, dest, watch, series, parallel } = require('gulp'); -const sass = require('gulp-sass')(require('sass')); -const autoprefixer = require('gulp-autoprefixer'); -const cleanCSS = require('gulp-clean-css'); -const browserify = require('browserify'); -const babelify = require('babelify'); -const source = require('vinyl-source-stream'); -const buffer = require('vinyl-buffer'); -const uglify = require('gulp-uglify'); -const rename = require('gulp-rename'); -const browserSync = require('browser-sync').create(); -const imagemin = require('gulp-imagemin'); -const del = require('del'); -const plumber = require('gulp-plumber'); -const sourcemaps = require('gulp-sourcemaps'); -const gulpif = require('gulp-if'); -const pug = ${choices.markup === 'Pug' ? "require('gulp-pug')" : 'null'}; -const typescript = ${choices.script === 'TypeScript' ? "require('gulp-typescript')" : 'null'}; -const tsify = ${choices.script === 'TypeScript' ? "require('tsify')" : 'null'}; - -const production = process.env.NODE_ENV === 'production'; - -async function clean() { - await del(['dist']); -} - -function styles() { - return src('src/${choices.style.toLowerCase()}/**/*.${choices.style.toLowerCase()}') - .pipe(plumber()) - .pipe(gulpif(!production, sourcemaps.init())) - .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) - .pipe(autoprefixer()) - .pipe(cleanCSS()) - .pipe(gulpif(!production, sourcemaps.write('.'))) - .pipe(dest('dist/css')) - .pipe(browserSync.stream()); + const gulpfileContent = buildGulpfileTemplate(choices); + writeFile('gulpfile.js', `${gulpfileContent}\n`); } - -function scripts() { - const b = browserify({ - entries: 'src/${choices.script === 'TypeScript' ? 'ts' : 'js'}/main.${choices.script === 'TypeScript' ? 'ts' : 'js'}', - debug: !production, - }) - .transform(babelify, { - presets: ['@babel/preset-env'], - extensions: ['.js', '.ts'] - }); - - ${choices.script === 'TypeScript' ? 'b.plugin(tsify);' : ''} - - return b.bundle() - .pipe(source('main.js')) - .pipe(buffer()) - .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) - .pipe(dest('dist/js')) - .pipe(uglify()) - .pipe(rename('main.min.js')) - .pipe(gulpif(!production, sourcemaps.write('.'))) - .pipe(dest('dist/js')); -} - -function markup() { - return src('src/${choices.markup === 'Pug' ? 'pug' : 'html'}/**/*.${choices.markup === 'Pug' ? 'pug' : 'html'}') - .pipe(plumber()) - ${choices.markup === 'Pug' ? '.pipe(pug())' : ''} - .pipe(dest('dist')); -} - -function images() { - return src('src/img/**/*') - .pipe(imagemin()) - .pipe(dest('dist/img')); -} - -function serve(cb) { - browserSync.init({ - server: { - baseDir: './dist' - }, - open: true - }); - cb(); -} - -function watchFiles(cb) { - watch('src/${choices.style.toLowerCase()}/**/*.${choices.style.toLowerCase()}', styles); - watch('src/${choices.script === 'TypeScript' ? 'ts' : 'js'}/**/*.${choices.script === 'TypeScript' ? 'ts' : 'js'}', series(scripts, reload)); - watch('src/${choices.markup === 'Pug' ? 'pug' : 'html'}/**/*.${choices.markup === 'Pug' ? 'pug' : 'html'}', series(markup, reload)); - watch('src/img/**/*', series(images, reload)); - cb(); -} - -function reload(cb) { - browserSync.reload(); - cb(); -} - -exports.clean = clean; -exports.styles = styles; -exports.scripts = scripts; -exports.markup = markup; -exports.images = images; -exports.watch = watchFiles; - -exports.build = series(clean, parallel(styles, scripts, markup, images)); -exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); -`; - - writeFile('gulpfile.js', gulpfileContent); -} \ No newline at end of file diff --git a/_gulp/modules/gulpfileSections.test.ts b/_gulp/modules/gulpfileSections.test.ts new file mode 100644 index 0000000..257374a --- /dev/null +++ b/_gulp/modules/gulpfileSections.test.ts @@ -0,0 +1,33 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { markupSection } from '../templates/gulpfile/sections'; +import { createGulpTemplateContext } from '../templates/gulpfile/types'; + +test('markupSection includes pug compilation when markup is Pug', () => { + const context = createGulpTemplateContext({ + script: 'JavaScript', + style: 'Sass', + markup: 'Pug', + addNormalize: false, + addReset: false, + }); + + const output = markupSection(context); + + assert.match(output, /\.pipe\(pug\(\)\)/); +}); + +test('markupSection does not inject pug compilation when markup is HTML', () => { + const context = createGulpTemplateContext({ + script: 'TypeScript', + style: 'SCSS', + markup: 'HTML', + addNormalize: true, + addReset: true, + }); + + const output = markupSection(context); + + assert.doesNotMatch(output, /\.pipe\(pug\(\)\)/); + assert.match(output, /\.pipe\(plumber\(\)\)\n\s*\.pipe\(dest\('dist'\)\)/); +}); diff --git a/_gulp/modules/gulpfileTemplateSnapshots.test.ts b/_gulp/modules/gulpfileTemplateSnapshots.test.ts new file mode 100644 index 0000000..f77c77f --- /dev/null +++ b/_gulp/modules/gulpfileTemplateSnapshots.test.ts @@ -0,0 +1,65 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { buildGulpfileTemplate } from '../templates/gulpfile'; +import { UserChoices } from '../types'; + +interface MatrixCase { + name: string; + choices: UserChoices; + snapshotFile: string; +} + +const cases: MatrixCase[] = [ + { + name: 'JavaScript + Sass + HTML', + choices: { script: 'JavaScript', style: 'Sass', markup: 'HTML', addNormalize: false, addReset: false }, + snapshotFile: 'js-sass-html.gulpfile.snap', + }, + { + name: 'JavaScript + Sass + Pug', + choices: { script: 'JavaScript', style: 'Sass', markup: 'Pug', addNormalize: false, addReset: false }, + snapshotFile: 'js-sass-pug.gulpfile.snap', + }, + { + name: 'JavaScript + SCSS + HTML', + choices: { script: 'JavaScript', style: 'SCSS', markup: 'HTML', addNormalize: false, addReset: false }, + snapshotFile: 'js-scss-html.gulpfile.snap', + }, + { + name: 'JavaScript + SCSS + Pug', + choices: { script: 'JavaScript', style: 'SCSS', markup: 'Pug', addNormalize: false, addReset: false }, + snapshotFile: 'js-scss-pug.gulpfile.snap', + }, + { + name: 'TypeScript + Sass + HTML', + choices: { script: 'TypeScript', style: 'Sass', markup: 'HTML', addNormalize: false, addReset: false }, + snapshotFile: 'ts-sass-html.gulpfile.snap', + }, + { + name: 'TypeScript + Sass + Pug', + choices: { script: 'TypeScript', style: 'Sass', markup: 'Pug', addNormalize: false, addReset: false }, + snapshotFile: 'ts-sass-pug.gulpfile.snap', + }, + { + name: 'TypeScript + SCSS + HTML', + choices: { script: 'TypeScript', style: 'SCSS', markup: 'HTML', addNormalize: false, addReset: false }, + snapshotFile: 'ts-scss-html.gulpfile.snap', + }, + { + name: 'TypeScript + SCSS + Pug', + choices: { script: 'TypeScript', style: 'SCSS', markup: 'Pug', addNormalize: false, addReset: false }, + snapshotFile: 'ts-scss-pug.gulpfile.snap', + }, +]; + +for (const scenario of cases) { + test(`buildGulpfileTemplate snapshot: ${scenario.name}`, () => { + const actual = `${buildGulpfileTemplate(scenario.choices)}\n`; + const snapshotPath = path.join('_gulp', 'templates', 'gulpfile', '__snapshots__', scenario.snapshotFile); + const expected = fs.readFileSync(snapshotPath, 'utf8'); + + assert.equal(actual, expected); + }); +} diff --git a/_gulp/modules/pathConfig.test.ts b/_gulp/modules/pathConfig.test.ts new file mode 100644 index 0000000..c220700 --- /dev/null +++ b/_gulp/modules/pathConfig.test.ts @@ -0,0 +1,67 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { resolveProjectPaths, resolveWatchGlobs } from './pathConfig'; + +test('resolveProjectPaths returns correct folders/extensions for Sass + JavaScript + HTML', () => { + const result = resolveProjectPaths({ + script: 'JavaScript', + style: 'Sass', + markup: 'HTML', + addNormalize: false, + addReset: false, + }); + + assert.equal(result.scriptFolder, 'js'); + assert.equal(result.scriptExtension, 'js'); + assert.equal(result.styleFolder, 'sass'); + assert.equal(result.styleBaseFolder, 'sass/base'); + assert.equal(result.markupFolder, 'html'); + assert.equal(result.markupExtension, 'html'); +}); + +test('resolveProjectPaths returns correct folders/extensions for SCSS + TypeScript + Pug', () => { + const result = resolveProjectPaths({ + script: 'TypeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: true, + }); + + assert.equal(result.scriptFolder, 'ts'); + assert.equal(result.scriptExtension, 'ts'); + assert.equal(result.styleFolder, 'scss'); + assert.equal(result.styleBaseFolder, 'scss/base'); + assert.equal(result.markupFolder, 'pug'); + assert.equal(result.markupExtension, 'pug'); +}); + +test('resolveWatchGlobs returns expected watch paths for JavaScript + Sass + HTML', () => { + const globs = resolveWatchGlobs({ + script: 'JavaScript', + style: 'Sass', + markup: 'HTML', + addNormalize: false, + addReset: false, + }); + + assert.equal(globs.styleGlob, 'src/sass/**/*.sass'); + assert.equal(globs.scriptGlob, 'src/js/**/*.js'); + assert.equal(globs.markupGlob, 'src/html/**/*.html'); + assert.equal(globs.imageGlob, 'src/img/**/*'); +}); + +test('resolveWatchGlobs returns expected watch paths for TypeScript + SCSS + Pug', () => { + const globs = resolveWatchGlobs({ + script: 'TypeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: true, + }); + + assert.equal(globs.styleGlob, 'src/scss/**/*.scss'); + assert.equal(globs.scriptGlob, 'src/ts/**/*.ts'); + assert.equal(globs.markupGlob, 'src/pug/**/*.pug'); + assert.equal(globs.imageGlob, 'src/img/**/*'); +}); diff --git a/_gulp/modules/pathConfig.ts b/_gulp/modules/pathConfig.ts new file mode 100644 index 0000000..5e2ef7e --- /dev/null +++ b/_gulp/modules/pathConfig.ts @@ -0,0 +1,43 @@ +import { UserChoices } from '../types'; + +export interface ResolvedProjectPaths { + scriptFolder: 'js' | 'ts'; + scriptExtension: 'js' | 'ts'; + styleFolder: 'sass' | 'scss'; + styleExtension: 'sass' | 'scss'; + styleBaseFolder: 'sass/base' | 'scss/base'; + markupFolder: 'html' | 'pug'; + markupExtension: 'html' | 'pug'; +} + +export interface ResolvedWatchGlobs { + styleGlob: string; + scriptGlob: string; + markupGlob: string; + imageGlob: 'src/img/**/*'; +} + +export function resolveProjectPaths(choices: UserChoices): ResolvedProjectPaths { + const styleFolder = choices.style === 'Sass' ? 'sass' : 'scss'; + + return { + scriptFolder: choices.script === 'TypeScript' ? 'ts' : 'js', + scriptExtension: choices.script === 'TypeScript' ? 'ts' : 'js', + styleFolder, + styleExtension: styleFolder, + styleBaseFolder: choices.style === 'Sass' ? 'sass/base' : 'scss/base', + markupFolder: choices.markup === 'Pug' ? 'pug' : 'html', + markupExtension: choices.markup === 'Pug' ? 'pug' : 'html', + }; +} + +export function resolveWatchGlobs(choices: UserChoices): ResolvedWatchGlobs { + const paths = resolveProjectPaths(choices); + + return { + styleGlob: `src/${paths.styleFolder}/**/*.${paths.styleExtension}`, + scriptGlob: `src/${paths.scriptFolder}/**/*.${paths.scriptExtension}`, + markupGlob: `src/${paths.markupFolder}/**/*.${paths.markupExtension}`, + imageGlob: 'src/img/**/*', + }; +} diff --git a/_gulp/modules/scaffoldIntegration.test.ts b/_gulp/modules/scaffoldIntegration.test.ts new file mode 100644 index 0000000..e2602a5 --- /dev/null +++ b/_gulp/modules/scaffoldIntegration.test.ts @@ -0,0 +1,61 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { copyVendorCSS, createProjectFiles, createProjectStructure } from './fileSetup'; +import { generateGulpfile } from './gulpfileGenerator'; +import { UserChoices } from '../types'; + +function copyVendorFixtures(targetRoot: string): void { + const sourceVendorDir = path.resolve(__dirname, '..', 'vendors'); + const targetVendorDir = path.join(targetRoot, '_gulp', 'vendors'); + + fs.mkdirSync(targetVendorDir, { recursive: true }); + + for (const fileName of ['normalize.sass', 'normalize.scss', 'reset.sass', 'reset.scss']) { + fs.copyFileSync(path.join(sourceVendorDir, fileName), path.join(targetVendorDir, fileName)); + } +} + +test('scaffold integration creates expected project tree and generated files', () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wlk-scaffold-')); + const previousCwd = process.cwd(); + + try { + copyVendorFixtures(tmpRoot); + process.chdir(tmpRoot); + + const choices: UserChoices = { + script: 'TypeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: true, + }; + + createProjectStructure(choices); + createProjectFiles(choices); + copyVendorCSS(choices); + generateGulpfile(choices); + + assert.equal(fs.existsSync(path.join(tmpRoot, 'src', 'ts', 'main.ts')), true); + assert.equal(fs.existsSync(path.join(tmpRoot, 'src', 'scss', 'main.scss')), true); + assert.equal(fs.existsSync(path.join(tmpRoot, 'src', 'scss', 'base', '_normalize.scss')), true); + assert.equal(fs.existsSync(path.join(tmpRoot, 'src', 'scss', 'base', '_reset.scss')), true); + assert.equal(fs.existsSync(path.join(tmpRoot, 'src', 'pug', 'index.pug')), true); + assert.equal(fs.existsSync(path.join(tmpRoot, 'gulpfile.js')), true); + + const generatedMainStyle = fs.readFileSync(path.join(tmpRoot, 'src', 'scss', 'main.scss'), 'utf8'); + assert.match(generatedMainStyle, /@import 'base\/normalize';/); + assert.match(generatedMainStyle, /@import 'base\/reset';/); + + const generatedGulpfile = fs.readFileSync(path.join(tmpRoot, 'gulpfile.js'), 'utf8'); + assert.match(generatedGulpfile, /src\/ts\/main\.ts/); + assert.match(generatedGulpfile, /src\/scss\/\*\*\/\*\.scss/); + assert.match(generatedGulpfile, /src\/pug\/\*\*\/\*\.pug/); + } finally { + process.chdir(previousCwd); + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } +}); diff --git a/_gulp/modules/setupCliOptions.test.ts b/_gulp/modules/setupCliOptions.test.ts new file mode 100644 index 0000000..e3cec57 --- /dev/null +++ b/_gulp/modules/setupCliOptions.test.ts @@ -0,0 +1,40 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { parseSetupOptions } from './setupCliOptions'; + +test('parseSetupOptions returns interactive mode when no flags are provided', () => { + const parsed = parseSetupOptions([]); + assert.equal(parsed.shouldPrompt, true); + assert.equal(parsed.choices, undefined); +}); + +test('parseSetupOptions parses non-interactive flags with aliases and booleans', () => { + const parsed = parseSetupOptions(['--script', 'ts', '--style=scss', '--markup', 'pug', '--normalize']); + + assert.equal(parsed.shouldPrompt, false); + assert.deepEqual(parsed.choices, { + script: 'TypeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: false, + }); +}); + +test('parseSetupOptions throws when non-interactive flags are partially provided', () => { + assert.throws( + () => parseSetupOptions(['--script', 'js', '--style', 'sass']), + /provide --script, --style, and --markup together/, + ); +}); + +test('parseSetupOptions throws when a flag has no value', () => { + assert.throws(() => parseSetupOptions(['--script']), /Missing value for --script/); +}); + +test('parseSetupOptions throws when a value is invalid', () => { + assert.throws( + () => parseSetupOptions(['--script', 'coffee', '--style', 'scss', '--markup', 'html']), + /Invalid user choices/, + ); +}); diff --git a/_gulp/modules/setupCliOptions.ts b/_gulp/modules/setupCliOptions.ts new file mode 100644 index 0000000..5c9210f --- /dev/null +++ b/_gulp/modules/setupCliOptions.ts @@ -0,0 +1,77 @@ +import { UserChoices } from '../types'; +import { assertUserChoices } from './userChoicesValidation'; + +const scriptAliases: Record = { + javascript: 'JavaScript', + js: 'JavaScript', + typescript: 'TypeScript', + ts: 'TypeScript', +}; + +const styleAliases: Record = { + sass: 'Sass', + scss: 'SCSS', +}; + +const markupAliases: Record = { + html: 'HTML', + pug: 'Pug', +}; + +export interface ParsedSetupOptions { + choices?: UserChoices; + shouldPrompt: boolean; +} + +function readFlagValue(argv: string[], flagName: string): string | undefined { + const flagPrefix = `${flagName}=`; + const inline = argv.find((arg) => arg.startsWith(flagPrefix)); + if (inline) { + return inline.slice(flagPrefix.length); + } + + const index = argv.indexOf(flagName); + if (index === -1) { + return undefined; + } + + const next = argv[index + 1]; + if (!next || next.startsWith('--')) { + throw new Error(`Missing value for ${flagName}.`); + } + + return next; +} + +function normalizeFlagValue(value: string | undefined): string | undefined { + return value?.trim().toLowerCase(); +} + +export function parseSetupOptions(argv: string[]): ParsedSetupOptions { + const rawScript = normalizeFlagValue(readFlagValue(argv, '--script')); + const rawStyle = normalizeFlagValue(readFlagValue(argv, '--style')); + const rawMarkup = normalizeFlagValue(readFlagValue(argv, '--markup')); + + const hasChoiceFlag = rawScript !== undefined || rawStyle !== undefined || rawMarkup !== undefined; + if (!hasChoiceFlag) { + return { shouldPrompt: true }; + } + + if (!rawScript || !rawStyle || !rawMarkup) { + throw new Error('When using non-interactive flags, provide --script, --style, and --markup together.'); + } + + const candidateChoices = { + script: scriptAliases[rawScript], + style: styleAliases[rawStyle], + markup: markupAliases[rawMarkup], + addNormalize: argv.includes('--normalize'), + addReset: argv.includes('--reset'), + }; + + const choices = assertUserChoices(candidateChoices); + return { + choices, + shouldPrompt: false, + }; +} diff --git a/_gulp/modules/setupQuestions.test.ts b/_gulp/modules/setupQuestions.test.ts new file mode 100644 index 0000000..e7747fe --- /dev/null +++ b/_gulp/modules/setupQuestions.test.ts @@ -0,0 +1,31 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { questions } from './setupQuestions'; +import { markupChoices, scriptChoices, styleChoices } from './userChoicesValidation'; + +function questionByName(name: string): Record { + const question = questions.find((item) => item.name === name); + assert.ok(question, `Expected question '${name}' to exist`); + return question as Record; +} + +test('setup questions expose strict script/style/markup choice lists', () => { + const scriptQuestion = questionByName('script'); + const styleQuestion = questionByName('style'); + const markupQuestion = questionByName('markup'); + + assert.deepEqual(scriptQuestion.choices, [...scriptChoices]); + assert.deepEqual(styleQuestion.choices, [...styleChoices]); + assert.deepEqual(markupQuestion.choices, [...markupChoices]); +}); + +test('setup questions include boolean toggles with safe defaults', () => { + const addNormalizeQuestion = questionByName('addNormalize'); + const addResetQuestion = questionByName('addReset'); + + assert.equal(addNormalizeQuestion.type, 'confirm'); + assert.equal(addNormalizeQuestion.default, false); + + assert.equal(addResetQuestion.type, 'confirm'); + assert.equal(addResetQuestion.default, false); +}); diff --git a/_gulp/modules/setupQuestions.ts b/_gulp/modules/setupQuestions.ts index a990c9b..f0f60c5 100644 --- a/_gulp/modules/setupQuestions.ts +++ b/_gulp/modules/setupQuestions.ts @@ -1,10 +1,11 @@ import inquirer from 'inquirer'; import { UserChoices } from '../types'; +import { markupChoices, scriptChoices, styleChoices } from './userChoicesValidation'; export const questions = [ - { type: 'list', name: 'script', message: 'Choose JavaScript or TypeScript:', choices: ['JavaScript', 'TypeScript'] }, - { type: 'list', name: 'style', message: 'Choose Sass or SCSS:', choices: ['Sass', 'SCSS'] }, - { type: 'list', name: 'markup', message: 'Choose HTML or Pug:', choices: ['HTML', 'Pug'] }, + { type: 'list', name: 'script', message: 'Choose JavaScript or TypeScript:', choices: [...scriptChoices] }, + { type: 'list', name: 'style', message: 'Choose Sass or SCSS:', choices: [...styleChoices] }, + { type: 'list', name: 'markup', message: 'Choose HTML or Pug:', choices: [...markupChoices] }, { type: 'confirm', name: 'addNormalize', message: 'Add normalize.css?', default: false }, { type: 'confirm', name: 'addReset', message: 'Add reset.css?', default: false } ]; @@ -18,4 +19,4 @@ export async function confirmProjectDeletion(): Promise { { type: 'confirm', name: 'deleteProject', message: 'DELETE existing project and start new?', default: false } ]); return deleteProject; -} \ No newline at end of file +} diff --git a/_gulp/modules/userChoicesValidation.test.ts b/_gulp/modules/userChoicesValidation.test.ts new file mode 100644 index 0000000..c7616d2 --- /dev/null +++ b/_gulp/modules/userChoicesValidation.test.ts @@ -0,0 +1,31 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { assertUserChoices, isUserChoices } from './userChoicesValidation'; + +test('isUserChoices returns true for a valid prompt payload', () => { + const valid = { + script: 'TypeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: false, + }; + + assert.equal(isUserChoices(valid), true); +}); + +test('isUserChoices returns false for invalid payload shape', () => { + const invalid = { + script: 'CoffeeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: false, + }; + + assert.equal(isUserChoices(invalid), false); +}); + +test('assertUserChoices throws on invalid choices', () => { + assert.throws(() => assertUserChoices({ script: 'JavaScript' }), /Invalid user choices/); +}); diff --git a/_gulp/modules/userChoicesValidation.ts b/_gulp/modules/userChoicesValidation.ts new file mode 100644 index 0000000..7adb223 --- /dev/null +++ b/_gulp/modules/userChoicesValidation.ts @@ -0,0 +1,33 @@ +import { UserChoices } from '../types'; + +export const scriptChoices = ['JavaScript', 'TypeScript'] as const; +export const styleChoices = ['Sass', 'SCSS'] as const; +export const markupChoices = ['HTML', 'Pug'] as const; + +const validScripts = new Set(scriptChoices); +const validStyles = new Set(styleChoices); +const validMarkups = new Set(markupChoices); + +function isObjectRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +export function isUserChoices(value: unknown): value is UserChoices { + if (!isObjectRecord(value)) { + return false; + } + + return validScripts.has(value.script as UserChoices['script']) + && validStyles.has(value.style as UserChoices['style']) + && validMarkups.has(value.markup as UserChoices['markup']) + && typeof value.addNormalize === 'boolean' + && typeof value.addReset === 'boolean'; +} + +export function assertUserChoices(value: unknown): UserChoices { + if (!isUserChoices(value)) { + throw new Error('Invalid user choices received from prompts. Please re-run setup.'); + } + + return value; +} diff --git a/_gulp/tasks/cleanTask.ts b/_gulp/tasks/cleanTask.ts deleted file mode 100644 index 3a43dfb..0000000 --- a/_gulp/tasks/cleanTask.ts +++ /dev/null @@ -1,7 +0,0 @@ -import del from 'del'; - -export function cleanTask() { - return function(cb: (error?: Error | null) => void) { - del(['dist']).then(() => cb()).catch(cb); - }; -} \ No newline at end of file diff --git a/_gulp/tasks/imagesTask.ts b/_gulp/tasks/imagesTask.ts deleted file mode 100644 index 31e27dc..0000000 --- a/_gulp/tasks/imagesTask.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { dest, src, TaskFunction } from 'gulp'; -import imagemin from 'gulp-imagemin'; - -export function imagesTask(): TaskFunction { - return function() { - return src('src/img/**/*') - .pipe(imagemin()) - .pipe(dest('dist/img')); - }; -} \ No newline at end of file diff --git a/_gulp/tasks/markupTask.ts b/_gulp/tasks/markupTask.ts deleted file mode 100644 index 625af01..0000000 --- a/_gulp/tasks/markupTask.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { dest, src } from 'gulp'; -import plumber from 'gulp-plumber'; -import pug from 'gulp-pug'; -import { UserChoices } from '../types'; - -export function markupTask(choices: UserChoices) { - return function() { - return src(`src/${choices.markup.toLowerCase()}/**/*.${choices.markup === 'Pug' ? 'pug' : 'html'}`) - .pipe(plumber()) - .pipe(choices.markup === 'Pug' ? pug() : plumber()) - .pipe(dest('dist')); - }; -} \ No newline at end of file diff --git a/_gulp/tasks/scriptTask.ts b/_gulp/tasks/scriptTask.ts deleted file mode 100644 index 117c21e..0000000 --- a/_gulp/tasks/scriptTask.ts +++ /dev/null @@ -1,32 +0,0 @@ -import browserify from 'browserify'; -import { dest, TaskFunction } from 'gulp'; -import rename from 'gulp-rename'; -import sourcemaps from 'gulp-sourcemaps'; -import uglify from 'gulp-uglify'; -import tsify from 'tsify'; -import buffer from 'vinyl-buffer'; -import source from 'vinyl-source-stream'; -import { UserChoices } from '../types'; - -export function scriptTask(choices: UserChoices): TaskFunction { - return function() { - const b = browserify({ - entries: `src/${choices.script === 'TypeScript' ? 'ts' : 'js'}/main.${choices.script === 'TypeScript' ? 'ts' : 'js'}`, - debug: true, - }); - - if (choices.script === 'TypeScript') { - b.plugin(tsify); - } - - return b.bundle() - .pipe(source('main.js')) - .pipe(buffer()) - .pipe(sourcemaps.init({ loadMaps: true })) - .pipe(dest('dist/js')) - .pipe(uglify()) - .pipe(rename('main.min.js')) - .pipe(sourcemaps.write('./')) - .pipe(dest('dist/js')); - }; -} \ No newline at end of file diff --git a/_gulp/tasks/styleTask.ts b/_gulp/tasks/styleTask.ts deleted file mode 100644 index 42b14b0..0000000 --- a/_gulp/tasks/styleTask.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { dest, src, TaskFunction } from 'gulp'; -import autoprefixer from 'gulp-autoprefixer'; -import cleanCSS from 'gulp-clean-css'; -import plumber from 'gulp-plumber'; -import gulpSass from 'gulp-sass'; -import sourcemaps from 'gulp-sourcemaps'; -import * as sass from 'sass'; -import { UserChoices } from '../types'; - -const sassCompiler = gulpSass(sass); - -export function styleTask(choices: UserChoices): TaskFunction { - return function() { - return src(`src/${choices.style.toLowerCase()}/**/*.${choices.style.toLowerCase()}`, { sourcemaps: true }) - .pipe(plumber()) - .pipe(sassCompiler({ indentedSyntax: choices.style === 'Sass' })) - .pipe(autoprefixer()) - .pipe(cleanCSS()) - .pipe(sourcemaps.write('.')) - .pipe(dest('dist/css')); - }; -} \ No newline at end of file diff --git a/_gulp/templates/gulpfile/__snapshots__/js-sass-html.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/js-sass-html.gulpfile.snap new file mode 100644 index 0000000..9e52313 --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/js-sass-html.gulpfile.snap @@ -0,0 +1,104 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = null; +const tsify = null; + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/sass/**/*.sass') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/js/main.js', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/html/**/*.html') + .pipe(plumber()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/sass/**/*.sass', styles); + watch('src/js/**/*.js', series(scripts, reload)); + watch('src/html/**/*.html', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/__snapshots__/js-sass-pug.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/js-sass-pug.gulpfile.snap new file mode 100644 index 0000000..7b4bd8c --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/js-sass-pug.gulpfile.snap @@ -0,0 +1,105 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = require('gulp-pug'); +const tsify = null; + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/sass/**/*.sass') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/js/main.js', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/pug/**/*.pug') + .pipe(plumber()) + .pipe(pug()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/sass/**/*.sass', styles); + watch('src/js/**/*.js', series(scripts, reload)); + watch('src/pug/**/*.pug', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/__snapshots__/js-scss-html.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/js-scss-html.gulpfile.snap new file mode 100644 index 0000000..4e27b4b --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/js-scss-html.gulpfile.snap @@ -0,0 +1,104 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = null; +const tsify = null; + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/scss/**/*.scss') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/js/main.js', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/html/**/*.html') + .pipe(plumber()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/scss/**/*.scss', styles); + watch('src/js/**/*.js', series(scripts, reload)); + watch('src/html/**/*.html', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/__snapshots__/js-scss-pug.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/js-scss-pug.gulpfile.snap new file mode 100644 index 0000000..76627a6 --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/js-scss-pug.gulpfile.snap @@ -0,0 +1,105 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = require('gulp-pug'); +const tsify = null; + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/scss/**/*.scss') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/js/main.js', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/pug/**/*.pug') + .pipe(plumber()) + .pipe(pug()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/scss/**/*.scss', styles); + watch('src/js/**/*.js', series(scripts, reload)); + watch('src/pug/**/*.pug', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/__snapshots__/ts-sass-html.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/ts-sass-html.gulpfile.snap new file mode 100644 index 0000000..523e0f8 --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/ts-sass-html.gulpfile.snap @@ -0,0 +1,104 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = null; +const tsify = require('tsify'); + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/sass/**/*.sass') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/ts/main.ts', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + b.plugin(tsify); + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/html/**/*.html') + .pipe(plumber()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/sass/**/*.sass', styles); + watch('src/ts/**/*.ts', series(scripts, reload)); + watch('src/html/**/*.html', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/__snapshots__/ts-sass-pug.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/ts-sass-pug.gulpfile.snap new file mode 100644 index 0000000..9a604bb --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/ts-sass-pug.gulpfile.snap @@ -0,0 +1,105 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = require('gulp-pug'); +const tsify = require('tsify'); + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/sass/**/*.sass') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/ts/main.ts', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + b.plugin(tsify); + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/pug/**/*.pug') + .pipe(plumber()) + .pipe(pug()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/sass/**/*.sass', styles); + watch('src/ts/**/*.ts', series(scripts, reload)); + watch('src/pug/**/*.pug', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/__snapshots__/ts-scss-html.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/ts-scss-html.gulpfile.snap new file mode 100644 index 0000000..3afd681 --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/ts-scss-html.gulpfile.snap @@ -0,0 +1,104 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = null; +const tsify = require('tsify'); + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/scss/**/*.scss') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/ts/main.ts', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + b.plugin(tsify); + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/html/**/*.html') + .pipe(plumber()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/scss/**/*.scss', styles); + watch('src/ts/**/*.ts', series(scripts, reload)); + watch('src/html/**/*.html', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/__snapshots__/ts-scss-pug.gulpfile.snap b/_gulp/templates/gulpfile/__snapshots__/ts-scss-pug.gulpfile.snap new file mode 100644 index 0000000..a746896 --- /dev/null +++ b/_gulp/templates/gulpfile/__snapshots__/ts-scss-pug.gulpfile.snap @@ -0,0 +1,105 @@ +const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = require('gulp-pug'); +const tsify = require('tsify'); + +const production = process.env.NODE_ENV === 'production'; + +async function clean() { + await del(['dist']); +} + +function styles() { + return src('src/scss/**/*.scss') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +} + +function scripts() { + const b = browserify({ + entries: 'src/ts/main.ts', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + b.plugin(tsify); + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +} + +function markup() { + return src('src/pug/**/*.pug') + .pipe(plumber()) + .pipe(pug()) + .pipe(dest('dist')); +} + +function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +} + +function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/scss/**/*.scss', styles); + watch('src/ts/**/*.ts', series(scripts, reload)); + watch('src/pug/**/*.pug', series(markup, reload)); + watch('src/img/**/*', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +} + +exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles); diff --git a/_gulp/templates/gulpfile/index.ts b/_gulp/templates/gulpfile/index.ts new file mode 100644 index 0000000..e43e24c --- /dev/null +++ b/_gulp/templates/gulpfile/index.ts @@ -0,0 +1,27 @@ +import { + cleanSection, + devServerSection, + exportsSection, + imagesSection, + importsSection, + markupSection, + scriptsSection, + stylesSection, +} from './sections'; +import { createGulpTemplateContext } from './types'; +import { UserChoices } from '../../types'; + +export function buildGulpfileTemplate(choices: UserChoices): string { + const context = createGulpTemplateContext(choices); + + return [ + importsSection(context), + cleanSection(), + stylesSection(context), + scriptsSection(context), + markupSection(context), + imagesSection(), + devServerSection(context), + exportsSection(), + ].join('\n\n'); +} diff --git a/_gulp/templates/gulpfile/sections.ts b/_gulp/templates/gulpfile/sections.ts new file mode 100644 index 0000000..daf9975 --- /dev/null +++ b/_gulp/templates/gulpfile/sections.ts @@ -0,0 +1,124 @@ +import { GulpTemplateContext } from './types'; + +export function importsSection({ choices }: GulpTemplateContext): string { + return `const { src, dest, watch, series, parallel } = require('gulp'); +const sass = require('gulp-sass')(require('sass')); +const autoprefixer = require('gulp-autoprefixer'); +const cleanCSS = require('gulp-clean-css'); +const browserify = require('browserify'); +const babelify = require('babelify'); +const source = require('vinyl-source-stream'); +const buffer = require('vinyl-buffer'); +const uglify = require('gulp-uglify'); +const rename = require('gulp-rename'); +const browserSync = require('browser-sync').create(); +const imagemin = require('gulp-imagemin'); +const del = require('del'); +const plumber = require('gulp-plumber'); +const sourcemaps = require('gulp-sourcemaps'); +const gulpif = require('gulp-if'); +const pug = ${choices.markup === 'Pug' ? "require('gulp-pug')" : 'null'}; +const tsify = ${choices.script === 'TypeScript' ? "require('tsify')" : 'null'}; + +const production = process.env.NODE_ENV === 'production';`; +} + +export function cleanSection(): string { + return `async function clean() { + await del(['dist']); +}`; +} + +export function stylesSection({ styleFolder, styleExtension }: GulpTemplateContext): string { + return `function styles() { + return src('src/${styleFolder}/**/*.${styleExtension}') + .pipe(plumber()) + .pipe(gulpif(!production, sourcemaps.init())) + .pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError)) + .pipe(autoprefixer()) + .pipe(cleanCSS()) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/css')) + .pipe(browserSync.stream()); +}`; +} + +export function scriptsSection({ choices, scriptFolder, scriptExtension }: GulpTemplateContext): string { + return `function scripts() { + const b = browserify({ + entries: 'src/${scriptFolder}/main.${scriptExtension}', + debug: !production, + }) + .transform(babelify, { + presets: ['@babel/preset-env'], + extensions: ['.js', '.ts'] + }); + + ${choices.script === 'TypeScript' ? 'b.plugin(tsify);' : ''} + + return b.bundle() + .pipe(source('main.js')) + .pipe(buffer()) + .pipe(gulpif(!production, sourcemaps.init({ loadMaps: true }))) + .pipe(dest('dist/js')) + .pipe(uglify()) + .pipe(rename('main.min.js')) + .pipe(gulpif(!production, sourcemaps.write('.'))) + .pipe(dest('dist/js')); +}`; +} + +export function markupSection({ choices, markupFolder, markupExtension }: GulpTemplateContext): string { + const compileMarkupPipe = choices.markup === 'Pug' ? '\n .pipe(pug())' : ''; + + return `function markup() { + return src('src/${markupFolder}/**/*.${markupExtension}') + .pipe(plumber())${compileMarkupPipe} + .pipe(dest('dist')); +}`; +} + +export function imagesSection(): string { + return `function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +}`; +} + +export function devServerSection({ styleGlob, scriptGlob, markupGlob, imageGlob }: GulpTemplateContext): string { + return `function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('${styleGlob}', styles); + watch('${scriptGlob}', series(scripts, reload)); + watch('${markupGlob}', series(markup, reload)); + watch('${imageGlob}', series(images, reload)); + cb(); +} + +function reload(cb) { + browserSync.reload(); + cb(); +}`; +} + +export function exportsSection(): string { + return `exports.clean = clean; +exports.styles = styles; +exports.scripts = scripts; +exports.markup = markup; +exports.images = images; +exports.watch = watchFiles; + +exports.build = series(clean, parallel(styles, scripts, markup, images)); +exports.default = series(clean, parallel(styles, scripts, markup, images), serve, watchFiles);`; +} diff --git a/_gulp/templates/gulpfile/types.ts b/_gulp/templates/gulpfile/types.ts new file mode 100644 index 0000000..4c2f7b0 --- /dev/null +++ b/_gulp/templates/gulpfile/types.ts @@ -0,0 +1,14 @@ +import { resolveProjectPaths, ResolvedProjectPaths, resolveWatchGlobs, ResolvedWatchGlobs } from '../../modules/pathConfig'; +import { UserChoices } from '../../types'; + +export interface GulpTemplateContext extends ResolvedProjectPaths, ResolvedWatchGlobs { + choices: UserChoices; +} + +export function createGulpTemplateContext(choices: UserChoices): GulpTemplateContext { + return { + choices, + ...resolveProjectPaths(choices), + ...resolveWatchGlobs(choices), + }; +}