grunt-contrib-connect
Version:
Start a connect web server
260 lines (229 loc) • 9.77 kB
JavaScript
/*
* grunt-contrib-connect
* https://gruntjs.com/
*
* Copyright (c) 2016 "Cowboy" Ben Alman, contributors
* Licensed under the MIT license.
*/
;
module.exports = function(grunt) {
var path = require('path');
var connect = require('connect');
var morgan = require('morgan');
var serveStatic = require('serve-static');
var serveIndex = require('serve-index');
var http = require('http');
var https = require('https');
var http2 = require('http2-wrapper');
var injectLiveReload = require('connect-livereload');
var open = require('open');
var portscanner = require('portscanner');
var async = require('async');
var MAX_PORTS = 30; // Maximum available ports to check after the specified port
var createDefaultMiddleware = function createDefaultMiddleware(connect, options) {
var middlewares = [];
if (!Array.isArray(options.base)) {
options.base = [options.base];
}
// Options for serve-static module. See https://www.npmjs.com/package/serve-static
var defaultStaticOptions = {};
var directory = options.directory || options.base[options.base.length - 1];
options.base.forEach(function(base) {
// Serve static files.
var path = base.path || base;
var staticOptions = base.options || defaultStaticOptions;
middlewares.push(serveStatic(path, staticOptions));
});
// Make directory browse-able.
middlewares.push(serveIndex(directory.path || directory, directory.options));
return middlewares;
};
grunt.registerMultiTask('connect', 'Start a connect web server.', function() {
var done = this.async();
// Merge task-specific options with these defaults.
var options = this.options({
protocol: 'http',
port: 8000,
hostname: '0.0.0.0',
base: '.',
directory: null,
keepalive: false,
debug: false,
livereload: false,
open: false,
useAvailablePort: false,
onCreateServer: null,
// if nothing passed, then is set below 'middleware = createDefaultMiddleware.call(this, connect, options);'
middleware: null
});
if (options.protocol !== 'http' && options.protocol !== 'https' && options.protocol !== 'http2') {
grunt.fatal('protocol option must be \'http\', \'https\' or \'http2\'');
}
// Connect requires the base path to be absolute.
if (Array.isArray(options.base)) {
options.base = options.base.map(function(base) {
if (base.path) {
base.path = path.resolve(base.path);
return base;
}
return path.resolve(base);
});
} else {
if (options.base.path) {
options.base.path = path.resolve(options.base.path);
} else {
options.base = path.resolve(options.base);
}
}
// Backward compatibility: Support wildcard (see README).
//
// Support Node 18: "localhost" now expands to IPv6 "::" by default.
// Listening for "0.0.0.0" is IPv4-only. In order to make sure grunt-contrib-connect usage
// works as-is, both if you relied on "localhost" and if you relied on "127.0.0.1", bind the
// listen to IPv6 "::" so that "localhost" keeps working. On most operating systems,
// listening on "::" also results in receiving packets for 127.0.0.1.
// Ref https://github.com/nodejs/node/issues/40702
if (!options.hostname || options.hostname === '*' || options.hostname === '0.0.0.0') {
options.hostname = '::';
}
// Connect will listen to ephemeral port if asked
if (options.port === '?') {
options.port = 0;
}
if (options.onCreateServer && !Array.isArray(options.onCreateServer)) {
options.onCreateServer = [options.onCreateServer];
}
// The middleware options may be null, an array of middleware objects,
// or a factory function that creates an array of middleware objects.
// * For a null value, use the default array of middleware
// * For a function, include the default array of middleware as the last arg
// which enables the function to patch the default middleware without needing to know
// the implementation of the default middleware factory function
var middleware;
if (options.middleware instanceof Array) {
middleware = options.middleware;
} else {
middleware = createDefaultMiddleware.call(this, connect, options);
if (typeof options.middleware === 'function') {
middleware = options.middleware.call(this, connect, options, middleware);
}
}
// If --debug was specified, enable logging.
if (grunt.option('debug') || options.debug === true) {
middleware.unshift(morgan('dev'));
}
// Start server.
var taskTarget = this.target;
var keepAlive = this.flags.keepalive || options.keepalive;
async.waterfall([
// find a port for livereload if needed first
function(callback) {
// Inject live reload snippet
if (options.livereload !== false) {
if (options.livereload === true) {
options.livereload = {port: 35729, hostname: options.hostname};
} else if (typeof options.livereload === 'number') {
options.livereload = {port: options.livereload, hostname: options.hostname};
}
middleware.unshift(injectLiveReload(options.livereload));
callback(null);
} else {
callback(null);
}
},
function() {
var app = connect();
var server = null;
var httpsOptions = {
key: options.key || grunt.file.read(path.join(__dirname, 'certs', 'server.key')).toString(),
cert: options.cert || grunt.file.read(path.join(__dirname, 'certs', 'server.crt')).toString(),
ca: options.ca || grunt.file.read(path.join(__dirname, 'certs', 'ca.crt')).toString(),
passphrase: options.passphrase || 'grunt',
secureProtocol: options.secureProtocol || '',
};
middleware.forEach(function (m) {
if (!Array.isArray(m)) {
m = [m];
}
app.use.apply(app, m);
});
if (options.protocol === 'https') {
server = https.createServer(httpsOptions, app);
} else if (options.protocol === 'http2') {
server = http2.createSecureServer(httpsOptions, app);
} else {
server = http.createServer(app);
}
// Call any onCreateServer functions that are present
if (options.onCreateServer) {
options.onCreateServer.forEach(function(func) {
func.call(null, server, connect, options);
});
}
function findUnusedPort(port, maxPort, hostname, callback) {
if (hostname === '::') {
hostname = '127.0.0.1';
}
if (port === 0) {
async.nextTick(function() {
callback(null, 0);
});
} else {
portscanner.findAPortNotInUse(port, maxPort, hostname, callback);
}
}
findUnusedPort(options.port, options.port + MAX_PORTS, options.hostname, function(error, foundPort) {
if (error) {
grunt.log.writeln('Failed to find unused port: ' + error);
}
// if the found port doesn't match the option port, and we are forced to use the option port
if (options.port !== foundPort && options.useAvailablePort === false) {
grunt.fatal('Port ' + options.port + ' is already in use by another process.');
}
server
.listen(foundPort, options.hostname)
.on('listening', function() {
var port = foundPort;
var scheme = options.protocol === 'http2' ? 'https' : options.protocol;
var hostname = options.hostname;
var targetHostname = hostname === '::' ? 'localhost' : hostname;
var target = scheme + '://' + targetHostname + ':' + port;
grunt.log.writeln('Started connect web server on ' + target);
grunt.config.set('connect.' + taskTarget + '.options.hostname', hostname);
grunt.config.set('connect.' + taskTarget + '.options.port', port);
grunt.event.emit('connect.' + taskTarget + '.listening', hostname, port);
if (options.open === true) {
open(target);
} else if (typeof options.open === 'object') {
options.open.target = options.open.target || target;
options.open.appName = options.open.appName || null;
options.open.callback = options.open.callback || function() {};
open(options.open.target, { app: options.open.appName })
.then(options.open.callback, grunt.fatal);
} else if (typeof options.open === 'string') {
open(options.open);
}
if (!keepAlive) {
done();
}
})
.on('error', function(err) {
if (err.code === 'EADDRINUSE') {
grunt.fatal('Port ' + foundPort + ' is already in use by another process.');
} else {
grunt.fatal(err);
}
});
});
// So many people expect this task to keep alive that I'm adding an option
// for it. Running the task explicitly as grunt:keepalive will override any
// value stored in the config. Have fun, people.
if (keepAlive) {
// This is now an async task. Since we don't call the "done"
// function, this task will never, ever, ever terminate. Have fun!
grunt.log.write('Waiting forever...\n');
}
}
]);
});
};