├── .npmignore ├── test ├── fixtures │ ├── simplest │ │ ├── test.txt │ │ └── index.html │ └── multiple │ │ ├── app │ │ └── index.html │ │ └── dist │ │ └── dist.html ├── test_node20.mjs └── test.js ├── jetbrains.png ├── .gitignore ├── .travis.yml ├── examples ├── multiple │ ├── dist │ │ └── dist.html │ ├── app │ │ ├── stylus │ │ │ └── main.styl │ │ └── index.html │ └── gulpfile.js ├── simplest │ ├── app │ │ └── index.html │ └── gulpfile.js └── livereload │ ├── app │ ├── stylus │ │ └── main.styl │ └── index.html │ ├── path │ └── path.html │ └── gulpfile.js ├── .github └── workflows │ └── github-actions-node.yml ├── bin └── make_certs.sh ├── certs ├── server.crt └── server.key ├── LICENSE ├── package.json ├── README.md └── src └── index.coffee /.npmignore: -------------------------------------------------------------------------------- 1 | examples -------------------------------------------------------------------------------- /test/fixtures/simplest/test.txt: -------------------------------------------------------------------------------- 1 | Hello world -------------------------------------------------------------------------------- /test/fixtures/multiple/app/index.html: -------------------------------------------------------------------------------- 1 | app test -------------------------------------------------------------------------------- /test/fixtures/multiple/dist/dist.html: -------------------------------------------------------------------------------- 1 | dist test -------------------------------------------------------------------------------- /test/fixtures/simplest/index.html: -------------------------------------------------------------------------------- 1 | index page 2 | -------------------------------------------------------------------------------- /jetbrains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avevlad/gulp-connect/HEAD/jetbrains.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | node_modules 4 | index.js 5 | *.css 6 | *.iml 7 | *.tgz -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "lts/*" 6 | - "stable" 7 | -------------------------------------------------------------------------------- /examples/multiple/dist/dist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
11 |
12 |
13 | ## Install
14 |
15 | ```
16 | npm install --save-dev gulp-connect
17 | ```
18 |
19 | ## Usage
20 |
21 | ```js
22 | var gulp = require('gulp');
23 | var connect = require('gulp-connect');
24 |
25 | gulp.task('connect', function() {
26 | connect.server();
27 | });
28 |
29 | gulp.task('default', ['connect']);
30 | ```
31 |
32 | #### LiveReload
33 | ```js
34 | var gulp = require('gulp');
35 | var connect = require('gulp-connect');
36 |
37 | gulp.task('connect', function() {
38 | connect.server({
39 | root: 'app',
40 | livereload: true
41 | });
42 | });
43 |
44 | gulp.task('html', function () {
45 | gulp.src('./app/*.html')
46 | .pipe(gulp.dest('./app'))
47 | .pipe(connect.reload());
48 | });
49 |
50 | gulp.task('watch', function () {
51 | gulp.watch(['./app/*.html'], ['html']);
52 | });
53 |
54 | gulp.task('default', ['connect', 'watch']);
55 | ```
56 |
57 |
58 | #### Start and stop server
59 |
60 | ```js
61 | gulp.task('jenkins-tests', function() {
62 | connect.server({
63 | port: 8888
64 | });
65 | // run some headless tests with phantomjs
66 | // when process exits:
67 | connect.serverClose();
68 | });
69 | ```
70 |
71 |
72 | #### Multiple server
73 |
74 | ```js
75 | var gulp = require('gulp');
76 | var connect = require('gulp-connect');
77 | var stylus = require('gulp-stylus');
78 |
79 | gulp.task('connectDev', function () {
80 | connect.server({
81 | name: 'Dev App',
82 | root: ['app', 'tmp'],
83 | port: 8000,
84 | livereload: true
85 | });
86 | });
87 |
88 | gulp.task('connectDist', function () {
89 | connect.server({
90 | name: 'Dist App',
91 | root: 'dist',
92 | port: 8001,
93 | livereload: true
94 | });
95 | });
96 |
97 | gulp.task('html', function () {
98 | gulp.src('./app/*.html')
99 | .pipe(gulp.dest('./app'))
100 | .pipe(connect.reload());
101 | });
102 |
103 | gulp.task('stylus', function () {
104 | gulp.src('./app/stylus/*.styl')
105 | .pipe(stylus())
106 | .pipe(gulp.dest('./app/css'))
107 | .pipe(connect.reload());
108 | });
109 |
110 | gulp.task('watch', function () {
111 | gulp.watch(['./app/*.html'], ['html']);
112 | gulp.watch(['./app/stylus/*.styl'], ['stylus']);
113 | });
114 |
115 | gulp.task('default', ['connectDist', 'connectDev', 'watch']);
116 | ```
117 |
118 | #### http2 support
119 |
120 | If the [http2](https://www.npmjs.com/package/http2) package is installed and you use an https connection to gulp connect then http 2 will be used in preference to http 1.
121 |
122 | ## API
123 |
124 | #### options.root
125 |
126 | Type: `Array or String`
127 | Default: `Directory with gulpfile`
128 |
129 | The root path
130 |
131 | #### options.port
132 |
133 | Type: `Number`
134 | Default: `8080`
135 |
136 | The connect webserver port
137 |
138 | #### options.host
139 |
140 | Type: `String`
141 | Default: `localhost`
142 |
143 | #### options.name
144 |
145 | Type: `String`
146 | Default: `Server`
147 |
148 | The name that will be output when the server starts/stops.
149 |
150 | #### options.https
151 |
152 | Type: `Object`
153 | Default: `false`
154 |
155 | Can be any options documented at https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
156 |
157 | When https is just set to `true` (boolean), then internally some defaults will be used.
158 |
159 | #### options.livereload
160 |
161 | Type: `Object or Boolean`
162 | Default: `false`
163 |
164 | #### options.livereload.port
165 |
166 | Type: `Number`
167 | Default: `35729`
168 |
169 | Overrides the hostname of the script livereload injects in index.html
170 |
171 | #### options.livereload.hostname
172 |
173 | Type: `String`
174 | Default: 'undefined'
175 |
176 | #### options.fallback
177 |
178 | Type: `String`
179 | Default: `undefined`
180 |
181 | Fallback file (e.g. `index.html`)
182 |
183 | #### options.middleware
184 |
185 | Type: `Function`
186 | Default: `[]`
187 |
188 | #### options.debug
189 |
190 | Type: `Boolean`
191 | Default: `false`
192 |
193 | #### options.index
194 |
195 | Type: `Boolean or String of a new index pass or Array of new indexes in preferred order`
196 | Default: `true`
197 |
198 | ```js
199 | gulp.task('connect', function() {
200 | connect.server({
201 | root: "app",
202 | middleware: function(connect, opt) {
203 | return [
204 | // ...
205 | ]
206 | }
207 | });
208 | });
209 | ```
210 |
211 | ## Contributing
212 |
213 | To contribute to this project, you must have CoffeeScript installed: `npm install -g coffee-script`.
214 |
215 | Then, to build the `index.js` file, run `coffee -o . -bc src/`. Run `npm test` to run the tests.
216 |
217 | ## Contributors
218 |
219 | * [AveVlad](https://github.com/AveVlad)
220 | * [schickling](https://github.com/schickling)
221 | * [justinmchase](https://github.com/justinmchase)
222 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | var request = require('supertest');
2 | var connect = require('../index');
3 | require('mocha');
4 |
5 | var portCounter = 35000;
6 | describe('gulp-connect', function () {
7 | describe('Simple', function() {
8 | after(function() {
9 | connect.serverClose();
10 | });
11 | it('Explicit /test.txt', function (done) {
12 | var port = portCounter++;
13 | connect.server({
14 | port: port
15 | }, function() {
16 | request('http://localhost:' + port)
17 | .get('/fixtures/simplest/test.txt')
18 | .expect(/Hello world/)
19 | .expect(200)
20 | .end(function (err, res) {
21 | done(err);
22 | });
23 | });
24 | });
25 | it('Implicit /index.html', function (done) {
26 | var port = portCounter++;
27 | connect.server({
28 | port: port
29 | }, function() {
30 | request('http://localhost:' + port)
31 | .get('/fixtures/simplest/')
32 | .expect(/index page/)
33 | .expect(200)
34 | .end(function (err, res) {
35 | done(err);
36 | });
37 | });
38 | })
39 | })
40 | });
41 | describe('Self Start / Stop', function() {
42 | after(function() {
43 | connect.serverClose();
44 | });
45 | it('Root string', function (done) {
46 | var port = portCounter++;
47 | connect.server({
48 | port: port,
49 | root: __dirname + "/fixtures"
50 | });
51 | request('http://localhost:' + port)
52 | .get('/multiple/app/index.html')
53 | .expect(/app test/)
54 | .end(function (err, res) {
55 | connect.serverClose();
56 | if (err) return done(err);
57 | done()
58 | });
59 | });
60 | it('Root array', function (done) {
61 | var port = portCounter++;
62 | connect.server({
63 | port: port,
64 | root: [__dirname + "/fixtures/multiple/app", __dirname + "/fixtures/multiple/dist"]
65 | });
66 | request('http://localhost:' + port)
67 | .get('/index.html')
68 | .expect(/app test/)
69 | .expect(200)
70 | .end(function (err) {
71 | if (err) return done(err);
72 | });
73 | request('http://localhost:' + port)
74 | .get('/dist.html')
75 | .expect(/dist test/)
76 | .expect(200)
77 | .end(function (err) {
78 | connect.serverClose();
79 | if (err) return done(err);
80 | done()
81 | });
82 | });
83 | it('Port test', function (done) {
84 | connect.server({
85 | root: __dirname + "/fixtures/multiple/app",
86 | port: 3333
87 | });
88 | request('http://localhost:3333')
89 | .get('/index.html')
90 | .expect(/app test/)
91 | .end(function (err) {
92 | connect.serverClose();
93 | if (err) return done(err);
94 | done()
95 | });
96 | });
97 | it('Https test', function (done) {
98 | //suppress invalid self-signed ssl certificate error
99 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"
100 | var port = portCounter++;
101 | connect.server({
102 | port: port,
103 | root: __dirname + "/fixtures/multiple/app",
104 | https: true
105 | });
106 | request('https://localhost:' + port)
107 | .get('/index.html')
108 | .expect(/app test/)
109 | .end(function (err) {
110 | connect.serverClose();
111 | if (err) return done(err);
112 | done()
113 | });
114 | });
115 | it('Livereload test', function (done) {
116 | var port = portCounter++;
117 | connect.server({
118 | port: port,
119 | livereload: true
120 | }, function() {
121 | request('http://localhost:35729')
122 | .get('/')
123 | .expect('Content-Type', /json/)
124 | .end(function (err) {
125 | if (err) return done(err);
126 | request('http://localhost:35729')
127 | .get('/livereload.js')
128 | .expect(200)
129 | .end(function (err) {
130 | connect.serverClose();
131 | if (err) return done(err);
132 | done();
133 | });
134 | });
135 | });
136 | });
137 | it('Livereload https test', function (done) {
138 | var port = portCounter++;
139 | connect.server({
140 | port: port,
141 | livereload: true,
142 | https: true
143 | }, function() {
144 | request('https://localhost:35729')
145 | .get('/')
146 | .expect('Content-Type', /json/)
147 | .end(function (err) {
148 | if (err) return done(err);
149 | request('https://localhost:35729')
150 | .get('/livereload.js')
151 | .expect(200)
152 | .end(function (err) {
153 | connect.serverClose();
154 | if (err) return done(err);
155 | done();
156 | });
157 | });
158 | });
159 | });
160 | it('Livereload port', function (done) {
161 | var port = portCounter++;
162 | var liveReloadPort = portCounter++;
163 | connect.server({
164 | port: port,
165 | livereload: {
166 | port: liveReloadPort
167 | }
168 | },
169 | function() {
170 | request('http://localhost:' + liveReloadPort)
171 | .get('/')
172 | .expect('Content-Type', /json/)
173 | .end(function (err) {
174 | connect.serverClose();
175 | if (err) return done(err);
176 | done();
177 | });
178 | });
179 | });
180 | it('livereload closes', function (done) {
181 | this.timeout(10000);
182 | var port = portCounter++;
183 | var liveReloadPort = portCounter++;
184 | connect.server({
185 | port: port,
186 | livereload: {
187 | port: liveReloadPort
188 | }
189 | },
190 | function() {
191 | request('http://localhost:' + liveReloadPort)
192 | .get('/')
193 | .expect('Content-Type', /json/)
194 | .expect(200)
195 | .end(function (err) {
196 | if (err) return done(err);
197 | connect.serverClose();
198 | setTimeout(function() {
199 | request('http://localhost:' + liveReloadPort)
200 | .get('/')
201 | .end(function (err) {
202 | if (err) return done();
203 | done(new Error("Live reload is still running."));
204 | })}, 100);
205 | })
206 | });
207 | });
208 | it('Fallback test', function (done) {
209 | var port = portCounter++;
210 | connect.server({
211 | port: port,
212 | fallback: __dirname + '/fixtures/simplest/index.html'
213 | }, function() {
214 | request('http://localhost:' + port)
215 | .get('/not/existing/path')
216 | .expect(/index page/)
217 | .expect('Content-Type', new RegExp('text/html; charset=UTF-8'))
218 | .expect(200)
219 | .end(function (err, res) {
220 | connect.serverClose();
221 | if (err) return done(err);
222 | done()
223 | });
224 | });
225 | })
226 | });
227 |
--------------------------------------------------------------------------------
/src/index.coffee:
--------------------------------------------------------------------------------
1 | path = require("path")
2 | fancyLog = require("fancy-log")
3 | colors = require("ansi-colors")
4 | mapStream = require("map-stream")
5 | http = require("http")
6 | https = require("https")
7 | fs = require("fs")
8 | connect = require("connect")
9 | serveStatic = require('serve-static')
10 | serveIndex = require('serve-index')
11 | liveReload = require("connect-livereload")
12 | send = require("send")
13 | tiny_lr = require("tiny-lr")
14 | apps = []
15 |
16 | class ConnectApp
17 | constructor: (options, startedCallback) ->
18 | @name = options.name || "Server"
19 | @port = options.port || "8080"
20 | @root = options.root || path.dirname(module?.parent?.id || ".")
21 | @host = options.host || "localhost"
22 | @debug = options.debug || false
23 | @silent = options.silent || false
24 | @https = options.https || false
25 | @preferHttp1 = options.preferHttp1 || false;
26 | @livereload = options.livereload || false
27 | @middleware = options.middleware || undefined
28 | @startedCallback = startedCallback || () -> {};
29 | @serverInit = options.serverInit || undefined
30 | @fallback = options.fallback || undefined
31 | @index = options.index
32 | @oldMethod("open") if options.open
33 | @sockets = []
34 | @app = undefined
35 | @lr = undefined
36 | @state = "initializing"
37 | @run()
38 |
39 | run: ->
40 | if @state == "stopped"
41 | return
42 |
43 | @state = "starting"
44 | @log "Starting server..."
45 | @app = connect()
46 |
47 | @handlers().forEach (middleware) =>
48 | if typeof (middleware) is "object"
49 | @app.use middleware[0], middleware[1]
50 | else
51 | @app.use middleware
52 |
53 | @app.use serveIndex(if typeof @root == "object" then @root[0] else @root)
54 |
55 | if @https
56 |
57 | # use some defaults when not set. do not touch when a key is already specified
58 | # see https://github.com/AveVlad/gulp-connect/issues/172
59 | if typeof (@https) is 'boolean' || (!@https.key && !@https.pfx)
60 |
61 | # change it into an object if it is not already one
62 | if !(typeof (@https) is "object")
63 | @https = {}
64 |
65 | @https.key = fs.readFileSync __dirname + '/certs/server.key'
66 | @https.cert = fs.readFileSync __dirname + '/certs/server.crt'
67 | @https.ca = fs.readFileSync __dirname + '/certs/server.crt'
68 | @https.passphrase = 'gulp'
69 |
70 | http2 = undefined
71 | if !@preferHttp1
72 | try
73 | http2 = require('http2')
74 |
75 | if http2
76 | @https.allowHTTP1 = true
77 | @server = http2.createSecureServer(@https, @app)
78 | else
79 | @server = https.createServer(@https, @app)
80 |
81 | else
82 | @server = http.createServer @app
83 | if @serverInit
84 | @serverInit @server
85 | @server.listen @port, @host, (err) =>
86 | if err
87 | @log "Error on starting server: #{err}"
88 | else
89 | @log "#{@name} started http#{if @https then 's' else ''}://#{if @host == '0.0.0.0' then 'localhost' else @host}:#{@port}"
90 |
91 | stoped = false
92 | sockets = []
93 |
94 | @server.on "close", =>
95 | if (!stoped)
96 | stoped = true
97 | @log "#{@name} stopped"
98 |
99 | # Log connections and request in debug
100 | @server.on "connection", (socket) =>
101 | @logDebug "Received incoming connection from #{socket.address().address}"
102 | @sockets.push socket
103 | socket.on "close", =>
104 | @sockets.splice @sockets.indexOf(socket), 1
105 |
106 | @server.on "request", (request, response) =>
107 | @logDebug "Received request #{request.method} #{request.url}"
108 |
109 | @server.on "error", (err) =>
110 | @log err.toString()
111 |
112 | stopServer = =>
113 | if (!stoped)
114 | @sockets.forEach (socket) =>
115 | socket.destroy()
116 |
117 | @server.close()
118 | if @livereload
119 | @lr.close()
120 | process.nextTick( ->
121 | process.exit(0)
122 | )
123 |
124 | process.on("SIGINT", stopServer)
125 | process.on("exit", stopServer)
126 |
127 | if @livereload
128 | tiny_lr.Server::error = ->
129 | if @https
130 | @lr = tiny_lr
131 | key: @https.key || fs.readFileSync __dirname + '/certs/server.key'
132 | cert: @https.cert || fs.readFileSync __dirname + '/certs/server.crt'
133 | else
134 | @lr = tiny_lr()
135 |
136 | @lr.listen @livereload.port
137 | @log "LiveReload started on port #{@livereload.port}"
138 | @state = "running";
139 | @log "Running server"
140 | @startedCallback()
141 |
142 | close: ->
143 | if @state == "running"
144 | @log "Stopping server"
145 | if @livereload
146 | @lr.close()
147 | @server.close()
148 | @state = "stopped"
149 | @log "Stopped server"
150 | else if @state == "stopped"
151 | @log "Server has already been stopped."
152 | else
153 | @log "Ignoring stop as server is in " + @state + " state."
154 |
155 | handlers: ->
156 | steps = if @middleware then @middleware.call(this, connect, @) else []
157 | if @livereload
158 | @livereload = {} if typeof @livereload is "boolean"
159 | @livereload.port = 35729 unless @livereload.port
160 | steps.unshift liveReload(@livereload)
161 | if @index is true then @index = "index.html"
162 | if typeof @root == "object"
163 | @root.forEach (path) ->
164 | steps.push serveStatic(path, {index: @index})
165 | else
166 | steps.push serveStatic(@root, {index: @index})
167 | if @fallback
168 | steps.push (req, res) =>
169 | fallbackPath = @fallback
170 |
171 | if typeof @fallback is "function"
172 | fallbackPath = @fallback(req, res)
173 |
174 | send(req, fallbackPath).pipe(res);
175 |
176 | return steps
177 |
178 | log: (text) ->
179 | if !@silent
180 | fancyLog colors.green(text)
181 |
182 | logWarning: (text) ->
183 | if !@silent
184 | fancyLog colors.yellow(text)
185 |
186 | logDebug: (text) ->
187 | if @debug
188 | fancyLog colors.blue(text)
189 |
190 | oldMethod: (type) ->
191 | text = 'does not work in gulp-connect v 2.*. Please read "readme" https://github.com/AveVlad/gulp-connect'
192 | switch type
193 | when "open" then @logWarning("Option open #{text}")
194 |
195 | module.exports =
196 | server: (options = {}, startedCallback = null) ->
197 | app = new ConnectApp(options, startedCallback)
198 | apps.push(app)
199 | app
200 | reload: ->
201 | mapStream (file, callback) ->
202 | apps.forEach (app) =>
203 | if app.livereload and typeof app.lr == "object"
204 | app.lr.changed body:
205 | files: file.path
206 | callback null, file
207 | serverClose: ->
208 | apps.forEach((app) -> do app.close)
209 | apps = []
210 |
--------------------------------------------------------------------------------