From f56ba662a978956456bc7af6f7f5a133d9e6d81d Mon Sep 17 00:00:00 2001 From: Josef Bredreck Date: Sun, 12 Apr 2020 23:59:10 +0200 Subject: [PATCH 1/2] Update server to support asset change detection Also did a refactoring on the server to match eslint config. --- packages/core/src/lib/server.js | 24 +++ packages/live-server/index.js | 276 +++++++++++++++++--------------- 2 files changed, 173 insertions(+), 127 deletions(-) diff --git a/packages/core/src/lib/server.js b/packages/core/src/lib/server.js index f5101c5a4..c55e2c4ab 100644 --- a/packages/core/src/lib/server.js +++ b/packages/core/src/lib/server.js @@ -36,6 +36,30 @@ const server = patternlab => { patternlab.config.paths.public.root ) ); + defaults.assets = [ + path.resolve( + path.join( + process.cwd(), + patternlab.config.paths.source.js, + '**', + '*.js' // prevent preprocessors like typescript from reloading + ) + ), + path.resolve( + path.join(process.cwd(), patternlab.config.paths.source.images) + ), + path.resolve( + path.join(process.cwd(), patternlab.config.paths.source.fonts) + ), + path.resolve( + path.join( + process.cwd(), + patternlab.config.paths.source.css, + '**', + '*.css' // prevent preprocessors from reloading + ) + ), + ]; // allow for overrides should they exist inside patternlab-config.json const liveServerConfig = Object.assign( diff --git a/packages/live-server/index.js b/packages/live-server/index.js index e8248d0a3..e372fac33 100644 --- a/packages/live-server/index.js +++ b/packages/live-server/index.js @@ -1,25 +1,26 @@ #!/usr/bin/env node -var fs = require('fs'), - connect = require('connect'), - serveIndex = require('serve-index'), - logger = require('morgan'), - WebSocket = require('faye-websocket'), - path = require('path'), - url = require('url'), - http = require('http'), - send = require('send'), - open = require('opn'), - es = require('event-stream'), - os = require('os'), - chokidar = require('chokidar'); +const fs = require('fs'); +const connect = require('connect'); +const serveIndex = require('serve-index'); +const logger = require('morgan'); +const WebSocket = require('faye-websocket'); +const path = require('path'); +const url = require('url'); +const http = require('http'); +const send = require('send'); +const open = require('opn'); +const es = require('event-stream'); +const os = require('os'); +const chokidar = require('chokidar'); + require('colors'); -var INJECTED_CODE = fs.readFileSync( +const INJECTED_CODE = fs.readFileSync( path.join(__dirname, 'injected.html'), 'utf8' ); -var LiveServer = { +const LiveServer = { server: null, watcher: null, logLevel: 2, @@ -35,7 +36,7 @@ function escape(html) { // Based on connect.static(), but streamlined and with added code injecter function staticServer(root) { - var isFile = false; + let isFile = false; try { // For supporting mounting files instead of just directories isFile = fs.statSync(root).isFile(); @@ -44,38 +45,49 @@ function staticServer(root) { } return function(req, res, next) { if (req.method !== 'GET' && req.method !== 'HEAD') return next(); - var reqpath = isFile ? '' : url.parse(req.url).pathname; - var hasNoOrigin = !req.headers.origin; - var injectCandidates = [ + + const reqpath = isFile ? '' : url.parse(req.url).pathname; + const hasNoOrigin = !req.headers.origin; + const injectCandidates = [ new RegExp('', 'i'), new RegExp('', 'g'), new RegExp('', 'i'), ]; - var injectTag = null; - var injectCount = 0; + + let injectTag = null; + let injectCount = 0; function directory() { - var pathname = url.parse(req.originalUrl).pathname; + const pathname = url.parse(req.originalUrl).pathname; res.statusCode = 301; res.setHeader('Location', pathname + '/'); res.end('Redirecting to ' + escape(pathname) + '/'); } function file(filepath /*, stat*/) { - var x = path.extname(filepath).toLocaleLowerCase(), - matches, - possibleExtensions = ['', '.html', '.htm', '.xhtml', '.php', '.svg']; + const x = path.extname(filepath).toLocaleLowerCase(); + const possibleExtensions = [ + '', + '.html', + '.htm', + '.xhtml', + '.php', + '.svg', + ]; + + let matches; if (hasNoOrigin && possibleExtensions.indexOf(x) > -1) { // TODO: Sync file read here is not nice, but we need to determine if the html should be injected or not - var contents = fs.readFileSync(filepath, 'utf8'); - for (var i = 0; i < injectCandidates.length; ++i) { + const contents = fs.readFileSync(filepath, 'utf8'); + for (let i = 0; i < injectCandidates.length; ++i) { matches = contents.match(injectCandidates[i]); - injectCount = matches && matches.length || 0; + injectCount = (matches && matches.length) || 0; if (injectCount) { injectTag = matches[0]; break; } } + if (injectTag === null && LiveServer.logLevel >= 3) { console.warn( 'Failed to inject refresh script!'.yellow, @@ -90,15 +102,17 @@ function staticServer(root) { function error(err) { if (err.status === 404) return next(); - next(err); + return next(err); } function inject(stream) { if (injectTag) { // We need to modify the length given to browser - var len = INJECTED_CODE.length * injectCount + res.getHeader('Content-Length'); + const len = + INJECTED_CODE.length * injectCount + res.getHeader('Content-Length'); res.setHeader('Content-Length', len); - var originalPipe = stream.pipe; + + const originalPipe = stream.pipe; stream.pipe = function(resp) { originalPipe .call( @@ -110,7 +124,7 @@ function staticServer(root) { } } - send(req, reqpath, { root: root }) + return send(req, reqpath, { root: root }) .on('error', error) .on('directory', directory) .on('file', file) @@ -152,41 +166,44 @@ function entryPoint(staticHandler, file) { * @param wait {number} Server will wait for all changes, before reloading * @param htpasswd {string} Path to htpasswd file to enable HTTP Basic authentication * @param middleware {array} Append middleware to stack, e.g. [function(req, res, next) { next(); }]. + * @param assets {String[]} path of asset directories to watch */ LiveServer.start = function(options) { - options = options || {}; - var host = options.host || '0.0.0.0'; - var port = options.port !== undefined ? options.port : 8080; // 0 means random - var root = options.root || process.cwd(); - var mount = options.mount || []; - var watchPaths = options.watch || [root]; + const host = options.host || '0.0.0.0'; + const port = options.port !== undefined ? options.port : 8080; // 0 means random + const root = options.root || process.cwd(); + const mount = options.mount || []; + const watchPaths = options.watch || [root, ...options.assets]; LiveServer.logLevel = options.logLevel === undefined ? 2 : options.logLevel; - var openPath = + + let openPath = options.open === undefined || options.open === true ? '' - : options.open === null || options.open === false ? null : options.open; + : options.open === null || options.open === false + ? null + : options.open; if (options.noBrowser) openPath = null; // Backwards compatibility with 0.7.0 - var file = options.file; - var staticServerHandler = staticServer(root); - var wait = options.wait === undefined ? 100 : options.wait; - var browser = options.browser || null; - var htpasswd = options.htpasswd || null; - var cors = options.cors || false; - var https = options.https || null; - var proxy = options.proxy || []; - var middleware = options.middleware || []; - var noCssInject = options.noCssInject; - var httpsModule = options.httpsModule; + + const file = options.file; + const staticServerHandler = staticServer(root); + const wait = options.wait === undefined ? 100 : options.wait; + const browser = options.browser || null; + const htpasswd = options.htpasswd || null; + const cors = options.cors || false; + const https = options.https || null; + const proxy = options.proxy || []; + const middleware = options.middleware || []; + const noCssInject = options.noCssInject; + let httpsModule = options.httpsModule; if (httpsModule) { try { require.resolve(httpsModule); } catch (e) { console.error( - ('HTTPS module "' + httpsModule + '" you\'ve provided was not found.') - .red + `HTTPS module "${httpsModule}" you've provided was not found.`.red ); - console.error('Did you do', '"npm install ' + httpsModule + '"?'); + console.error('Did you do', `"npm install ${httpsModule}"?`); return; } } else { @@ -194,7 +211,7 @@ LiveServer.start = function(options) { } // Setup a web server - var app = connect(); + const app = connect(); // Add logger. Level 2 logs only errors if (LiveServer.logLevel === 2) { @@ -209,31 +226,34 @@ LiveServer.start = function(options) { } else if (LiveServer.logLevel > 2) { app.use(logger('dev')); } + if (options.spa) { middleware.push('spa'); } + // Add middleware - middleware.map(function(mw) { + middleware.map(mw => { + let mwm = mw; if (typeof mw === 'string') { - var ext = path.extname(mw).toLocaleLowerCase(); - if (ext !== '.js') { - mw = require(path.join(__dirname, 'middleware', mw + '.js')); + if (path.extname(mw).toLocaleLowerCase() !== '.js') { + mwm = require(path.join(__dirname, 'middleware', mw + '.js')); } else { - mw = require(mw); + mwm = require(mw); } } - app.use(mw); + app.use(mwm); }); // Use http-auth if configured if (htpasswd !== null) { - var auth = require('http-auth'); - var basic = auth.basic({ + const auth = require('http-auth'); + const basic = auth.basic({ realm: 'Please authorize', file: htpasswd, }); app.use(auth.connect(basic)); } + if (cors) { app.use( require('cors')({ @@ -242,34 +262,43 @@ LiveServer.start = function(options) { }) ); } - mount.forEach(function(mountRule) { - var mountPath = path.resolve(process.cwd(), mountRule[1]); - if (!options.watch) + + mount.forEach(mountRule => { + const mountPath = path.resolve(process.cwd(), mountRule[1]); + if (!options.watch) { // Auto add mount paths to wathing but only if exclusive path option is not given watchPaths.push(mountPath); + } + app.use(mountRule[0], staticServer(mountPath)); - if (LiveServer.logLevel >= 1) + if (LiveServer.logLevel >= 1) { console.log('Mapping %s to "%s"', mountRule[0], mountPath); + } }); - proxy.forEach(function(proxyRule) { - var proxyOpts = url.parse(proxyRule[1]); + + proxy.forEach(proxyRule => { + const proxyOpts = url.parse(proxyRule[1]); proxyOpts.via = true; proxyOpts.preserveHost = true; app.use(proxyRule[0], require('proxy-middleware')(proxyOpts)); - if (LiveServer.logLevel >= 1) + + if (LiveServer.logLevel >= 1) { console.log('Mapping %s to "%s"', proxyRule[0], proxyRule[1]); + } }); + app .use(staticServerHandler) // Custom static server .use(entryPoint(staticServerHandler, file)) .use(serveIndex(root, { icons: true })); - var server, protocol; + let server, protocol; if (https !== null) { - var httpsConfig = https; + let httpsConfig = https; if (typeof https === 'string') { httpsConfig = require(path.resolve(process.cwd(), https)); } + server = require(httpsModule).createServer(httpsConfig, app); protocol = 'https'; } else { @@ -280,10 +309,9 @@ LiveServer.start = function(options) { // Handle server startup errors server.addListener('error', function(e) { if (e.code === 'EADDRINUSE') { - var serveURL = protocol + '://' + host + ':' + port; console.log( '%s is already in use. Trying another port.'.yellow, - serveURL + `${protocol}://${host}:${port}` ); setTimeout(function() { server.listen(0, host); @@ -298,35 +326,27 @@ LiveServer.start = function(options) { server.addListener('listening', function(/*e*/) { LiveServer.server = server; - var address = server.address(); - var serveHost = + const address = server.address(); + const serveHost = address.address === '0.0.0.0' ? '127.0.0.1' : address.address; - var openHost = host === '0.0.0.0' ? '127.0.0.1' : host; + const openHost = host === '0.0.0.0' ? '127.0.0.1' : host; - var serveURL = protocol + '://' + serveHost + ':' + address.port; - var openURL = protocol + '://' + openHost + ':' + address.port; + const serveURL = `${protocol}://${serveHost}:${address.port}`; + const openURL = `${protocol}://${openHost}:${address.port}`; - var serveURLs = [serveURL]; + let serveURLs = [serveURL]; if (LiveServer.logLevel > 2 && address.address === '0.0.0.0') { - var ifaces = os.networkInterfaces(); + const ifaces = os.networkInterfaces(); serveURLs = Object.keys(ifaces) - .map(function(iface) { - return ifaces[iface]; - }) + .map(iface => ifaces[iface]) // flatten address data, use only IPv4 - .reduce(function(data, addresses) { + .reduce((data, addresses) => { addresses - .filter(function(addr) { - return addr.family === 'IPv4'; - }) - .forEach(function(addr) { - data.push(addr); - }); + .filter(addr => addr.family === 'IPv4') + .forEach(addr => data.push(addr)); return data; }, []) - .map(function(addr) { - return protocol + '://' + addr.address + ':' + address.port; - }); + .map(addr => `${protocol}://${addr.address}:${address.port}`); } // Output @@ -348,9 +368,7 @@ LiveServer.start = function(options) { // Launch browser if (openPath !== null) if (typeof openPath === 'object') { - openPath.forEach(function(p) { - open(openURL + p, { app: browser }); - }); + openPath.forEach(p => open(openURL + p, { app: browser })); } else { open(openURL + openPath, { app: browser }); } @@ -360,19 +378,19 @@ LiveServer.start = function(options) { server.listen(port, host); // WebSocket - var clients = []; + let clients = []; server.addListener('upgrade', function(request, socket, head) { - var ws = new WebSocket(request, socket, head); + const ws = new WebSocket(request, socket, head); ws.onopen = function() { ws.send('connected'); }; if (wait > 0) { (function() { - var wssend = ws.send; - var waitTimeout; + const wssend = ws.send; + let waitTimeout; ws.send = function() { - var args = arguments; + const args = arguments; if (waitTimeout) clearTimeout(waitTimeout); waitTimeout = setTimeout(function() { wssend.apply(ws, args); @@ -382,15 +400,13 @@ LiveServer.start = function(options) { } ws.onclose = function() { - clients = clients.filter(function(x) { - return x !== ws; - }); + clients = clients.filter(x => x !== ws); }; clients.push(ws); }); - var ignored = [ + let ignored = [ function(testPath) { // Always ignore dotfiles (important e.g. because editor hidden temp files) return ( @@ -398,27 +414,39 @@ LiveServer.start = function(options) { ); }, ]; + if (options.ignore) { ignored = ignored.concat(options.ignore); } + if (options.ignorePattern) { ignored.push(options.ignorePattern); } + // Setup file watcher - LiveServer.watcher = chokidar.watch(watchPaths, { - ignored: ignored, - ignoreInitial: true, - }); + LiveServer.watcher = chokidar.watch( + // Replace backslashes with slashes, because chokidar pattern + // like path/**/*.xyz only acceps linux file path + watchPaths.map(p => p.replace(/\\/g, '/')), + { + ignored: ignored, + ignoreInitial: true, + awaitWriteFinish: true, + } + ); + function handleChange(changePath) { - var cssChange = path.extname(changePath) === '.css' && !noCssInject; + const cssChange = path.extname(changePath) === '.css' && !noCssInject; if (LiveServer.logLevel >= 1) { if (cssChange) console.log('CSS change detected'.magenta, changePath); else console.log('Change detected'.cyan, changePath); } - clients.forEach(function(ws) { + + clients.forEach(ws => { if (ws) ws.send(cssChange ? 'refreshcss' : 'reload'); }); } + LiveServer.watcher .on('change', handleChange) .on('add', handleChange) @@ -434,34 +462,28 @@ LiveServer.start = function(options) { LiveServer.refreshCSS = function() { if (clients.length) { - clients.forEach(function(ws) { - if (ws) { - ws.send('refreshcss'); - } + clients.forEach(ws => { + if (ws) ws.send('refreshcss'); }); } }; LiveServer.reload = function() { if (clients.length) { - clients.forEach(function(ws) { - if (ws) { - ws.send('reload'); - } + clients.forEach(ws => { + if (ws) ws.send('reload'); }); } }; - - return server; }; LiveServer.shutdown = function() { - var watcher = LiveServer.watcher; - if (watcher) { - watcher.close(); + if (LiveServer.watcher) { + LiveServer.watcher.close(); + } + if (LiveServer.server) { + LiveServer.server.close(); } - var server = LiveServer.server; - if (server) server.close(); }; module.exports = LiveServer; From 8c7cf5ecc2036e3b6486709e0807489b08c5c499 Mon Sep 17 00:00:00 2001 From: Josef Bredreck Date: Mon, 13 Apr 2020 14:58:54 +0200 Subject: [PATCH 2/2] Add watch property for gulp watch task --- packages/edition-node-gulp/gulpfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/edition-node-gulp/gulpfile.js b/packages/edition-node-gulp/gulpfile.js index e1947e4fd..62ddc007e 100644 --- a/packages/edition-node-gulp/gulpfile.js +++ b/packages/edition-node-gulp/gulpfile.js @@ -27,6 +27,7 @@ function serve() { return patternlab.server .serve({ cleanPublic: config.cleanPublic, + watch: true, }) .then(() => { // do something else when this promise resolves