From b4861d0892ee9a91ff49b3bb72498d8c42c02778 Mon Sep 17 00:00:00 2001
From: Sam Tolmay <sam@lowdefy.com>
Date: Mon, 17 Jan 2022 16:49:12 +0200
Subject: [PATCH] feat(server-dev): Updates to dev server manager.

---
 .../server-dev/src/manager/getContext.mjs     | 21 ++++++++++-
 .../server-dev/src/manager/initialBuild.mjs   | 12 ++----
 .../src/manager/processes/installServer.mjs   | 20 +++++-----
 .../src/manager/processes/lowdefyBuild.mjs    | 32 ++++++++--------
 .../src/manager/processes/nextBuild.mjs       | 19 ++++++----
 .../src/manager/processes/reloadClients.mjs   | 26 +++++++++++++
 .../src/manager/processes/startServer.mjs     | 11 ++++--
 packages/server-dev/src/manager/wait.mjs      | 21 +++++++++++
 .../src/manager/watchers/configWatcher.mjs    |  5 +--
 .../src/manager/watchers/envWatcher.mjs       | 37 +++++++++++++++++++
 .../src/manager/watchers/setupWatcher.mjs     | 11 ++++--
 .../src/manager/watchers/startWatchers.mjs    |  3 +-
 packages/utils/node-utils/src/writeFile.js    |  8 +---
 13 files changed, 167 insertions(+), 59 deletions(-)
 create mode 100644 packages/server-dev/src/manager/processes/reloadClients.mjs
 create mode 100644 packages/server-dev/src/manager/wait.mjs
 create mode 100644 packages/server-dev/src/manager/watchers/envWatcher.mjs

diff --git a/packages/server-dev/src/manager/getContext.mjs b/packages/server-dev/src/manager/getContext.mjs
index 7e0f8e5ef..c0c28fa87 100644
--- a/packages/server-dev/src/manager/getContext.mjs
+++ b/packages/server-dev/src/manager/getContext.mjs
@@ -18,18 +18,35 @@ import path from 'path';
 import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
 
+import lowdefyBuild from './processes/lowdefyBuild.mjs';
+import nextBuild from './processes/nextBuild.mjs';
+import installServer from './processes/installServer.mjs';
+import reloadClients from './processes/reloadClients.mjs';
+
 const argv = yargs(hideBin(process.argv)).argv;
 
 async function getContext() {
-  const { configDirectory = process.cwd(), packageManager = 'npm', skipInstall } = argv;
+  const {
+    configDirectory = process.cwd(),
+    packageManager = 'npm',
+    skipInstall,
+    verbose = false,
+  } = argv;
   const context = {
     directories: {
       config: path.resolve(configDirectory),
     },
     packageManager,
-    restartServer: () => {},
     skipInstall,
+    restartServer: () => {},
+    shutdownServer: () => {},
+    verbose,
   };
+  context.installServer = installServer(context);
+  context.lowdefyBuild = lowdefyBuild(context);
+  context.nextBuild = nextBuild(context);
+  context.reloadClients = reloadClients(context);
+
   return context;
 }
 
diff --git a/packages/server-dev/src/manager/initialBuild.mjs b/packages/server-dev/src/manager/initialBuild.mjs
index 32579b925..cd96934ef 100644
--- a/packages/server-dev/src/manager/initialBuild.mjs
+++ b/packages/server-dev/src/manager/initialBuild.mjs
@@ -15,15 +15,11 @@
   limitations under the License.
 */
 
-import installServer from './processes/installServer.mjs';
-import lowdefyBuild from './processes/lowdefyBuild.mjs';
-import nextBuild from './processes/nextBuild.mjs';
-
 async function initialBuild(context) {
-  await installServer(context);
-  await lowdefyBuild(context);
-  await installServer(context);
-  await nextBuild(context);
+  await context.installServer();
+  await context.lowdefyBuild();
+  await context.installServer();
+  await context.nextBuild();
 }
 
 export default initialBuild;
diff --git a/packages/server-dev/src/manager/processes/installServer.mjs b/packages/server-dev/src/manager/processes/installServer.mjs
index 8f13605f7..a949416e9 100644
--- a/packages/server-dev/src/manager/processes/installServer.mjs
+++ b/packages/server-dev/src/manager/processes/installServer.mjs
@@ -21,15 +21,17 @@ const args = {
   yarn: ['install'],
 };
 
-async function installServer({ packageManager, skipInstall }) {
-  if (skipInstall) return;
-  console.log('Installing server');
-  await spawnProcess({
-    logger: console,
-    command: packageManager, // npm or yarn
-    args: args[packageManager],
-    silent: false,
-  });
+function installServer({ packageManager, skipInstall, verbose }) {
+  return async () => {
+    if (skipInstall) return;
+    console.log('Installing server...');
+    await spawnProcess({
+      logger: console,
+      command: packageManager, // npm or yarn
+      args: args[packageManager],
+      silent: !verbose,
+    });
+  };
 }
 
 export default installServer;
diff --git a/packages/server-dev/src/manager/processes/lowdefyBuild.mjs b/packages/server-dev/src/manager/processes/lowdefyBuild.mjs
index eb7e6b211..b8b047550 100644
--- a/packages/server-dev/src/manager/processes/lowdefyBuild.mjs
+++ b/packages/server-dev/src/manager/processes/lowdefyBuild.mjs
@@ -16,21 +16,23 @@
 
 import { spawnProcess } from '@lowdefy/node-utils';
 
-async function runLowdefyBuild({ packageManager, directories }) {
-  await spawnProcess({
-    logger: console,
-    args: ['run', 'build:lowdefy'],
-    command: packageManager,
-    processOptions: {
-      env: {
-        ...process.env,
-        LOWDEFY_BUILD_DIRECTORY: './build',
-        LOWDEFY_CONFIG_DIRECTORY: directories.config,
-        LOWDEFY_SERVER_DIRECTORY: process.cwd(),
+function lowdefyBuild({ packageManager, directories }) {
+  return async () => {
+    await spawnProcess({
+      logger: console,
+      args: ['run', 'build:lowdefy'],
+      command: packageManager,
+      processOptions: {
+        env: {
+          ...process.env,
+          LOWDEFY_BUILD_DIRECTORY: './build',
+          LOWDEFY_CONFIG_DIRECTORY: directories.config,
+          LOWDEFY_SERVER_DIRECTORY: process.cwd(),
+        },
       },
-    },
-    silent: false,
-  });
+      silent: false,
+    });
+  };
 }
 
-export default runLowdefyBuild;
+export default lowdefyBuild;
diff --git a/packages/server-dev/src/manager/processes/nextBuild.mjs b/packages/server-dev/src/manager/processes/nextBuild.mjs
index 16db81081..8a40201ae 100644
--- a/packages/server-dev/src/manager/processes/nextBuild.mjs
+++ b/packages/server-dev/src/manager/processes/nextBuild.mjs
@@ -16,13 +16,16 @@
 
 import { spawnProcess } from '@lowdefy/node-utils';
 
-async function runNextBuild({ packageManager }) {
-  await spawnProcess({
-    logger: console,
-    args: ['run', 'build:next'],
-    command: packageManager,
-    silent: false,
-  });
+function nextBuild({ packageManager, verbose }) {
+  return async () => {
+    console.log('Building next app...');
+    await spawnProcess({
+      logger: console,
+      args: ['run', 'build:next'],
+      command: packageManager,
+      silent: !verbose,
+    });
+  };
 }
 
-export default runNextBuild;
+export default nextBuild;
diff --git a/packages/server-dev/src/manager/processes/reloadClients.mjs b/packages/server-dev/src/manager/processes/reloadClients.mjs
new file mode 100644
index 000000000..ff8ee7189
--- /dev/null
+++ b/packages/server-dev/src/manager/processes/reloadClients.mjs
@@ -0,0 +1,26 @@
+/*
+  Copyright 2020-2021 Lowdefy, Inc
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+import { writeFile } from '@lowdefy/node-utils';
+
+function reloadClients(context) {
+  return async () => {
+    await writeFile({ filePath: './build/reload', content: `${Date.now()}` });
+    console.log('Reloaded');
+  };
+}
+
+export default reloadClients;
diff --git a/packages/server-dev/src/manager/processes/startServer.mjs b/packages/server-dev/src/manager/processes/startServer.mjs
index 99b97ac12..92697bb29 100644
--- a/packages/server-dev/src/manager/processes/startServer.mjs
+++ b/packages/server-dev/src/manager/processes/startServer.mjs
@@ -24,17 +24,22 @@ function startServerProcess({ context, handleExit }) {
     silent: false,
   });
   context.serverProcess.on('exit', handleExit);
-  context.restartServer = async () => {
+  context.restartServer = () => {
+    console.log('Restarting server...');
     context.serverProcess.kill();
     startServerProcess({ context, handleExit });
   };
+  context.shutdownServer = () => {
+    console.log('Shutting down server...');
+    context.serverProcess.kill();
+  };
 }
 
 async function startServer(context) {
   return new Promise((resolve, reject) => {
     function handleExit(code) {
       if (code !== 0) {
-        // TODO: Shutdown server
+        context.shutdownServer && context.shutdownServer();
         reject(new Error('Server error.'));
       }
       resolve();
@@ -42,7 +47,7 @@ async function startServer(context) {
     try {
       startServerProcess({ context, handleExit });
     } catch (error) {
-      // TODO: Shutdown server
+      context.shutdownServer && context.shutdownServer();
       reject(error);
     }
   });
diff --git a/packages/server-dev/src/manager/wait.mjs b/packages/server-dev/src/manager/wait.mjs
new file mode 100644
index 000000000..0014c7d99
--- /dev/null
+++ b/packages/server-dev/src/manager/wait.mjs
@@ -0,0 +1,21 @@
+/*
+  Copyright 2020-2021 Lowdefy, Inc
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+async function wait(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export default wait;
diff --git a/packages/server-dev/src/manager/watchers/configWatcher.mjs b/packages/server-dev/src/manager/watchers/configWatcher.mjs
index ef2447d4c..73a2e9e1b 100644
--- a/packages/server-dev/src/manager/watchers/configWatcher.mjs
+++ b/packages/server-dev/src/manager/watchers/configWatcher.mjs
@@ -14,13 +14,12 @@
   limitations under the License.
 */
 
-import lowdefyBuild from '../processes/lowdefyBuild.mjs';
 import setupWatcher from './setupWatcher.mjs';
 
 async function configWatcher(context) {
   const callback = async () => {
-    await lowdefyBuild(context);
-    context.restartServer();
+    await context.lowdefyBuild();
+    context.reloadClients({ type: 'soft' });
   };
   // TODO: Add ignored paths
   return setupWatcher({ callback, watchPaths: [context.directories.config] });
diff --git a/packages/server-dev/src/manager/watchers/envWatcher.mjs b/packages/server-dev/src/manager/watchers/envWatcher.mjs
new file mode 100644
index 000000000..c15fc4c33
--- /dev/null
+++ b/packages/server-dev/src/manager/watchers/envWatcher.mjs
@@ -0,0 +1,37 @@
+/*
+  Copyright 2020-2021 Lowdefy, Inc
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+*/
+
+import path from 'path';
+import setupWatcher from './setupWatcher.mjs';
+import wait from '../wait.mjs';
+
+async function envWatcher(context) {
+  const callback = async () => {
+    console.log('.env file changed, restarting server...');
+    context.reloadClients({ type: 'hard' });
+    // Wait for clients to get reload event.
+    await wait(500);
+    context.restartServer();
+  };
+  // TODO: Add ignored paths
+  return setupWatcher({
+    callback,
+    watchPaths: [path.resolve(context.directories.config, '.env')],
+    watchDotfiles: true,
+  });
+}
+
+export default envWatcher;
diff --git a/packages/server-dev/src/manager/watchers/setupWatcher.mjs b/packages/server-dev/src/manager/watchers/setupWatcher.mjs
index 40cc66495..4952b1997 100644
--- a/packages/server-dev/src/manager/watchers/setupWatcher.mjs
+++ b/packages/server-dev/src/manager/watchers/setupWatcher.mjs
@@ -17,16 +17,19 @@
 import chokidar from 'chokidar';
 import BatchChanges from '../BatchChanges.mjs';
 
-function setupWatcher({ callback, watchPaths }) {
+function setupWatcher({ callback, watchDotfiles = false, ignorePaths = [], watchPaths }) {
   return new Promise((resolve) => {
     // const { watch = [], watchIgnore = [] } = context.options;
     // const resolvedWatchPaths = watch.map((pathName) => path.resolve(pathName));
 
     const batchChanges = new BatchChanges({ fn: callback });
+    const defaultIgnorePaths = watchDotfiles
+      ? []
+      : [
+          /(^|[/\\])\../, // ignore dotfiles
+        ];
     const configWatcher = chokidar.watch(watchPaths, {
-      ignored: [
-        /(^|[/\\])\../, // ignore dotfiles
-      ],
+      ignored: [...defaultIgnorePaths, ...ignorePaths],
       persistent: true,
       ignoreInitial: true,
     });
diff --git a/packages/server-dev/src/manager/watchers/startWatchers.mjs b/packages/server-dev/src/manager/watchers/startWatchers.mjs
index 142b2b7a5..a90886024 100644
--- a/packages/server-dev/src/manager/watchers/startWatchers.mjs
+++ b/packages/server-dev/src/manager/watchers/startWatchers.mjs
@@ -15,6 +15,7 @@
 */
 
 import configWatcher from './configWatcher.mjs';
+import envWatcher from './envWatcher.mjs';
 
 /*
 Config change
@@ -57,7 +58,7 @@ Watch <server-dir>/build/plugins/*
 */
 
 async function startWatchers(context) {
-  await Promise.all([configWatcher(context)]);
+  await Promise.all([configWatcher(context), envWatcher(context)]);
 }
 
 export default startWatchers;
diff --git a/packages/utils/node-utils/src/writeFile.js b/packages/utils/node-utils/src/writeFile.js
index e7c4b1356..d348db5b2 100644
--- a/packages/utils/node-utils/src/writeFile.js
+++ b/packages/utils/node-utils/src/writeFile.js
@@ -28,13 +28,9 @@ async function writeFile({ filePath, content }) {
       `Could not write file, file path should be a string, received ${JSON.stringify(filePath)}.`
     );
   }
-  if (filePath !== path.resolve(filePath)) {
-    throw new Error(
-      `Could not write file, file path was not resolved, received ${JSON.stringify(filePath)}.`
-    );
-  }
+
   try {
-    await writeFilePromise(filePath, content);
+    await writeFilePromise(path.resolve(filePath), content);
   } catch (error) {
     if (error.code === 'ENOENT') {
       await mkdirPromise(path.dirname(filePath), { recursive: true });