diff --git a/packages/cli/package.json b/packages/cli/package.json index 0da0478dc..48b8c0dd4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -11,6 +11,7 @@ "dependencies": { "@pattern-lab/core": "^5.9.0", "@pattern-lab/live-server": "^5.0.0", + "@pattern-lab/starterkit-mustache-base": "3.0.3", "archiver": "2.1.1", "chalk": "2.4.1", "commander": "2.15.1", @@ -23,7 +24,11 @@ "lodash": "4.17.15", "ora": "2.1.0", "path-exists": "3.0.0", - "sanitize-filename": "1.6.1" + "sanitize-filename": "1.6.1", + "starterkit-mustache-acidtest": "0.0.3", + "starterkit-mustache-bootstrap": "0.1.1", + "starterkit-mustache-foundation": "0.1.1", + "starterkit-mustache-materialdesign": "0.1.2" }, "devDependencies": { "eslint": "4.18.2", @@ -44,7 +49,7 @@ ], "scripts": { "lint": "eslint ./{bin,test}", - "test:separate": "tap './test/*.test.js' --reporter spec --timeout=120" + "test:separate": "tap ./test/*.test.js --reporter spec --timeout=120" }, "repository": "https://github.com/pattern-lab/patternlab-node/tree/master/packages/cli", "bugs": "https://github.com/pattern-lab/patternlab-node/issues", 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/docs/src/docs/advanced-config-options.md b/packages/docs/src/docs/advanced-config-options.md index 5db225d05..bc1258050 100644 --- a/packages/docs/src/docs/advanced-config-options.md +++ b/packages/docs/src/docs/advanced-config-options.md @@ -27,7 +27,7 @@ Sets whether or not to delete `public.patterns/` upon each build of Pattern Lab. **default**: `true` -### defaultPatttern +### defaultPattern Sets a specific pattern upon launch of the styleguide. This pattern will not be available in the navigation, or in view all pages. The only way to get to it will be via a refresh. Set it using the [short-hand pattern-include syntax](http://localhost:4000/docs/pattern-including.html): 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 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;