diff --git a/packages/cli/src/utils/BatchChanges.test.js b/packages/cli/src/utils/BatchChanges.test.js index ce3489896..600e0b546 100644 --- a/packages/cli/src/utils/BatchChanges.test.js +++ b/packages/cli/src/utils/BatchChanges.test.js @@ -132,7 +132,6 @@ test('BatchChanges calls function again if it receives new change while executin expect(fn).toHaveBeenCalledTimes(1); await wait(50); expect(fn).toHaveBeenCalledTimes(2); - await wait(50); }); test('BatchChanges provides arguments to the called function', async () => { diff --git a/packages/server-dev/src/manager/BatchChanges.mjs b/packages/server-dev/src/manager/BatchChanges.mjs index fddafe38b..def45d671 100644 --- a/packages/server-dev/src/manager/BatchChanges.mjs +++ b/packages/server-dev/src/manager/BatchChanges.mjs @@ -16,14 +16,16 @@ class BatchChanges { constructor({ fn, minDelay }) { - this.args = []; - this.fn = fn; - this.delay = minDelay || 500; - this.minDelay = minDelay || 500; this._call = this._call.bind(this); + this.args = []; + this.delay = minDelay || 500; + this.fn = fn; + this.minDelay = minDelay || 500; + this.repeat = false; + this.running = false; } - newChange(args) { + newChange(...args) { this.args.push(args); this.delay = this.minDelay; this._startTimer(); @@ -33,18 +35,29 @@ class BatchChanges { if (this.timer) { clearTimeout(this.timer); } - this.timer = setTimeout(this._call, this.delay); + if (this.running) { + this.repeat = true; + } else { + this.timer = setTimeout(this._call, this.delay); + } } async _call() { + this.running = true; try { const args = this.args; this.args = []; await this.fn(args); + this.running = false; + if (this.repeat) { + this.repeat = false; + this._call(); + } } catch (error) { + this.running = false; console.error(error); this.delay *= 2; - console.warn(`Retrying in ${this.delay / 1000}s.`, { timestamp: true }); + console.warn(`Retrying in ${this.delay / 1000}s.`); this._startTimer(); } } diff --git a/packages/server-dev/src/manager/getContext.mjs b/packages/server-dev/src/manager/getContext.mjs index 30405e53c..7e0f8e5ef 100644 --- a/packages/server-dev/src/manager/getContext.mjs +++ b/packages/server-dev/src/manager/getContext.mjs @@ -27,6 +27,7 @@ async function getContext() { config: path.resolve(configDirectory), }, packageManager, + restartServer: () => {}, skipInstall, }; return context; diff --git a/packages/server-dev/src/manager/resetServer.mjs b/packages/server-dev/src/manager/initialBuild.mjs similarity index 64% rename from packages/server-dev/src/manager/resetServer.mjs rename to packages/server-dev/src/manager/initialBuild.mjs index 2309e7b9e..32579b925 100644 --- a/packages/server-dev/src/manager/resetServer.mjs +++ b/packages/server-dev/src/manager/initialBuild.mjs @@ -1,3 +1,4 @@ +#!/usr/bin/env node /* Copyright 2020-2021 Lowdefy, Inc @@ -14,17 +15,15 @@ limitations under the License. */ -import runLowdefyBuild from './runLowdefyBuild.mjs'; -import runNextBuild from './runNextBuild.mjs'; -import installServer from './installServer.mjs'; +import installServer from './processes/installServer.mjs'; +import lowdefyBuild from './processes/lowdefyBuild.mjs'; +import nextBuild from './processes/nextBuild.mjs'; -async function resetServer(context) { - // TODO: Only install when needed +async function initialBuild(context) { await installServer(context); - await runLowdefyBuild(context); - // TODO: Only install when needed + await lowdefyBuild(context); await installServer(context); - await runNextBuild(context); + await nextBuild(context); } -export default resetServer; +export default initialBuild; diff --git a/packages/server-dev/src/manager/installServer.mjs b/packages/server-dev/src/manager/processes/installServer.mjs similarity index 100% rename from packages/server-dev/src/manager/installServer.mjs rename to packages/server-dev/src/manager/processes/installServer.mjs diff --git a/packages/server-dev/src/manager/runLowdefyBuild.mjs b/packages/server-dev/src/manager/processes/lowdefyBuild.mjs similarity index 96% rename from packages/server-dev/src/manager/runLowdefyBuild.mjs rename to packages/server-dev/src/manager/processes/lowdefyBuild.mjs index c9bd697e9..eb7e6b211 100644 --- a/packages/server-dev/src/manager/runLowdefyBuild.mjs +++ b/packages/server-dev/src/manager/processes/lowdefyBuild.mjs @@ -20,7 +20,7 @@ async function runLowdefyBuild({ packageManager, directories }) { await spawnProcess({ logger: console, args: ['run', 'build:lowdefy'], - command: packageManager || 'npm', + command: packageManager, processOptions: { env: { ...process.env, diff --git a/packages/server-dev/src/manager/runNextBuild.mjs b/packages/server-dev/src/manager/processes/nextBuild.mjs similarity index 95% rename from packages/server-dev/src/manager/runNextBuild.mjs rename to packages/server-dev/src/manager/processes/nextBuild.mjs index 98c1c3cbb..16db81081 100644 --- a/packages/server-dev/src/manager/runNextBuild.mjs +++ b/packages/server-dev/src/manager/processes/nextBuild.mjs @@ -20,7 +20,7 @@ async function runNextBuild({ packageManager }) { await spawnProcess({ logger: console, args: ['run', 'build:next'], - command: packageManager || 'npm', + command: packageManager, silent: false, }); } diff --git a/packages/server-dev/src/manager/processes/startServer.mjs b/packages/server-dev/src/manager/processes/startServer.mjs new file mode 100644 index 000000000..99b97ac12 --- /dev/null +++ b/packages/server-dev/src/manager/processes/startServer.mjs @@ -0,0 +1,51 @@ +/* + 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 spawnKillableProcess from '../spawnKillableProcess.mjs'; + +function startServerProcess({ context, handleExit }) { + context.serverProcess = spawnKillableProcess({ + logger: console, + args: ['run', 'next', 'start'], + command: context.packageManager, + silent: false, + }); + context.serverProcess.on('exit', handleExit); + context.restartServer = async () => { + context.serverProcess.kill(); + startServerProcess({ context, handleExit }); + }; +} + +async function startServer(context) { + return new Promise((resolve, reject) => { + function handleExit(code) { + if (code !== 0) { + // TODO: Shutdown server + reject(new Error('Server error.')); + } + resolve(); + } + try { + startServerProcess({ context, handleExit }); + } catch (error) { + // TODO: Shutdown server + reject(error); + } + }); +} + +export default startServer; diff --git a/packages/server-dev/src/manager/run.mjs b/packages/server-dev/src/manager/run.mjs index 9e31a8fb6..391d500a7 100644 --- a/packages/server-dev/src/manager/run.mjs +++ b/packages/server-dev/src/manager/run.mjs @@ -16,14 +16,14 @@ */ import getContext from './getContext.mjs'; -import resetServer from './resetServer.mjs'; -import setupFileWatchers from './setupFileWatchers.mjs'; -import startServer from './startServer.mjs'; +import initialBuild from './initialBuild.mjs'; +import startWatchers from './watchers/startWatchers.mjs'; +import startServer from './processes/startServer.mjs'; async function run() { const context = await getContext(); - await resetServer(context); - await setupFileWatchers(context); + await initialBuild(context); + await startWatchers(context); await startServer(context); } diff --git a/packages/server-dev/src/manager/setupFileWatchers.mjs b/packages/server-dev/src/manager/setupFileWatchers.mjs deleted file mode 100644 index 307b2c9e1..000000000 --- a/packages/server-dev/src/manager/setupFileWatchers.mjs +++ /dev/null @@ -1,23 +0,0 @@ -/* - 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 setupConfigWatcher from './watchers/setupConfigWatcher.mjs'; - -async function setupWatchers(context) { - await Promise.all([setupConfigWatcher(context)]); -} - -export default setupWatchers; diff --git a/packages/server-dev/src/manager/spawnKillableProcess.mjs b/packages/server-dev/src/manager/spawnKillableProcess.mjs new file mode 100644 index 000000000..b0f9a10c7 --- /dev/null +++ b/packages/server-dev/src/manager/spawnKillableProcess.mjs @@ -0,0 +1,55 @@ +/* + 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 { spawn } from 'child_process'; + +function spawnKillableProcess({ logger, command, args, processOptions, silent }) { + const process = spawn(command, args, processOptions); + + process.stdout.on('data', (data) => { + if (!silent) { + data + .toString('utf8') + .split('\n') + .forEach((line) => { + if (line) { + logger.log(line); + } + }); + } + }); + + process.stderr.on('data', (data) => { + if (!silent) { + data + .toString('utf8') + .split('\n') + .forEach((line) => { + if (line) { + logger.warn(line); + } + }); + } + }); + + process.on('error', (error) => { + throw error; + }); + + return process; +} + +export default spawnKillableProcess; diff --git a/packages/server-dev/src/manager/startServer.mjs b/packages/server-dev/src/manager/startServer.mjs deleted file mode 100644 index d0451cdae..000000000 --- a/packages/server-dev/src/manager/startServer.mjs +++ /dev/null @@ -1,28 +0,0 @@ -/* - 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 { spawnProcess } from '@lowdefy/node-utils'; - -async function startServer({ packageManager }) { - await spawnProcess({ - logger: console, - args: ['run', 'next', 'start'], - command: packageManager || 'npm', - silent: false, - }); -} - -export default startServer; diff --git a/packages/server-dev/src/manager/watchers/setupConfigWatcher.mjs b/packages/server-dev/src/manager/watchers/configWatcher.mjs similarity index 77% rename from packages/server-dev/src/manager/watchers/setupConfigWatcher.mjs rename to packages/server-dev/src/manager/watchers/configWatcher.mjs index 4e27241df..ef2447d4c 100644 --- a/packages/server-dev/src/manager/watchers/setupConfigWatcher.mjs +++ b/packages/server-dev/src/manager/watchers/configWatcher.mjs @@ -14,15 +14,16 @@ limitations under the License. */ -import runLowdefyBuild from '../runLowdefyBuild.mjs'; +import lowdefyBuild from '../processes/lowdefyBuild.mjs'; import setupWatcher from './setupWatcher.mjs'; -async function setupConfigWatcher(context) { +async function configWatcher(context) { const callback = async () => { - console.log('Running build'); - await runLowdefyBuild(context); + await lowdefyBuild(context); + context.restartServer(); }; + // TODO: Add ignored paths return setupWatcher({ callback, watchPaths: [context.directories.config] }); } -export default setupConfigWatcher; +export default configWatcher; diff --git a/packages/server-dev/src/manager/watchers/startWatchers.mjs b/packages/server-dev/src/manager/watchers/startWatchers.mjs new file mode 100644 index 000000000..142b2b7a5 --- /dev/null +++ b/packages/server-dev/src/manager/watchers/startWatchers.mjs @@ -0,0 +1,63 @@ +/* + 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 configWatcher from './configWatcher.mjs'; + +/* +Config change +Watch , , ! +- Lowdefy build +- Trigger soft reload +---------------------------------------- +Install new plugin +Watch /package.json + +- Install Server. +- Next build. +- No need for Lowdefy build (confirm?) +- Trigger hard reload +- Restart server. + +---------------------------------------- +.env change +Watch /.env +- Trigger hard reload +- Restart server. +---------------------------------------- +Lowdefy version changed +Watch /lowdefy.yaml +- Warn and process.exit() +---------------------------------------- +Style vars/app config change +Watch /build/app.json +Watch /build/config.json +- Next build. +- Trigger hard reload +- Restart server. + +---------------------------------------- +New plugin or icon used. +Watch /build/plugins/* +- Next build. (or dynamic import?) +- Trigger hard reload +- Restart server. +*/ + +async function startWatchers(context) { + await Promise.all([configWatcher(context)]); +} + +export default startWatchers;