From 8ee4c5af9b11db10173cbc8d98b8b23b1b55c388 Mon Sep 17 00:00:00 2001 From: Raniro Coelho Date: Tue, 17 Mar 2026 12:03:23 -0300 Subject: [PATCH] docs: align branding and add architecture/ops documentation suite --- CONTRIBUTING.md | 16 +++ README.md | 31 +++++ _gulp/combineTasks.ts | 110 ---------------- _gulp/gulpSetup.ts | 13 +- _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 | 50 +++++++ _gulp/modules/setupCliOptions.ts | 83 ++++++++++++ _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 ++ docs/adr/0001-generated-gulpfile-runtime.md | 22 ++++ docs/adr/0002-template-composition.md | 17 +++ docs/generated-output-examples.md | 29 ++++ docs/roadmap-tracking.md | 25 ++++ docs/troubleshooting.md | 26 ++++ eslint.config.cjs | 29 ++++ package.json | 11 +- tsconfig.json | 30 ++++- typings/custon.d.ts | 6 - 43 files changed, 1756 insertions(+), 381 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 create mode 100644 docs/adr/0001-generated-gulpfile-runtime.md create mode 100644 docs/adr/0002-template-composition.md create mode 100644 docs/generated-output-examples.md create mode 100644 docs/roadmap-tracking.md create mode 100644 docs/troubleshooting.md create mode 100644 eslint.config.cjs delete mode 100644 typings/custon.d.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39a29f1..8902391 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,22 @@ Thanks for your interest in improving Web Learning Kit Generator. 3. Run the interactive generator: - `npm run init` +## Test strategy + +- **Unit tests**: validate individual modules (path resolution, setup option parsing, validators). +- **Template tests**: verify section behavior and snapshot generated gulpfile variants. +- **Integration tests**: scaffold in a temp directory and validate expected output tree/content. + +## Acceptance criteria for changes + +A change is ready to merge when all of the following are true: + +- Behavior changes are covered by tests (unit/integration/snapshot as appropriate). +- `npm run typecheck` passes. +- `npm test` passes. +- Docs are updated for any user-facing command or setup-flow change. +- PR notes describe generated-project behavior changes (if any). + ## Contribution guidelines - Keep changes small and focused. diff --git a/README.md b/README.md index 8f8f96b..ddc67ae 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 --yes +``` + Quality checks: ```bash @@ -73,6 +79,10 @@ npm test └── README.md ``` +## Generated output examples + +See concrete scaffold output trees here: [Generated output examples](./docs/generated-output-examples.md). + ## Current quality/devops baseline This repository now includes: @@ -81,11 +91,28 @@ 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) + +## Architecture decision + +This project intentionally uses **one runtime path**: + +1. collect user choices (interactive prompts or CLI flags) +2. scaffold source files +3. generate a single `gulpfile.js` +4. run gulp commands through npm scripts + +See ADRs for rationale and trade-offs: +- [ADR 0001: single generated-gulpfile runtime](./docs/adr/0001-generated-gulpfile-runtime.md) +- [ADR 0002: template composition](./docs/adr/0002-template-composition.md) --- ## Deep improvement roadmap +Track roadmap execution with milestones/labels using this guide: [Roadmap tracking](./docs/roadmap-tracking.md). + + If your goal is to make this starter more production-realistic for learners, implement the following in phases. ### Phase 1 — Reliability and maintainability (short term) @@ -146,6 +173,10 @@ If your goal is to make this starter more production-realistic for learners, imp - [Contributing guide](./CONTRIBUTING.md) - [Security policy](./SECURITY.md) +- [Troubleshooting](./docs/troubleshooting.md) +- [Generated output examples](./docs/generated-output-examples.md) +- [Roadmap tracking guide](./docs/roadmap-tracking.md) +- [Architecture Decision Records](./docs/adr) ## Inspiration 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..03884b9 100644 --- a/_gulp/gulpSetup.ts +++ b/_gulp/gulpSetup.ts @@ -3,15 +3,19 @@ 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'; async function setup(): Promise { try { + const parsedOptions = parseSetupOptions(process.argv.slice(2)); + const projectExists = fileExists('src') || fileExists('dist'); if (projectExists) { - const shouldDelete = await confirmProjectDeletion(); + const shouldDelete = parsedOptions.autoConfirm ? true : await confirmProjectDeletion(); if (!shouldDelete) { logger.info('Project setup canceled. Exiting...'); return; @@ -20,8 +24,9 @@ async function setup(): Promise { deleteDirectory('dist'); } - const choices: UserChoices = await promptUser(); - + const rawChoices = parsedOptions.shouldPrompt ? await promptUser() : parsedOptions.choices; + const choices: UserChoices = assertUserChoices(rawChoices); + await writeFile('_gulp/user-choices.json', JSON.stringify(choices, null, 2)); createProjectStructure(choices); @@ -32,7 +37,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..9cd8aa5 --- /dev/null +++ b/_gulp/modules/setupCliOptions.test.ts @@ -0,0 +1,50 @@ +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.autoConfirm, false); + 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.equal(parsed.autoConfirm, false); + assert.deepEqual(parsed.choices, { + script: 'TypeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: false, + }); +}); + +test('parseSetupOptions sets autoConfirm when --yes or -y is provided', () => { + const longFlag = parseSetupOptions(['--yes']); + const shortFlag = parseSetupOptions(['-y']); + + assert.equal(longFlag.autoConfirm, true); + assert.equal(shortFlag.autoConfirm, true); +}); + +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..479deb3 --- /dev/null +++ b/_gulp/modules/setupCliOptions.ts @@ -0,0 +1,83 @@ +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; + autoConfirm: 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 autoConfirm = argv.includes('--yes') || argv.includes('-y'); + + const hasChoiceFlag = rawScript !== undefined || rawStyle !== undefined || rawMarkup !== undefined; + if (!hasChoiceFlag) { + return { + shouldPrompt: true, + autoConfirm, + }; + } + + 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, + autoConfirm, + }; +} 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), + }; +} diff --git a/docs/adr/0001-generated-gulpfile-runtime.md b/docs/adr/0001-generated-gulpfile-runtime.md new file mode 100644 index 0000000..8e6214f --- /dev/null +++ b/docs/adr/0001-generated-gulpfile-runtime.md @@ -0,0 +1,22 @@ +# ADR 0001: Keep a single generated-gulpfile runtime + +- Status: Accepted +- Date: 2026-03-17 + +## Context +The project previously had two competing runtime paths: +1. A generated `gulpfile.js` flow. +2. A dynamic TypeScript task runtime. + +This created maintenance drift, duplicated behavior, and made onboarding harder. + +## Decision +Use a single runtime architecture: setup writes one generated `gulpfile.js` based on user choices. + +## Consequences +- Pros: Lower complexity, easier debugging, fewer drift bugs. +- Cons: Generator templates become the central maintenance surface. + +## Follow-up +- Keep snapshot tests for generated outputs. +- Keep path/option resolution centralized. diff --git a/docs/adr/0002-template-composition.md b/docs/adr/0002-template-composition.md new file mode 100644 index 0000000..c4a8812 --- /dev/null +++ b/docs/adr/0002-template-composition.md @@ -0,0 +1,17 @@ +# ADR 0002: Compose gulpfile from template sections + +- Status: Accepted +- Date: 2026-03-17 + +## Context +A monolithic template string for `gulpfile.js` was hard to evolve and review. + +## Decision +Split generation into composable template sections with a shared context object. + +## Consequences +- Pros: Easier review, easier targeted testing, smaller future diffs. +- Cons: More files to navigate for newcomers. + +## Follow-up +- Keep section-level unit tests and matrix snapshots. diff --git a/docs/generated-output-examples.md b/docs/generated-output-examples.md new file mode 100644 index 0000000..cc26c06 --- /dev/null +++ b/docs/generated-output-examples.md @@ -0,0 +1,29 @@ +# Generated output examples + +## Example A: JavaScript + Sass + HTML + +```text +src/ + js/main.js + sass/main.sass + html/index.html + img/ + favicon.ico +``` + +## Example B: TypeScript + SCSS + Pug + +```text +src/ + ts/main.ts + scss/main.scss + scss/base/_normalize.scss (optional) + scss/base/_reset.scss (optional) + pug/index.pug + img/ + favicon.ico +``` + +## Generated build entrypoints +- Dev/build pipeline in `gulpfile.js` +- Distribution output under `dist/` diff --git a/docs/roadmap-tracking.md b/docs/roadmap-tracking.md new file mode 100644 index 0000000..e3f93b2 --- /dev/null +++ b/docs/roadmap-tracking.md @@ -0,0 +1,25 @@ +# Roadmap tracking guide + +Convert roadmap items into trackable work with these conventions. + +## Labels +- `phase:1` ... `phase:5` +- `area:generator` +- `area:docs` +- `area:devops` +- `area:security` +- `type:refactor|feature|test|docs` + +## Milestones +- `Phase 1 - Reliability` +- `Phase 2 - Security` +- `Phase 3 - Learning UX` +- `Phase 4 - DevOps` +- `Phase 5 - Ecosystem` + +## Issue template for roadmap items +- Problem statement +- Proposed approach +- Acceptance criteria +- Validation commands +- Out-of-scope notes diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..f7a7b93 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,26 @@ +# Troubleshooting + +## `npm run init` fails with prompt/runtime errors +- Run `npm run typecheck` and `npm test` first. +- Remove generated folders and retry: `rm -rf src dist && npm run init`. +- For automation, pass full flags: `npm run init -- --script ts --style scss --markup pug --yes`. + +## `npm run lint` fails in restricted environments +If your environment blocks registry packages, lint dependencies may not install. +Use `npm run typecheck` + `npm test` as a fallback until package access is available. + +## BrowserSync or gulp startup issues +- Ensure Node 18+. +- Ensure dependencies are installed with `npm install`. +- Re-run init to regenerate `gulpfile.js` after changing options. + +## Invalid setup choices in automation mode +Provide all required flags together: +- `--script` +- `--style` +- `--markup` + +Optional flags: +- `--normalize` +- `--reset` +- `--yes` / `-y` diff --git a/eslint.config.cjs b/eslint.config.cjs new file mode 100644 index 0000000..f8d5e19 --- /dev/null +++ b/eslint.config.cjs @@ -0,0 +1,29 @@ +const js = require('@eslint/js'); +const tsParser = require('@typescript-eslint/parser'); +const tsPlugin = require('@typescript-eslint/eslint-plugin'); + +module.exports = [ + { + ignores: ['dist/**', 'node_modules/**', '_gulp/templates/gulpfile/__snapshots__/**'], + }, + js.configs.recommended, + { + files: ['_gulp/**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + sourceType: 'module', + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/consistent-type-imports': 'warn', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + }, +]; diff --git a/package.json b/package.json index be4d24f..4e008b7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "web-boilerplate-starter-kit", + "name": "web-learning-kit-generator", "version": "4.0.0", - "description": "A Front-end web kit and boilerplate for building web apps or small sites", + "description": "A learning-focused front-end project generator with a realistic Gulp workflow", "main": "gulpfile.js", "type": "commonjs", "scripts": { @@ -9,13 +9,15 @@ "start": "NODE_OPTIONS=--no-deprecation gulp", "build": "NODE_OPTIONS=--no-deprecation gulp build", "typecheck": "tsc --noEmit", - "test": "node --test -r ts-node/register _gulp/**/*.test.ts" + "test": "node --test -r ts-node/register _gulp/**/*.test.ts", + "lint": "eslint \"_gulp/**/*.ts\"" }, "author": "Raniro Coelho", "license": "MIT", "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", + "@eslint/js": "^9.39.2", "@types/browser-sync": "^2.27.0", "@types/browserify": "^12.0.40", "@types/gulp": "^4.0.10", @@ -32,10 +34,13 @@ "@types/pug": "^2.0.10", "@types/vinyl-buffer": "^1.0.3", "@types/vinyl-source-stream": "^0.0.34", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", "babelify": "^10.0.0", "browser-sync": "^2.29.3", "browserify": "^17.0.0", "del": "^6.1.1", + "eslint": "^9.39.2", "gulp": "^4.0.2", "gulp-autoprefixer": "^8.0.0", "gulp-clean-css": "^4.3.0", diff --git a/tsconfig.json b/tsconfig.json index 696ec67..f1e44a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,10 @@ "compilerOptions": { "target": "ES2018", "module": "commonjs", - "lib": ["es2018", "dom"], + "lib": [ + "es2018", + "dom" + ], "declaration": true, "declarationMap": true, "sourceMap": true, @@ -23,14 +26,27 @@ "moduleResolution": "node", "baseUrl": "./", "paths": { - "*": ["node_modules/*", "_gulp/types/*", "./typings"] + "*": [ + "node_modules/*", + "_gulp/types/*" + ] }, "esModuleInterop": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, "forceConsistentCasingInFileNames": true, - "types": ["node", "gulp", "gulp-imagemin", "browserify", "gulp-rename", "gulp-uglify", "vinyl-buffer", "vinyl-source-stream"], - "typeRoots": ["node_modules/@types", "_gulp/types", "./typings"] + "types": [ + "node", + "gulp", + "gulp-imagemin", + "browserify", + "gulp-rename", + "gulp-uglify", + "vinyl-buffer", + "vinyl-source-stream" + ], + "typeRoots": [ + "node_modules/@types", + "_gulp/types" + ] }, "include": [ "_gulp/**/*", @@ -40,4 +56,4 @@ "node_modules", "**/*.spec.ts" ] -} \ No newline at end of file +} diff --git a/typings/custon.d.ts b/typings/custon.d.ts deleted file mode 100644 index 3edbec1..0000000 --- a/typings/custon.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module 'gulp-imagemin'; -declare module 'browserify'; -declare module 'gulp-rename'; -declare module 'gulp-uglify'; -declare module 'vinyl-buffer'; -declare module 'vinyl-source-stream'; \ No newline at end of file