Skip to main content

Migrating from Gulp/Grunt to Xec

Overview​

This guide helps you migrate from JavaScript task runners like Gulp and Grunt to Xec's modern execution system. While Gulp and Grunt revolutionized front-end build tooling, Xec provides a more comprehensive solution with TypeScript support, multi-environment execution, and better performance.

Why Migrate from Gulp/Grunt?​

Gulp/Grunt Limitations​

Gulp Example:

// gulpfile.js
const gulp = require('gulp');
const sass = require('gulp-sass');
const uglify = require('gulp-uglify');
const concat = require('gulp-concat');

gulp.task('styles', () => {
return gulp.src('src/scss/**/*.scss')
.pipe(sass().on('error', sass.logError))
.pipe(concat('app.css'))
.pipe(gulp.dest('dist/css'));
});

gulp.task('scripts', () => {
return gulp.src('src/js/**/*.js')
.pipe(uglify())
.pipe(concat('app.js'))
.pipe(gulp.dest('dist/js'));
});

gulp.task('default', gulp.series('styles', 'scripts'));

Grunt Example:

// Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
sass: {
dist: {
files: {
'dist/css/app.css': 'src/scss/main.scss'
}
}
},
uglify: {
dist: {
files: {
'dist/js/app.js': ['src/js/**/*.js']
}
}
},
watch: {
styles: {
files: ['src/scss/**/*.scss'],
tasks: ['sass']
}
}
});

grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');

grunt.registerTask('default', ['sass', 'uglify']);
};

Problems:

  • Plugin ecosystem fragmentation
  • Complex configuration syntax
  • Memory intensive streaming
  • Limited to local execution
  • Callback hell in complex workflows
  • Maintenance burden of plugins

Xec Advantages​

// scripts/build.ts
import { $, glob, watch } from '@xec-sh/core';

// Compile styles
async function buildStyles() {
await $`sass src/scss/main.scss dist/css/app.css --compressed`;
console.log('βœ… Styles compiled');
}

// Build scripts
async function buildScripts() {
const files = await glob('src/js/**/*.js');
await $`esbuild ${files.join(' ')} --bundle --minify --outfile=dist/js/app.js`;
console.log('βœ… Scripts bundled');
}

// Run both in parallel
await Promise.all([buildStyles(), buildScripts()]);

Benefits:

  • Native TypeScript with type safety
  • Direct tool invocation (no wrapper plugins)
  • Multi-environment execution
  • Better performance (no streaming overhead)
  • Modern async/await patterns
  • Integrated file watching

Core Concepts Mapping​

Gulp/Grunt β†’ Xec​

Gulp/Grunt ConceptXec EquivalentDescription
TaskTask/ScriptUnit of work
gulp.src()glob()File selection
.pipe()Shell pipes or awaitCommand chaining
gulp.dest()File operationsOutput handling
gulp.watch()watch commandFile monitoring
gulp.series()Sequential stepsTask ordering
gulp.parallel()Promise.all()Parallel execution
PluginsDirect toolsNo wrapper needed

Common Task Migrations​

1. Style Processing​

Gulp:

const sass = require('gulp-sass')(require('sass'));
const autoprefixer = require('gulp-autoprefixer');
const cleanCSS = require('gulp-clean-css');

gulp.task('styles', () => {
return gulp.src('src/scss/**/*.scss')
.pipe(sass().on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cleanCSS())
.pipe(gulp.dest('dist/css'));
});

Xec Script:

// scripts/styles.ts
import { $, glob } from '@xec-sh/core';
import path from 'path';

export async function buildStyles() {
const sassFiles = await glob('src/scss/**/*.scss');

for (const file of sassFiles) {
const output = file
.replace('src/scss', 'dist/css')
.replace('.scss', '.css');

// Compile SASS, add prefixes, and minify
await $`sass ${file} ${output} --compressed`;
await $`postcss ${output} --use autoprefixer -o ${output}`;
}

console.log(`βœ… Compiled ${sassFiles.length} style files`);
}

// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
await buildStyles();
}

2. JavaScript Bundling​

Grunt:

grunt.initConfig({
concat: {
dist: {
src: ['src/js/lib/*.js', 'src/js/app.js'],
dest: 'dist/js/bundle.js'
}
},
uglify: {
dist: {
files: {
'dist/js/bundle.min.js': ['dist/js/bundle.js']
}
}
},
babel: {
options: {
presets: ['@babel/preset-env']
},
dist: {
files: {
'dist/js/app.js': 'src/js/app.js'
}
}
}
});

Xec Script:

// scripts/bundle.ts
import { $, glob } from '@xec-sh/core';

export async function bundleScripts() {
// Use modern bundler instead of concat/uglify
await $`esbuild src/js/app.js \
--bundle \
--minify \
--sourcemap \
--target=es2020 \
--outfile=dist/js/bundle.min.js`;

// Or use webpack/rollup if preferred
// await $`webpack --mode production`;

console.log('βœ… JavaScript bundled and minified');
}

3. Image Optimization​

Gulp:

const imagemin = require('gulp-imagemin');

gulp.task('images', () => {
return gulp.src('src/images/**/*')
.pipe(imagemin([
imagemin.gifsicle({interlaced: true}),
imagemin.mozjpeg({quality: 75}),
imagemin.optipng({optimizationLevel: 5}),
imagemin.svgo()
]))
.pipe(gulp.dest('dist/images'));
});

Xec Script:

// scripts/images.ts
import { $, glob } from '@xec-sh/core';
import path from 'path';
import { mkdir } from 'fs/promises';

export async function optimizeImages() {
const images = await glob('src/images/**/*.{jpg,png,gif,svg}');

// Ensure output directory exists
await mkdir('dist/images', { recursive: true });

// Process images in parallel with concurrency limit
const batchSize = 5;
for (let i = 0; i < images.length; i += batchSize) {
const batch = images.slice(i, i + batchSize);

await Promise.all(batch.map(async (image) => {
const output = image.replace('src/', 'dist/');
const dir = path.dirname(output);

await mkdir(dir, { recursive: true });

// Use sharp or imagemin CLI
await $`imagemin ${image} --out-dir=${dir}`;
}));
}

console.log(`βœ… Optimized ${images.length} images`);
}

4. File Watching​

Gulp:

gulp.task('watch', () => {
gulp.watch('src/scss/**/*.scss', gulp.series('styles'));
gulp.watch('src/js/**/*.js', gulp.series('scripts'));
gulp.watch('src/images/**/*', gulp.series('images'));
});

Xec Configuration:

# .xec/config.yaml
tasks:
watch:styles:
command: xec watch --pattern "src/scss/**/*.scss" --exec "xec build:styles"

watch:scripts:
command: xec watch --pattern "src/js/**/*.js" --exec "xec build:scripts"

watch:all:
parallel: true
steps:
- command: xec watch:styles
- command: xec watch:scripts

Xec Script:

// scripts/watch.ts
import { watch } from '@xec-sh/core';
import { buildStyles } from './styles';
import { bundleScripts } from './bundle';

// Watch multiple patterns
watch({
'src/scss/**/*.scss': buildStyles,
'src/js/**/*.js': bundleScripts,
'src/images/**/*': () => $`xec optimize:images`
});

console.log('πŸ‘€ Watching for changes...');

5. Clean Task​

Grunt:

grunt.initConfig({
clean: {
dist: ['dist/**/*', 'tmp/**/*'],
cache: ['.sass-cache', 'node_modules/.cache']
}
});

Xec Script:

// scripts/clean.ts
import { rm } from 'fs/promises';
import { $, glob } from '@xec-sh/core';

export async function clean() {
const dirs = ['dist', 'tmp', '.sass-cache', 'node_modules/.cache'];

await Promise.all(
dirs.map(dir =>
rm(dir, { recursive: true, force: true })
)
);

// Clean specific file patterns
const tempFiles = await glob('**/*.tmp');
await Promise.all(
tempFiles.map(file => rm(file))
);

console.log('🧹 Cleaned build artifacts');
}

Complex Workflow Migration​

Gulp Workflow Example​

// gulpfile.js
const gulp = require('gulp');
const sass = require('gulp-sass');
const browserSync = require('browser-sync').create();
const useref = require('gulp-useref');
const gulpIf = require('gulp-if');
const uglify = require('gulp-uglify');
const cssnano = require('gulp-cssnano');
const del = require('del');
const runSequence = require('run-sequence');

// Development tasks
gulp.task('sass', () => {
return gulp.src('app/scss/**/*.scss')
.pipe(sass())
.pipe(gulp.dest('app/css'))
.pipe(browserSync.reload({
stream: true
}));
});

gulp.task('browserSync', () => {
browserSync.init({
server: {
baseDir: 'app'
}
});
});

gulp.task('watch', ['browserSync', 'sass'], () => {
gulp.watch('app/scss/**/*.scss', ['sass']);
gulp.watch('app/*.html', browserSync.reload);
gulp.watch('app/js/**/*.js', browserSync.reload);
});

// Production tasks
gulp.task('useref', () => {
return gulp.src('app/*.html')
.pipe(useref())
.pipe(gulpIf('*.js', uglify()))
.pipe(gulpIf('*.css', cssnano()))
.pipe(gulp.dest('dist'));
});

gulp.task('images', () => {
return gulp.src('app/images/**/*.+(png|jpg|gif|svg)')
.pipe(imagemin())
.pipe(gulp.dest('dist/images'));
});

gulp.task('fonts', () => {
return gulp.src('app/fonts/**/*')
.pipe(gulp.dest('dist/fonts'));
});

gulp.task('clean:dist', () => {
return del.sync('dist');
});

gulp.task('build', (callback) => {
runSequence('clean:dist',
['sass', 'useref', 'images', 'fonts'],
callback
);
});

gulp.task('default', ['watch']);

Migrated to Xec​

# .xec/config.yaml
tasks:
dev:
description: Start development server with watch
parallel: true
steps:
- name: Build styles
command: xec build:styles --watch
- name: Start server
command: xec serve

build:
description: Production build
steps:
- name: Clean
command: xec clean
- name: Build assets
parallel: true
steps:
- command: xec build:styles --production
- command: xec build:scripts --production
- command: xec optimize:images
- command: xec copy:fonts
- name: Generate HTML
command: xec build:html

serve:
command: browser-sync start --server app --files "app/**/*"
// scripts/build.ts
import { $, glob, fs } from '@xec-sh/core';
import path from 'path';

const isProduction = process.argv.includes('--production');
const isWatch = process.argv.includes('--watch');

export async function buildStyles() {
const sassFiles = await glob('app/scss/**/*.scss');

for (const file of sassFiles) {
const output = file
.replace('app/scss', isProduction ? 'dist/css' : 'app/css')
.replace('.scss', '.css');

const commands = [
`sass ${file} ${output}`,
isProduction && `postcss ${output} --use cssnano -o ${output}`
].filter(Boolean);

for (const cmd of commands) {
await $`${cmd}`;
}
}

console.log('βœ… Styles built');
}

export async function buildScripts() {
const target = isProduction ? 'dist' : 'app';

await $`esbuild app/js/main.js \
--bundle \
${isProduction ? '--minify' : ''} \
--sourcemap \
--outfile=${target}/js/bundle.js`;

console.log('βœ… Scripts built');
}

export async function buildHtml() {
const html = await fs.readFile('app/index.html', 'utf-8');

// Simple asset processing
const processed = html
.replace(/<!-- build:css (.+?) -->/g, '<link rel="stylesheet" href="$1">')
.replace(/<!-- build:js (.+?) -->/g, '<script src="$1"></script>');

await fs.writeFile('dist/index.html', processed);
console.log('βœ… HTML processed');
}

export async function optimizeImages() {
const images = await glob('app/images/**/*.{jpg,png,gif,svg}');

await Promise.all(
images.map(async (image) => {
const output = image.replace('app/', 'dist/');
await fs.mkdir(path.dirname(output), { recursive: true });
await $`imagemin ${image} --out-dir=${path.dirname(output)}`;
})
);

console.log(`βœ… Optimized ${images.length} images`);
}

export async function copyFonts() {
await $`xec copy app/fonts/ dist/fonts/`;
console.log('βœ… Fonts copied');
}

export async function clean() {
await fs.rm('dist', { recursive: true, force: true });
console.log('🧹 Cleaned dist directory');
}

// Main build orchestration
export async function build() {
await clean();

await Promise.all([
buildStyles(),
buildScripts(),
optimizeImages(),
copyFonts()
]);

await buildHtml();

console.log('πŸŽ‰ Build complete!');
}

// Watch mode
if (isWatch) {
const { watch } = await import('@xec-sh/core');

watch({
'app/scss/**/*.scss': buildStyles,
'app/js/**/*.js': buildScripts,
'app/images/**/*': optimizeImages
});

console.log('πŸ‘€ Watching for changes...');
}

Stream Processing Alternative​

If you prefer Gulp's streaming approach, you can create similar patterns:

// scripts/stream-example.ts
import { $, glob } from '@xec-sh/core';
import { pipeline } from 'stream/promises';
import { createReadStream, createWriteStream } from 'fs';
import { Transform } from 'stream';

// Create a transform stream
function createMinifyStream() {
return new Transform({
async transform(chunk, encoding, callback) {
// Process chunk
const minified = await minifyCode(chunk.toString());
callback(null, minified);
}
});
}

// Use pipeline for streaming
async function processLargeFile() {
await pipeline(
createReadStream('src/large-file.js'),
createMinifyStream(),
createWriteStream('dist/large-file.min.js')
);
}

Plugin Replacement Guide​

Common Gulp/Grunt Plugins β†’ Direct Tools​

Gulp/Grunt PluginXec ReplacementCommand Example
gulp-sasssass CLIsass input.scss output.css
gulp-uglifyterser/esbuildterser input.js -o output.js
gulp-concatcat/esbuildcat file1.js file2.js > bundle.js
gulp-autoprefixerpostcsspostcss file.css --use autoprefixer
gulp-imageminimagemin-cliimagemin images/* --out-dir=dist
gulp-babelbabel CLIbabel src -d dist
gulp-eslinteslinteslint src/**/*.js
gulp-renamemv/cpcp file.js file.min.js
browser-syncbrowser-syncbrowser-sync start
gulp-sourcemapsBuilt into toolsesbuild --sourcemap

Migration Strategy​

Phase 1: Assessment​

  1. List all Gulp/Grunt tasks
  2. Identify plugin dependencies
  3. Map to equivalent CLI tools
  4. Plan migration order

Phase 2: Parallel Implementation​

// package.json - Keep both during transition
{
"scripts": {
"build:gulp": "gulp build",
"build:xec": "xec build",
"build": "npm run build:xec"
}
}

Phase 3: Incremental Migration​

Week 1-2: Core Tasks

  • Build tasks
  • Clean tasks
  • Copy tasks

Week 3-4: Complex Workflows

  • Watch tasks
  • Development server
  • Production builds

Week 5: Optimization

  • Performance tuning
  • Parallel execution
  • Error handling

Phase 4: Cleanup​

  • Remove Gulp/Grunt dependencies
  • Delete old config files
  • Update documentation

Performance Comparison​

Build Time Improvements​

// Measure build performance
import { performance } from 'perf_hooks';

async function benchmarkBuild() {
const start = performance.now();

// Parallel execution with Xec
await Promise.all([
buildStyles(),
buildScripts(),
optimizeImages()
]);

const end = performance.now();
console.log(`Build completed in ${(end - start) / 1000}s`);

// Typical results:
// Gulp: 12-15s (sequential plugins)
// Xec: 4-6s (parallel, direct tools)
}

Advanced Features After Migration​

1. Multi-Environment Builds​

// Build for different environments
const env = process.env.NODE_ENV || 'development';

await $`webpack --mode ${env}`;

if (env === 'production') {
// Deploy to production servers
await on('production-servers', 'systemctl restart app');
}

2. Conditional Processing​

// Smart rebuilds based on changes
import { createHash } from 'crypto';

const cache = new Map();

async function shouldRebuild(file: string): Promise<boolean> {
const content = await fs.readFile(file);
const hash = createHash('md5').update(content).digest('hex');

if (cache.get(file) !== hash) {
cache.set(file, hash);
return true;
}

return false;
}

3. Remote Execution​

// Build locally, deploy remotely
await $`npm run build`;
await on('staging-server', 'docker build -t app .');
await on('staging-server', 'docker run -d app');

Common Migration Issues​

1. Plugin Dependencies​

  • Gulp/Grunt plugins may have unique features
  • Research CLI alternatives or Node packages
  • Some functionality may need custom implementation

2. Configuration Complexity​

  • Gulp/Grunt configs can be very complex
  • Break down into smaller, focused scripts
  • Use TypeScript for better organization

3. Streaming vs Promises​

  • Gulp uses streams extensively
  • Xec uses promises/async-await
  • Can use Node streams when needed

Summary​

Migrating from Gulp/Grunt to Xec provides:

  • βœ… Direct tool usage (no plugin overhead)
  • βœ… TypeScript with full type safety
  • βœ… Better performance through parallelization
  • βœ… Multi-environment execution
  • βœ… Modern async/await patterns
  • βœ… Simplified dependency management

Start by migrating simple tasks, then gradually move complex workflows to experience the benefits of Xec's modern approach!