Skip to content

Commit c9e9125

Browse files
snoackphated
authored andcommitted
New: Support ESM w/ mjs extension where available (#214)
1 parent e2d7bce commit c9e9125

File tree

10 files changed

+145
-31
lines changed

10 files changed

+145
-31
lines changed

index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ var parser = yargs.usage(usage, cliOptions);
6262
var opts = parser.argv;
6363

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

6873
cli.on('requireFail', function(name, error) {

lib/shared/require-or-import.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
3+
var pathToFileURL = require('url').pathToFileURL;
4+
5+
var importESM;
6+
try {
7+
// Node.js <10 errors out with a SyntaxError when loading a script that uses import().
8+
// So a function is dynamically created to catch the SyntaxError at runtime instead of parsetime.
9+
// That way we can keep supporting all Node.js versions all the way back to 0.10.
10+
importESM = new Function('id', 'return import(id);');
11+
} catch (e) {
12+
importESM = null;
13+
}
14+
15+
function requireOrImport(path, callback) {
16+
var err = null;
17+
var cjs;
18+
try {
19+
cjs = require(path);
20+
} catch (e) {
21+
if (pathToFileURL && importESM && e.code === 'ERR_REQUIRE_ESM') {
22+
// This is needed on Windows, because import() fails if providing a Windows file path.
23+
var url = pathToFileURL(path);
24+
importESM(url).then(function(esm) { callback(null, esm); }, callback);
25+
return;
26+
}
27+
err = e;
28+
}
29+
process.nextTick(function() { callback(err, cjs); });
30+
}
31+
32+
module.exports = requireOrImport;

lib/versioned/^3.7.0/index.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ var copyTree = require('../../shared/log/copy-tree');
1111
var tildify = require('../../shared/tildify');
1212
var logTasks = require('../../shared/log/tasks');
1313
var ansi = require('../../shared/ansi');
14+
var exit = require('../../shared/exit');
1415
var logEvents = require('./log/events');
1516
var logTasksSimple = require('./log/tasks-simple');
1617
var registerExports = require('../../shared/register-exports');
18+
var requireOrImport = require('../../shared/require-or-import');
1719

1820
function execute(opts, env, config) {
1921
var tasks = opts._;
@@ -25,20 +27,25 @@ function execute(opts, env, config) {
2527
}
2628

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

31-
var gulpInst = require(env.modulePath);
32-
logEvents(gulpInst);
38+
log.info('Using gulpfile', ansi.magenta(tildify(env.configPath)));
3339

34-
registerExports(gulpInst, exported);
40+
var gulpInst = require(env.modulePath);
41+
logEvents(gulpInst);
3542

36-
// Always unmute stdout after gulpfile is required
37-
stdout.unmute();
43+
registerExports(gulpInst, exported);
3844

39-
process.nextTick(function() {
40-
var tree;
45+
// Always unmute stdout after gulpfile is required
46+
stdout.unmute();
4147

48+
var tree;
4249
if (opts.tasksSimple) {
4350
return logTasksSimple(env, gulpInst);
4451
}

lib/versioned/^4.0.0-alpha.1/index.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var logTasksSimple = require('../^4.0.0/log/tasks-simple');
1616
var registerExports = require('../../shared/register-exports');
1717

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

2021
function execute(opts, env, config) {
2122

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

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

37-
registerExports(gulpInst, exported);
44+
registerExports(gulpInst, exported);
3845

39-
// Always unmute stdout after gulpfile is required
40-
stdout.unmute();
46+
// Always unmute stdout after gulpfile is required
47+
stdout.unmute();
4148

42-
process.nextTick(function() {
4349
var tree;
44-
4550
if (opts.tasksSimple) {
4651
return logTasksSimple(gulpInst.tree());
4752
}

lib/versioned/^4.0.0-alpha.2/index.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var registerExports = require('../../shared/register-exports');
1717

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

2122
function execute(opts, env, config) {
2223

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

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

38-
registerExports(gulpInst, exported);
45+
registerExports(gulpInst, exported);
3946

40-
// Always unmute stdout after gulpfile is required
41-
stdout.unmute();
47+
// Always unmute stdout after gulpfile is required
48+
stdout.unmute();
4249

43-
process.nextTick(function() {
4450
var tree;
45-
4651
if (opts.tasksSimple) {
4752
tree = gulpInst.tree();
4853
return logTasksSimple(tree.nodes);

lib/versioned/^4.0.0/index.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var registerExports = require('../../shared/register-exports');
1717

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

2122
function execute(opts, env, config) {
2223

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

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

38-
registerExports(gulpInst, exported);
45+
registerExports(gulpInst, exported);
3946

40-
// Always unmute stdout after gulpfile is required
41-
stdout.unmute();
47+
// Always unmute stdout after gulpfile is required
48+
stdout.unmute();
4249

43-
process.nextTick(function() {
4450
var tree;
45-
4651
if (opts.tasksSimple) {
4752
tree = gulpInst.tree();
4853
return logTasksSimple(tree.nodes);

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@
3939
"copy-props": "^2.0.1",
4040
"fancy-log": "^1.3.2",
4141
"gulplog": "^1.0.0",
42-
"interpret": "^1.1.0",
42+
"interpret": "^1.4.0",
4343
"isobject": "^3.0.1",
4444
"liftoff": "^3.1.0",
4545
"matchdep": "^2.0.0",
4646
"mute-stdout": "^1.0.0",
4747
"pretty-hrtime": "^1.0.0",
4848
"replace-homedir": "^1.0.0",
4949
"semver-greatest-satisfied-range": "^1.1.0",
50-
"v8flags": "^3.0.1",
50+
"v8flags": "^3.2.0",
5151
"yargs": "^7.1.0"
5252
},
5353
"devDependencies": {
@@ -62,7 +62,8 @@
6262
"marked-man": "^0.2.1",
6363
"mocha": "^3.2.0",
6464
"nyc": "^13.3.0",
65-
"rimraf": "^2.6.1"
65+
"rimraf": "^2.6.1",
66+
"semver": "^5.7.1"
6667
},
6768
"keywords": [
6869
"build",

test/esm.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
var expect = require('expect');
4+
var fs = require('fs');
5+
var path = require('path');
6+
var semver = require('semver');
7+
var skipLines = require('gulp-test-tools').skipLines;
8+
var eraseTime = require('gulp-test-tools').eraseTime;
9+
var runner = require('gulp-test-tools').gulpRunner;
10+
11+
var expectedDir = path.join(__dirname, 'expected');
12+
13+
describe('ESM', function() {
14+
15+
it('prints the task list', function(done) {
16+
if (semver.lt(process.version, '10.15.3')) {
17+
this.skip();
18+
}
19+
20+
var options = '--tasks --sort-tasks ' +
21+
'--gulpfile ./test/fixtures/gulpfiles/gulpfile.mjs';
22+
var trailingLines = 1;
23+
if (!semver.satisfies(process.version, '^12.17.0 || >=13.2.0')) {
24+
options += ' --experimental-modules';
25+
trailingLines += 2;
26+
}
27+
28+
runner({ verbose: false }).gulp(options).run(cb);
29+
30+
function cb(err, stdout, stderr) {
31+
expect(err).toEqual(null);
32+
expect(stderr).toMatch(/^(.*ExperimentalWarning: The ESM module loader is experimental\.\n)?$/);
33+
var filepath = path.join(expectedDir, 'esm.txt');
34+
var expected = fs.readFileSync(filepath, 'utf-8');
35+
stdout = eraseTime(skipLines(stdout, trailingLines));
36+
expect(stdout).toEqual(expected);
37+
done(err);
38+
}
39+
});
40+
41+
});

test/expected/esm.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
gulp-cli/test/fixtures/gulpfiles
2+
├── exported
3+
└── registered

test/fixtures/gulpfiles/gulpfile.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import gulp from 'gulp';
2+
3+
function noop(cb) {
4+
cb();
5+
}
6+
7+
gulp.task('registered', noop);
8+
9+
export function exported(){};
10+
export const string = 'no function';

0 commit comments

Comments
 (0)