diff --git a/README.md b/README.md index 8f8f96b..d12f9d0 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,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/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/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/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/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..7120936 --- /dev/null +++ b/_gulp/templates/gulpfile/sections.ts @@ -0,0 +1,123 @@ +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 { + return `function markup() { + return src('src/${markupFolder}/**/*.${markupExtension}') + .pipe(plumber()) + ${choices.markup === 'Pug' ? '.pipe(pug())' : ''} + .pipe(dest('dist')); +}`; +} + +export function imagesSection(): string { + return `function images() { + return src('src/img/**/*') + .pipe(imagemin()) + .pipe(dest('dist/img')); +}`; +} + +export function devServerSection({ styleFolder, styleExtension, scriptFolder, scriptExtension, markupFolder, markupExtension }: GulpTemplateContext): string { + return `function serve(cb) { + browserSync.init({ + server: { + baseDir: './dist' + }, + open: true + }); + cb(); +} + +function watchFiles(cb) { + watch('src/${styleFolder}/**/*.${styleExtension}', styles); + watch('src/${scriptFolder}/**/*.${scriptExtension}', series(scripts, reload)); + watch('src/${markupFolder}/**/*.${markupExtension}', series(markup, reload)); + watch('src/img/**/*', 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..490225f --- /dev/null +++ b/_gulp/templates/gulpfile/types.ts @@ -0,0 +1,23 @@ +import { UserChoices } from '../../types'; + +export interface GulpTemplateContext { + choices: UserChoices; + styleFolder: 'sass' | 'scss'; + styleExtension: 'sass' | 'scss'; + scriptFolder: 'js' | 'ts'; + scriptExtension: 'js' | 'ts'; + markupFolder: 'html' | 'pug'; + markupExtension: 'html' | 'pug'; +} + +export function createGulpTemplateContext(choices: UserChoices): GulpTemplateContext { + return { + choices, + styleFolder: choices.style === 'Sass' ? 'sass' : 'scss', + styleExtension: choices.style === 'Sass' ? 'sass' : 'scss', + scriptFolder: choices.script === 'TypeScript' ? 'ts' : 'js', + scriptExtension: choices.script === 'TypeScript' ? 'ts' : 'js', + markupFolder: choices.markup === 'Pug' ? 'pug' : 'html', + markupExtension: choices.markup === 'Pug' ? 'pug' : 'html', + }; +}