Skip to content

Commit

Permalink
New: Support ESM w/ mjs extension where available (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
snoack authored and phated committed Jun 3, 2020
1 parent e2d7bce commit c9e9125
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 31 deletions.
7 changes: 6 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ var parser = yargs.usage(usage, cliOptions);
var opts = parser.argv;

cli.on('require', function(name) {
log.info('Requiring external module', ansi.magenta(name));
// This is needed because interpret needs to stub the .mjs extension
// Without the .mjs require hook, rechoir blows up
// However, we don't want to show the mjs-stub loader in the logs
if (path.basename(name, '.js') !== 'mjs-stub') {
log.info('Requiring external module', ansi.magenta(name));
}
});

cli.on('requireFail', function(name, error) {
Expand Down
32 changes: 32 additions & 0 deletions lib/shared/require-or-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

var pathToFileURL = require('url').pathToFileURL;

var importESM;
try {
// Node.js <10 errors out with a SyntaxError when loading a script that uses import().
// So a function is dynamically created to catch the SyntaxError at runtime instead of parsetime.
// That way we can keep supporting all Node.js versions all the way back to 0.10.
importESM = new Function('id', 'return import(id);');
} catch (e) {
importESM = null;
}

function requireOrImport(path, callback) {
var err = null;
var cjs;
try {
cjs = require(path);
} catch (e) {
if (pathToFileURL && importESM && e.code === 'ERR_REQUIRE_ESM') {
// This is needed on Windows, because import() fails if providing a Windows file path.
var url = pathToFileURL(path);
importESM(url).then(function(esm) { callback(null, esm); }, callback);
return;
}
err = e;
}
process.nextTick(function() { callback(err, cjs); });
}

module.exports = requireOrImport;
25 changes: 16 additions & 9 deletions lib/versioned/^3.7.0/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ var copyTree = require('../../shared/log/copy-tree');
var tildify = require('../../shared/tildify');
var logTasks = require('../../shared/log/tasks');
var ansi = require('../../shared/ansi');
var exit = require('../../shared/exit');
var logEvents = require('./log/events');
var logTasksSimple = require('./log/tasks-simple');
var registerExports = require('../../shared/register-exports');
var requireOrImport = require('../../shared/require-or-import');

function execute(opts, env, config) {
var tasks = opts._;
Expand All @@ -25,20 +27,25 @@ function execute(opts, env, config) {
}

// This is what actually loads up the gulpfile
var exported = require(env.configPath);
log.info('Using gulpfile', ansi.magenta(tildify(env.configPath)));
requireOrImport(env.configPath, function(err, exported) {
// Before import(), if require() failed we got an unhandled exception on the module level.
// So console.error() & exit() were added here to mimic the old behavior as close as possible.
if (err) {
console.error(err);
exit(1);
}

var gulpInst = require(env.modulePath);
logEvents(gulpInst);
log.info('Using gulpfile', ansi.magenta(tildify(env.configPath)));

registerExports(gulpInst, exported);
var gulpInst = require(env.modulePath);
logEvents(gulpInst);

// Always unmute stdout after gulpfile is required
stdout.unmute();
registerExports(gulpInst, exported);

process.nextTick(function() {
var tree;
// Always unmute stdout after gulpfile is required
stdout.unmute();

var tree;
if (opts.tasksSimple) {
return logTasksSimple(env, gulpInst);
}
Expand Down
17 changes: 11 additions & 6 deletions lib/versioned/^4.0.0-alpha.1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var logTasksSimple = require('../^4.0.0/log/tasks-simple');
var registerExports = require('../../shared/register-exports');

var copyTree = require('../../shared/log/copy-tree');
var requireOrImport = require('../../shared/require-or-import');

function execute(opts, env, config) {

Expand All @@ -32,16 +33,20 @@ function execute(opts, env, config) {
logSyncTask(gulpInst, opts);

// This is what actually loads up the gulpfile
var exported = require(env.configPath);
requireOrImport(env.configPath, function(err, exported) {
// Before import(), if require() failed we got an unhandled exception on the module level.
// So console.error() & exit() were added here to mimic the old behavior as close as possible.
if (err) {
console.error(err);
exit(1);
}

registerExports(gulpInst, exported);
registerExports(gulpInst, exported);

// Always unmute stdout after gulpfile is required
stdout.unmute();
// Always unmute stdout after gulpfile is required
stdout.unmute();

process.nextTick(function() {
var tree;

if (opts.tasksSimple) {
return logTasksSimple(gulpInst.tree());
}
Expand Down
17 changes: 11 additions & 6 deletions lib/versioned/^4.0.0-alpha.2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var registerExports = require('../../shared/register-exports');

var copyTree = require('../../shared/log/copy-tree');
var getTask = require('../^4.0.0/log/get-task');
var requireOrImport = require('../../shared/require-or-import');

function execute(opts, env, config) {

Expand All @@ -33,16 +34,20 @@ function execute(opts, env, config) {
logSyncTask(gulpInst, opts);

// This is what actually loads up the gulpfile
var exported = require(env.configPath);
requireOrImport(env.configPath, function(err, exported) {
// Before import(), if require() failed we got an unhandled exception on the module level.
// So console.error() & exit() were added here to mimic the old behavior as close as possible.
if (err) {
console.error(err);
exit(1);
}

registerExports(gulpInst, exported);
registerExports(gulpInst, exported);

// Always unmute stdout after gulpfile is required
stdout.unmute();
// Always unmute stdout after gulpfile is required
stdout.unmute();

process.nextTick(function() {
var tree;

if (opts.tasksSimple) {
tree = gulpInst.tree();
return logTasksSimple(tree.nodes);
Expand Down
17 changes: 11 additions & 6 deletions lib/versioned/^4.0.0/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var registerExports = require('../../shared/register-exports');

var copyTree = require('../../shared/log/copy-tree');
var getTask = require('./log/get-task');
var requireOrImport = require('../../shared/require-or-import');

function execute(opts, env, config) {

Expand All @@ -33,16 +34,20 @@ function execute(opts, env, config) {
logSyncTask(gulpInst, opts);

// This is what actually loads up the gulpfile
var exported = require(env.configPath);
requireOrImport(env.configPath, function(err, exported) {
// Before import(), if require() failed we got an unhandled exception on the module level.
// So console.error() & exit() were added here to mimic the old behavior as close as possible.
if (err) {
console.error(err);
exit(1);
}

registerExports(gulpInst, exported);
registerExports(gulpInst, exported);

// Always unmute stdout after gulpfile is required
stdout.unmute();
// Always unmute stdout after gulpfile is required
stdout.unmute();

process.nextTick(function() {
var tree;

if (opts.tasksSimple) {
tree = gulpInst.tree();
return logTasksSimple(tree.nodes);
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@
"copy-props": "^2.0.1",
"fancy-log": "^1.3.2",
"gulplog": "^1.0.0",
"interpret": "^1.1.0",
"interpret": "^1.4.0",
"isobject": "^3.0.1",
"liftoff": "^3.1.0",
"matchdep": "^2.0.0",
"mute-stdout": "^1.0.0",
"pretty-hrtime": "^1.0.0",
"replace-homedir": "^1.0.0",
"semver-greatest-satisfied-range": "^1.1.0",
"v8flags": "^3.0.1",
"v8flags": "^3.2.0",
"yargs": "^7.1.0"
},
"devDependencies": {
Expand All @@ -62,7 +62,8 @@
"marked-man": "^0.2.1",
"mocha": "^3.2.0",
"nyc": "^13.3.0",
"rimraf": "^2.6.1"
"rimraf": "^2.6.1",
"semver": "^5.7.1"
},
"keywords": [
"build",
Expand Down
41 changes: 41 additions & 0 deletions test/esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

var expect = require('expect');
var fs = require('fs');
var path = require('path');
var semver = require('semver');
var skipLines = require('gulp-test-tools').skipLines;
var eraseTime = require('gulp-test-tools').eraseTime;
var runner = require('gulp-test-tools').gulpRunner;

var expectedDir = path.join(__dirname, 'expected');

describe('ESM', function() {

it('prints the task list', function(done) {
if (semver.lt(process.version, '10.15.3')) {
this.skip();
}

var options = '--tasks --sort-tasks ' +
'--gulpfile ./test/fixtures/gulpfiles/gulpfile.mjs';
var trailingLines = 1;
if (!semver.satisfies(process.version, '^12.17.0 || >=13.2.0')) {
options += ' --experimental-modules';
trailingLines += 2;
}

runner({ verbose: false }).gulp(options).run(cb);

function cb(err, stdout, stderr) {
expect(err).toEqual(null);
expect(stderr).toMatch(/^(.*ExperimentalWarning: The ESM module loader is experimental\.\n)?$/);
var filepath = path.join(expectedDir, 'esm.txt');
var expected = fs.readFileSync(filepath, 'utf-8');
stdout = eraseTime(skipLines(stdout, trailingLines));
expect(stdout).toEqual(expected);
done(err);
}
});

});
3 changes: 3 additions & 0 deletions test/expected/esm.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
gulp-cli/test/fixtures/gulpfiles
├── exported
└── registered
10 changes: 10 additions & 0 deletions test/fixtures/gulpfiles/gulpfile.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import gulp from 'gulp';

function noop(cb) {
cb();
}

gulp.task('registered', noop);

export function exported(){};
export const string = 'no function';

0 comments on commit c9e9125

Please sign in to comment.