feat(server-dev): Add the abilty to restart the dev server.

This commit is contained in:
Sam Tolmay 2022-01-14 13:59:14 +02:00
parent 77cd297117
commit b610a63a52
No known key found for this signature in database
GPG Key ID: D004126FCD1A6DF0
14 changed files with 211 additions and 80 deletions

View File

@ -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 () => {

View File

@ -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();
}
}

View File

@ -27,6 +27,7 @@ async function getContext() {
config: path.resolve(configDirectory),
},
packageManager,
restartServer: () => {},
skipInstall,
};
return context;

View File

@ -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;

View File

@ -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,

View File

@ -20,7 +20,7 @@ async function runNextBuild({ packageManager }) {
await spawnProcess({
logger: console,
args: ['run', 'build:next'],
command: packageManager || 'npm',
command: packageManager,
silent: false,
});
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 <config-dir>, <watch-dirs>, !<ignore-dirs>
- Lowdefy build
- Trigger soft reload
----------------------------------------
Install new plugin
Watch <server>/package.json
- Install Server.
- Next build.
- No need for Lowdefy build (confirm?)
- Trigger hard reload
- Restart server.
----------------------------------------
.env change
Watch <config-dir>/.env
- Trigger hard reload
- Restart server.
----------------------------------------
Lowdefy version changed
Watch <config-dir>/lowdefy.yaml
- Warn and process.exit()
----------------------------------------
Style vars/app config change
Watch <server-dir>/build/app.json
Watch <server-dir>/build/config.json
- Next build.
- Trigger hard reload
- Restart server.
----------------------------------------
New plugin or icon used.
Watch <server-dir>/build/plugins/*
- Next build. (or dynamic import?)
- Trigger hard reload
- Restart server.
*/
async function startWatchers(context) {
await Promise.all([configWatcher(context)]);
}
export default startWatchers;