Skip to content

Instantly share code, notes, and snippets.

@dotherightthing
Last active July 2, 2024 13:15
Show Gist options
  • Save dotherightthing/e0639c0c5102993b86362ebe2a651ccc to your computer and use it in GitHub Desktop.
Save dotherightthing/e0639c0c5102993b86362ebe2a651ccc to your computer and use it in GitHub Desktop.
[Migrating a Gulpfile from Gulp 3.9.1 to 4.0.2] #gulp #es6 #babel

Migrating a Gulpfile from Gulp 3.9.1 to 4.0.2

Background

Via one of those mental leaps that one can only be achieved by working in two languages at once, I had found my way to Typescript.

I was excited about putting some rigour around my code. But as I sought to install the necessary dependencies, I started hitting versioning issues. I couldn't move forward with modern packages without addressing the dinosaur in the room, Gulp 3.9.1.

Migration Steps

1. Update Gulp

The first step was to update Gulp, so I could see what was broken:

yarn upgrade --latest

I updated everything, because I wanted to be running the newest and most compatible stack.

Note: The --latest flag tells Yarn to get the latest versions from NPM, then updates package.json with these, so that the existing n.n.n version limits are overwritten.

2. Install Babel

The Gulp Github page provided instructions on how to Use latest JavaScript version in your gulpfile.

That wording was important.

  • I was installing Babel to allow the Gulp script to run error-free inside Nodejs
  • I wasn't installing Babel to transpile project files (although I do use it for the latter via gulp-babel)

The instructions contained three steps:

Step 1: Rename gulpfile.js to gulpfile.babel.js

You can write a gulpfile using a language that requires transpilation, like TypeScript or Babel, by changing the extension on your gulpfile.js to indicate the language and install the matching transpiler module.

For Babel, rename to gulpfile.babel.js and install the @babel/register module.

~ Gulp - Transpilation:

To see what effect this had, I reverted the file name to gulpfile.js, after my migration.

Running a previously working task gave me this eror:

/Volumes/DanBackup 1/Websites/wpdtrt-plugin-boilerplate/gulpfile.js:27
import { series } from 'gulp';
       ^

SyntaxError: Unexpected token {

Renaming the file back to gulpfile.babel.js resolved this.

Note: Don't forget to update any references to gulpfile.js, e.g. .eslintrc overrides.

Step 2: Install @babel/register, @babel/core & @babel/preset-env

Because my Gulp file transpiles Front End JS from ES6 to ES5, my package.json already contained babel-core and babel-preset-env.

Were these the same as @babel/core and @babel/preset-env?

No,

  • babel- is the syntax for Babel version <7
  • @babel is the scoped syntax, for Babel version 7+

More information:

I removed "babel-core": "^6.26.3". I'll find out if I still need it as I proceed with testing.

What about @babel/register?

Step 3: Add the preset @babel/preset-env to .babelrc

I also already had a .babelrc.

  • It already contained a preset called env
  • I replaced this with @babel/env
  • I also updated the presets reference in my separate transpile task:
    return src( sources.js, { allowEmpty: true } )
      .pipe( babel( {
        presets: [ '@babel/env' ]
      } ) )
      .pipe( rename( {
        suffix: '-es5'
      } ) )
      .pipe( dest( targets.js ) );

3. What broke

Issue #1: gulp.hasTask is not a function

Heading over to StackOverflow, I found Gulp error: gulp.hasTask is not a function.

There were some red herrings here about local vs global installs of Gulp, but the key takeaway was that:

gulp v4 has breaking changes and that creates some problems with run-sequence package.

.. try to use try to use gulp.series and gulp.parallel with your gulp tasks instead of run-sequence

~ Gulp error: gulp.hasTask is not a function

If you're not familiar with run-sequence, it:

Runs a sequence of gulp tasks in the specified order. This function is designed to solve the situation where you have defined run-order, but choose not to or cannot use dependencies.

This was intended to be a temporary solution

~ run-sequence

The Gulp 4 way has a very similar syntax, but works without the extra package:

A. The run-sequence way:
const runSequence = require( 'run-sequence' );

gulp.task( 'myTaskGroup', ( done ) => {
    runSequence(
        'task1',
        'task2',
        'task3',
        'task4',
        done
    );
} );
B. The Gulp 4 way:
import { series } from 'gulp';

series(
    task1,
    task2,
    task3,
    task4
);

Issue #2. Tasks in series not waiting for one another

So now my tasks were in series, but they were just firing off without returning anything.

With run-sequence gone, my flow control was in ruins.

I headed back to StackOverflow and found this gem: Gulp error: The following tasks did not complete: Did you forget to signal async completion?.

A. Refactoring done callbacks:

Old:

function github( done ) {
  return ghRateLimit( {
    token: getGhToken()
  } ).then( ( status ) => {
    log( 'Github API rate limit:' );
    log( `API calls remaining: ${status.core.remaining}/${status.core.limit}` );
    log( ' ' );
  } );
}

New:

function github( done ) {
  return ghRateLimit( {
    token: getGhToken()
  } ).then( ( status ) => {
    log( 'Github API rate limit:' );
    log( `API calls remaining: ${status.core.remaining}/${status.core.limit}` );
    log( ' ' );

    done();
  } ).catch( err => {
    console.error( err );

    done();
  } );
}
B. Refactoring shell callbacks:

Old:

const dummyFile = './README.md';

function wpUnit() {
  const boilerplate = isBoilerplate();
  const boilerplatePath = getBoilerplatePath();
  const dbName = `${pluginNameSafe}_wpunit_${Date.now()}`;
  const wpVersion = 'latest';
  let installerPath = 'bin/';

  if ( !boilerplate ) {
    installerPath = `${boilerplatePath}bin/`;
  }

  return gulp.src( dummyFile, { read: false } )
    .pipe( shell( [
      `bash ${installerPath}install-wp-tests.sh ${dbName} ${wpVersion}`
    ] ) );
}

New:

function wpUnit() {
  const boilerplate = isBoilerplate();
  const boilerplatePath = getBoilerplatePath();
  const dbName = `${pluginNameSafe}_wpunit_${Date.now()}`;
  const wpVersion = 'latest';
  let installerPath = 'bin/';

  if ( !boilerplate ) {
    installerPath = `${boilerplatePath}bin/`;
  }

  const { stdout, stderr } = await exec( `./vendor/bin/phpunit --configuration ${boilerplatePath()}phpunit.xml.dist` );
  console.log( stdout );
  console.error( stderr );
}

Issue #3: Task never defined: taskName

First up, I refactored the file to use export

I'd also read somewhere that large Gulpfiles would be better split into smaller modules. After reading Gulp's advice that task() isn't the recommended pattern anymore - export your tasks and there export laden instructions on Creating Tasks, I misunderstood this as encouragement to 'export' my file into smaller modules.

I'd done this in the past with my Gruntfile, so I went ahead and did that, though it really wasn't necessary.

/**
 * File: gulp-modules/documentation.js
 *
 * Gulp tasks to generate documentation.
 */

import { series, src } from 'gulp';
import shell from 'gulp-shell';

// internal modules
import taskHeader from './taskheader';

// constants
const dummyFile = './README.md';

/**
 * Group: Tasks
 * _____________________________________
 */

/**
 * Function: naturalDocs
 *
 * Generate JS & PHP documentation.
 *
 * Returns:
 *   A stream - to signal task completion
 */
function naturalDocs() {
  taskHeader(
    '5a',
    'Documentation',
    'Documentation',
    'Natural Docs (JS & PHP)'
  );

  // Quotes escape space better than backslash on Travis
  const naturalDocsPath = 'Natural Docs/NaturalDocs.exe';

  // note: src files are not used,
  // this structure is only used
  // to include the preceding log()
  return src( dummyFile, { read: false } )
    .pipe( shell( [
      `mono "${naturalDocsPath}" ./config/naturaldocs`
    ] ) );
}

export default series(
  naturalDocs
);
/**
 * File: gulpfile.js
 *
 * Gulp build tasks.
 *
 * Note:
 * - See package.json for scripts, which can be run with:
 *   --- bash
 *   yarn run scriptname
 *   ---
 */

import { series } from 'gulp';

// internal modules
import documentation from './gulp-modules/documentation';

export { documentation as documentation };

/*
 * Export the default task
 *
 * Example:
 * --- bash
 * gulp
 * ---
 */
export default series( documentation );

Note: I realise that the single series are redundant.

What was more important was to realise that even after importing a module, I still had to export the function reference in order to make it callable via the gulp default task, and directly via gulp taskName.

This may be related to Forward References, although I am already using named functions, but perhaps not for the series().

Thus, this issue was also resolved.

5. File not found with singular glob

When the globs argument can only match one file (such as foo/bar.js) and no match is found, throws an error with the message, "File not found with singular glob". To suppress this error, set the allowEmpty option to true.

~ Gulp: (src)

This is a breaking change in Gulp 4 and highlights the current lack of Migration documentation on the Gulp website.

Still, they did provide a solution, but I found their advice confusing.

For the most part, I fed arrays of globs into my tasks, not 'singular globs'. It seemed that glob arrays failed if any of their items didn't find a match.

Adding { allowEmpty: true } resolved this issue:

// constants
const sources = {
  // note: paths are relative to gulpfile, not this file
  js: [
    './js/frontend.js',
    './js/backend.js',
    `./${boilerplatePath()}js/frontend.js`,
    `./${boilerplatePath()}js/backend.js`
  ]
};
const targets = {
  // note: paths are relative to gulpfile, not this file
  css: './css',
  js: './js'
};

/**
 * Function: js
 *
 * Transpile ES6+ to ES5, so that modern code runs in old browsers.
 *
 * Returns:
 *   A stream to signal task completion
 */
function js() {
  taskHeader(
    '3a',
    'Assets',
    'Transpile',
    'ES6+ JS -> ES5 JS'
  );

  return src( sources.js, { allowEmpty: true } )
    .pipe( babel( {
      presets: [ '@babel/env' ]
    } ) )
    .pipe( rename( {
      suffix: '-es5'
    } ) )
    .pipe( dest( targets.js ) );
}

6. Async/Await broken

This was caused by introducing the modernised flow control, to address Issue #2. Tasks in series not waiting for one another

ReferenceError: regeneratorRuntime is not defined
    at _yarn (/Volumes/DanBackup/Websites/wpdtrt-plugin-boilerplate/gulp-modules/dependencies.js:207:3)
    at yarn (/Volumes/DanBackup/Websites/wpdtrt-plugin-boilerplate/gulp-modules/dependencies.js:201:16)
    at bound (domain.js:402:14)
    at runBound (domain.js:415:12)
    at asyncRunner (/Volumes/DanBackup/Websites/wpdtrt-plugin-boilerplate/node_modules/async-done/index.js:55:18)
    at process._tickCallback (internal/process/next_tick.js:61:11)

From: Babel 7 - ReferenceError: regeneratorRuntime is not defined:

@babel/polyfill

🚨 As of Babel 7.4.0, this package has been deprecated in favor of directly including core-js/stable (to polyfill ECMAScript features) and regenerator-runtime/runtime (needed to use transpiled generator functions):

yarn add core-js --dev
yarn add regenerator-runtime --dev

Then in my Gulpfile:

import 'core-js/stable';
import 'regenerator-runtime/runtime';

7. Other issues

TODO: I'll need to run my earlier code to see what other issues I had.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment