Skip to content

Commit

Permalink
Auto restart dev server on config changes
Browse files Browse the repository at this point in the history
Whenever a config file, loaded env files or package.json changes, we'll
restart wmr. Makes for a pretty nice user plugin dev experience.
  • Loading branch information
marvinhagemeister committed Mar 28, 2021
1 parent bb708e5 commit 5dcd256
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-turtles-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'wmr': minor
---

Automatically restart the dev server on config changes. This includes `package.json`, `*.env` and `wmr.config.(js|ts|mjs)`
4 changes: 3 additions & 1 deletion packages/wmr/src/lib/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@ export function parseEnvFile(str) {
* Load additional environment variables from .env files.
* @param {string} cwd
* @param {string[]} envFiles
* @param {string[]} [configWachFiles]
* @returns {Promise<Record<string, string>>}
*/
export async function readEnvFiles(cwd, envFiles) {
export async function readEnvFiles(cwd, envFiles, configWachFiles) {
const envs = await Promise.all(
envFiles.map(async file => {
const fileName = join(cwd, file);
try {
const content = await fs.readFile(fileName, 'utf-8');
if (configWachFiles) configWachFiles.push(fileName);
return parseEnvFile(content);
} catch (e) {
// Ignore, env file most likely doesn't exist
Expand Down
24 changes: 17 additions & 7 deletions packages/wmr/src/lib/normalize-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { getPort } from './net-utils.js';
/**
* @param {Partial<Options>} options
* @param {Mode} mode
* @param {string[]} [configWatchFiles]
* @returns {Promise<Options>}
*/
export async function normalizeOptions(options, mode) {
export async function normalizeOptions(options, mode, configWatchFiles = []) {
options.cwd = resolve(options.cwd || '');
process.chdir(options.cwd);

Expand All @@ -29,7 +30,11 @@ export async function normalizeOptions(options, mode) {
options.mode = mode;

const NODE_ENV = process.env.NODE_ENV || (prod ? 'production' : 'development');
options.env = await readEnvFiles(options.root, ['.env', '.env.local', `.env.${NODE_ENV}`, `.env.${NODE_ENV}.local`]);
options.env = await readEnvFiles(
options.root,
['.env', '.env.local', `.env.${NODE_ENV}`, `.env.${NODE_ENV}.local`],
configWatchFiles
);

// Output directory is relative to CWD *before* ./public is detected + appended:
options.out = resolve(options.cwd, options.out || '.cache');
Expand Down Expand Up @@ -62,8 +67,13 @@ export async function normalizeOptions(options, mode) {
await ensureOutDirPromise;

const pkgFile = resolve(options.root, 'package.json');
const pkg = fs.readFile(pkgFile, 'utf-8').then(JSON.parse);
options.aliases = (await pkg.catch(() => ({}))).alias || {};
try {
const pkg = JSON.parse(await fs.readFile(pkgFile, 'utf-8'));
options.aliases = pkg.alias || {};
configWatchFiles.push(pkgFile);
} catch (e) {
// ignore error, reading aliases from package.json is an optional feature
}

const EXTENSIONS = ['.js', '.ts', '.mjs'];

Expand All @@ -73,6 +83,8 @@ export async function normalizeOptions(options, mode) {
const file = resolve(options.root, `wmr.config${ext}`);
if (await isFile(file)) {
let configFile = file;
configWatchFiles.push(configFile);

if (ext === '.ts') {
// Create a temporary file to write compiled output into
// TODO: Do this in memory
Expand All @@ -82,7 +94,7 @@ export async function normalizeOptions(options, mode) {

const fileUrl = url.pathToFileURL(configFile);
try {
custom = await eval('(x => import(x))')(fileUrl);
custom = await eval(`(x => import(x + '?t=${Date.now()}'))`)(fileUrl);
} catch (err) {
console.log(err);
initialError = err;
Expand All @@ -102,8 +114,6 @@ export async function normalizeOptions(options, mode) {
}
}

Object.defineProperty(options, '_config', { value: custom });

/**
* @param {keyof import('wmr').Plugin} name
* @param {import('wmr').Plugin[]} plugins
Expand Down
52 changes: 51 additions & 1 deletion packages/wmr/src/start.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import chokidar from 'chokidar';
import * as kl from 'kolorist';
import server from './server.js';
import wmrMiddleware from './wmr-middleware.js';
import { getServerAddresses } from './lib/net-utils.js';
Expand All @@ -12,14 +14,51 @@ import { formatBootMessage } from './lib/output-utils.js';
* @property {Record<string, string>} [env]
*/

/**
* @type {<T>(obj: T) => T}]
*/
const deepCloneJSON = obj => JSON.parse(JSON.stringify(obj));

/**
* @param {Parameters<server>[0] & OtherOptions} options
*/
export default async function start(options = {}) {
// @todo remove this hack once registry.js is instantiable
setCwd(options.cwd);

options = await normalizeOptions(options, 'start');
// TODO: We seem to mutate our config object somewhere
const cloned = deepCloneJSON(options);

/** @type {string[]} */
const configWatchFiles = [];

// Reload server on config changes
let instance = await bootServer(cloned, configWatchFiles);
const watcher = chokidar.watch(configWatchFiles, {
cwd: cloned.root,
disableGlobbing: true
});
watcher.on('change', async () => {
await instance.close();

console.log(kl.yellow(`WMR: `) + kl.green(`config or .env file changed, restarting server...\n`));

// Fire up new instance
const cloned = deepCloneJSON(options);
const configWatchFiles = [];
instance = await bootServer(cloned, configWatchFiles);
watcher.add(configWatchFiles);
});
}

/**
*
* @param {Parameters<server>[0] & OtherOptions} options
* @param {string[]} configWatchFiles
* @returns {Promise<{ close: () => Promise<void>}>}
*/
async function bootServer(options, configWatchFiles) {
options = await normalizeOptions(options, 'start', configWatchFiles);

options.host = options.host || process.env.HOST;

Expand Down Expand Up @@ -70,4 +109,15 @@ export default async function start(options = {}) {
const message = `server running at:`;
process.stdout.write(formatBootMessage(message, addresses));
});

return {
close: () =>
new Promise((resolve, reject) => {
if (app.server) {
app.server.close(err => (err ? reject(err) : resolve()));
} else {
resolve();
}
})
};
}
61 changes: 54 additions & 7 deletions packages/wmr/test/fixtures.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ import {
getOutput,
get,
waitForMessage,
waitForNotMessage
waitForNotMessage,
wait
} from './test-helpers.js';
import { rollup } from 'rollup';
import nodeBuiltinsPlugin from '../src/plugins/node-builtins-plugin.js';

jest.setTimeout(30000);

async function updateFile(tempDir, file, replacer) {
const compPath = path.join(tempDir, file);
const content = await fs.readFile(compPath, 'utf-8');
await fs.writeFile(compPath, replacer(content));
}

describe('fixtures', () => {
/** @type {TestEnv} */
let env;
Expand Down Expand Up @@ -346,12 +353,6 @@ describe('fixtures', () => {
});

describe('hmr-scss', () => {
async function updateFile(tempDir, file, replacer) {
const compPath = path.join(tempDir, file);
const content = await fs.readFile(compPath, 'utf-8');
await fs.writeFile(compPath, replacer(content));
}

const timeout = n => new Promise(r => setTimeout(r, n));

it('should hot reload an scss-file imported from index.html', async () => {
Expand Down Expand Up @@ -601,6 +602,52 @@ describe('fixtures', () => {
await waitForMessage(instance.output, /plugin-A/);
expect(true).toEqual(true); // Silence linter
});

it('should restart server if config file changes', async () => {
await loadFixture('config-reload', env);
instance = await runWmrFast(env.tmp.path);

// TODO: Investigate why we need to wait here
await wait(2000);
// Trigger file change
await updateFile(env.tmp.path, 'wmr.config.mjs', content => content.replace(/foo/g, 'bar'));

await waitForMessage(instance.output, /restarting server/);
await waitForMessage(instance.output, /{ name: 'bar' }/);
expect(true).toEqual(true); // Silence linter
});

it('should restart server if .env file changes', async () => {
await loadFixture('config-reload-env', env);
instance = await runWmrFast(env.tmp.path);

// TODO: Investigate why we need to wait here
await wait(2000);
// Trigger file change
await updateFile(env.tmp.path, '.env', content => content.replace(/foo/g, 'bar'));

await waitForMessage(instance.output, /restarting server/);
await waitForMessage(instance.output, /{ FOO: 'bar' }/);
expect(true).toEqual(true); // Silence linter
});

it('should restart server if package.json file changes', async () => {
await loadFixture('config-reload-package-json', env);
instance = await runWmrFast(env.tmp.path);

// TODO: Investigate why we need to wait here
await wait(2000);
// Trigger file change
await updateFile(env.tmp.path, 'package.json', content => {
const json = JSON.parse(content);
json.alias = { foo: 'bar' };
return JSON.stringify(json);
});

await waitForMessage(instance.output, /restarting server/);
await waitForMessage(instance.output, /{ foo: 'bar' }/);
expect(true).toEqual(true); // Silence linter
});
});

describe('plugins', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/wmr/test/fixtures/config-reload-env/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO="foo"
1 change: 1 addition & 0 deletions packages/wmr/test/fixtures/config-reload-env/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Hello world</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Hello world</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions packages/wmr/test/fixtures/config-reload/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Hello world</h1>
7 changes: 7 additions & 0 deletions packages/wmr/test/fixtures/config-reload/wmr.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function () {
return [
{
name: 'foo'
}
];
}

0 comments on commit 5dcd256

Please sign in to comment.