diff --git a/.changeset/ten-ligers-develop.md b/.changeset/ten-ligers-develop.md new file mode 100644 index 000000000..5aac56118 --- /dev/null +++ b/.changeset/ten-ligers-develop.md @@ -0,0 +1,5 @@ +--- +'preact-cli': minor +--- + +This change allows a user to disable the Webpack dev server while using the CLI's 'watch' mode. Useful for developing apps for SSR as this is much quicker without the production optimizations and will auto-run on changes to the source. diff --git a/README.md b/README.md index 1261fa82f..5f7cb9764 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ Spin up a development server with multiple features like `hot-module-replacement $ preact watch --src Specify source directory (default src) + --dest Specify output directory if dev server is disabled (default build) --cwd A directory to use instead of $PWD (default .) --esm Builds ES-2015 bundles for your code (default false) --clear Clear the console (default true) @@ -150,6 +151,7 @@ $ preact watch --prerenderUrls Path to pre-rendered routes config (default prerender-urls.json) --template Path to custom HTML template --refresh Will use [`Preact-refresh`](https://github.com/JoviDeCroock/preact-refresh) to do hot-reloading + --devServer Determine if dev server should be enabled (default true) -c, --config Path to custom CLI config (default preact.config.js) -H, --host Set server hostname (default 0.0.0.0) -p, --port Set server port (default 8080) diff --git a/packages/cli/lib/commands/watch.js b/packages/cli/lib/commands/watch.js index d9cf4a5f6..12f4ac4d5 100644 --- a/packages/cli/lib/commands/watch.js +++ b/packages/cli/lib/commands/watch.js @@ -1,13 +1,21 @@ -const runWebpack = require('../lib/webpack/run-webpack'); +const rimraf = require('rimraf'); +const { resolve } = require('path'); +const { promisify } = require('util'); const { warn } = require('../util'); +const runWebpack = require('../lib/webpack/run-webpack'); + +const toBool = (val) => val === void 0 || (val === 'false' ? false : val); module.exports = async function (src, argv) { if (argv.rhl) { delete argv.rhl; argv.refresh = argv.rhl; } + argv.src = src || argv.src; argv.production = false; + argv.devServer = toBool(argv.devServer); + if (!argv.devServer) argv.prerender = true; if (argv.https || process.env.HTTPS) { let { key, cert, cacert } = argv; @@ -18,5 +26,10 @@ module.exports = async function (src, argv) { } } + if (argv.clean === void 0) { + let dest = resolve(argv.cwd, argv.dest); + await promisify(rimraf)(dest); + } + return runWebpack(argv, true); }; diff --git a/packages/cli/lib/index.js b/packages/cli/lib/index.js index 86bee372e..3892deda9 100755 --- a/packages/cli/lib/index.js +++ b/packages/cli/lib/index.js @@ -78,6 +78,11 @@ prog .command('watch [src]') .describe('Start a live-reload server for development') .option('--src', 'Specify source directory', 'src') + .option( + '--dest', + 'Specify output directory if dev server is disabled', + 'build' + ) .option('--cwd', 'A directory to use instead of $PWD', '.') .option('--esm', 'Builds ES-2015 bundles for your code', false) .option('--clear', 'Clear the console', true) @@ -101,6 +106,7 @@ prog 'Enables experimental preact-refresh functionality', false ) + .option('--devServer', 'Determine if dev server should be enabled', true) .option('-c, --config', 'Path to custom CLI config', 'preact.config.js') .option('-H, --host', 'Set server hostname', '0.0.0.0') .option('-p, --port', 'Set server port', 8080) diff --git a/packages/cli/lib/lib/webpack/run-webpack.js b/packages/cli/lib/lib/webpack/run-webpack.js index 6e200786f..7eb9d2e8f 100644 --- a/packages/cli/lib/lib/webpack/run-webpack.js +++ b/packages/cli/lib/lib/webpack/run-webpack.js @@ -13,9 +13,17 @@ const { error, isDir, warn } = require('../../util'); async function devBuild(env) { let config = await clientConfig(env); - await transformConfig(env, config); + const shouldRunDevServer = env.devServer; + + if (!shouldRunDevServer && env.prerender) { + let ssrConfig = serverConfig(env); + await transformConfig(env, ssrConfig, true); + let serverCompiler = webpack(ssrConfig); + await runWatch(serverCompiler); + } + let userPort = parseInt(process.env.PORT || config.devServer.port, 10) || 8080; let port = await getPort({ port: userPort }); @@ -39,6 +47,8 @@ async function devBuild(env) { }); compiler.hooks.done.tap('CliDevPlugin', (stats) => { + if (!shouldRunDevServer) return; + let devServer = config.devServer; let protocol = process.env.HTTPS || devServer.https ? 'https' : 'http'; let host = process.env.HOST || devServer.host || 'localhost'; @@ -75,6 +85,8 @@ async function devBuild(env) { stats: { colors: true }, }); + if (!shouldRunDevServer) return res(runWatch(compiler, c)); + let server = new DevServer(compiler, c); server.listen(port); res(server); @@ -121,6 +133,20 @@ function runCompiler(compiler) { }); } +function runWatch(compiler, options = {}) { + return new Promise((res, rej) => { + compiler.watch(options, (err, stats) => { + showStats(stats, false); + + if (err || (stats && stats.hasErrors())) { + rej(`${red('\n\nBuild failed! \n\n')} ${err || ''}`); + } + + res(stats); + }); + }); +} + function showStats(stats, isProd) { if (stats) { if (stats.hasErrors()) { diff --git a/packages/cli/lib/lib/webpack/webpack-base-config.js b/packages/cli/lib/lib/webpack/webpack-base-config.js index 95eee591f..245dd9877 100644 --- a/packages/cli/lib/lib/webpack/webpack-base-config.js +++ b/packages/cli/lib/lib/webpack/webpack-base-config.js @@ -71,7 +71,7 @@ function getSassConfiguration(...includePaths) { } module.exports = function (env) { - const { cwd, isProd, isWatch, src, source } = env; + const { cwd, isProd, isWatch, devServer, src, source } = env; const babelConfigFile = env.babelConfig || '.babelrc'; const IS_SOURCE_PREACT_X_OR_ABOVE = isInstalledVersionPreactXOrAbove(cwd); // Apply base-level `env` values @@ -227,7 +227,7 @@ module.exports = function (env) { test: /\.(p?css|less|s[ac]ss|styl)$/, include: [source('components'), source('routes')], use: [ - isWatch ? 'style-loader' : MiniCssExtractPlugin.loader, + devServer ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { @@ -253,7 +253,7 @@ module.exports = function (env) { test: /\.(p?css|less|s[ac]ss|styl)$/, exclude: [source('components'), source('routes')], use: [ - isWatch ? 'style-loader' : MiniCssExtractPlugin.loader, + devServer ? 'style-loader' : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { diff --git a/packages/cli/lib/lib/webpack/webpack-client-config.js b/packages/cli/lib/lib/webpack/webpack-client-config.js index 5a99326a4..3939c8e5d 100644 --- a/packages/cli/lib/lib/webpack/webpack-client-config.js +++ b/packages/cli/lib/lib/webpack/webpack-client-config.js @@ -184,7 +184,7 @@ function isProd(config) { 'process.env.ESM': config.esm, 'process.env.PRERENDER': config.prerender, }), - new SizePlugin() + new SizePlugin(), ], optimization: { diff --git a/packages/cli/tests/lib/cli.js b/packages/cli/tests/lib/cli.js index 5c0a78ca2..f2a781d1e 100644 --- a/packages/cli/tests/lib/cli.js +++ b/packages/cli/tests/lib/cli.js @@ -20,7 +20,7 @@ const argv = { 'inline-css': true, }; -exports.create = async function(template, name) { +exports.create = async function (template, name) { let dest = tmpDir(); name = name || `test-${template}`; @@ -35,7 +35,7 @@ exports.create = async function(template, name) { return dest; }; -exports.build = function(cwd, options, installNodeModules = false) { +exports.build = function (cwd, options, installNodeModules = false) { if (!installNodeModules) { mkdirp.sync(join(cwd, 'node_modules')); // ensure exists, avoid exit() linkPackage('preact', root, cwd); @@ -48,7 +48,12 @@ exports.build = function(cwd, options, installNodeModules = false) { return cmd.build(argv.src, Object.assign({}, opts, options)); }; -exports.watch = function(cwd, port, host = '127.0.0.1') { - let opts = Object.assign({ cwd, host, port, https: false }, argv); +exports.watch = function (cwd, port, host = '127.0.0.1', devServer = true) { + if (!devServer) { + mkdirp.sync(join(cwd, 'node_modules')); // ensure exists, avoid exit() + linkPackage('preact', root, cwd); + linkPackage('preact-render-to-string', root, cwd); + } + let opts = Object.assign({ cwd, host, port, https: false, devServer }, argv); return cmd.watch(argv.src, opts); }; diff --git a/packages/cli/tests/watch.test.js b/packages/cli/tests/watch.test.js index 6979d4909..325afe5e5 100644 --- a/packages/cli/tests/watch.test.js +++ b/packages/cli/tests/watch.test.js @@ -2,6 +2,7 @@ const fs = require('../lib/fs'); const { resolve } = require('path'); const startChrome = require('./lib/chrome'); const { create, watch } = require('./lib/cli'); +const { sleep } = require('./lib/utils'); const { loadPage, waitUntilExpression } = startChrome; let chrome, server; @@ -33,4 +34,28 @@ describe('preact', () => { server.close(); }); + + it('should hot reload and prerender without a development server', async () => { + let dir = await create('default'); + await watch(dir, undefined, undefined, false); + + const initialContent = await fs.readFile( + resolve(dir, './build/bundle.js'), + 'utf8' + ); + + let header = resolve(dir, './src/components/header/index.js'); + let original = await fs.readFile(header, 'utf8'); + let update = original.replace('

Preact App

', '

Test App

'); + await fs.writeFile(header, update); + + await sleep(1500); // wait for a rebuild + const updatedContent = await fs.readFile( + resolve(dir, './build/bundle.js'), + 'utf8' + ); + + expect(updatedContent).not.toEqual(initialContent); + expect(updatedContent).not.toContain('Preact App'); + }); });