Merge pull request #1055 from lowdefy/dev-server

Development server manager process
This commit is contained in:
Gerrie van Wyk 2022-01-18 08:50:04 +02:00 committed by GitHub
commit d99ad74cfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 344 additions and 105 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

@ -18,17 +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,
skipInstall,
restartServer: () => {},
shutdownServer: () => {},
verbose,
};
context.installServer = installServer(context);
context.lowdefyBuild = lowdefyBuild(context);
context.nextBuild = nextBuild(context);
context.reloadClients = reloadClients(context);
return context;
}

View File

@ -0,0 +1,25 @@
#!/usr/bin/env node
/*
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 initialBuild(context) {
await context.installServer();
await context.lowdefyBuild();
await context.installServer();
await context.nextBuild();
}
export default initialBuild;

View File

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

View File

@ -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 || 'npm',
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;

View File

@ -16,13 +16,16 @@
import { spawnProcess } from '@lowdefy/node-utils';
async function startServer({ packageManager }) {
await spawnProcess({
logger: console,
args: ['run', 'next', 'start'],
command: packageManager || 'npm',
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 startServer;
export default nextBuild;

View File

@ -14,15 +14,13 @@
limitations under the License.
*/
import { spawnProcess } from '@lowdefy/node-utils';
import { writeFile } from '@lowdefy/node-utils';
async function runNextBuild({ packageManager }) {
await spawnProcess({
logger: console,
args: ['run', 'build:next'],
command: packageManager || 'npm',
silent: false,
});
function reloadClients(context) {
return async () => {
await writeFile({ filePath: './build/reload', content: `${Date.now()}` });
console.log('Reloaded');
};
}
export default runNextBuild;
export default reloadClients;

View File

@ -0,0 +1,56 @@
/*
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 = () => {
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) {
context.shutdownServer && context.shutdownServer();
reject(new Error('Server error.'));
}
resolve();
}
try {
startServerProcess({ context, handleExit });
} catch (error) {
context.shutdownServer && context.shutdownServer();
reject(error);
}
});
}
export default startServer;

View File

@ -1,30 +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 runLowdefyBuild from './runLowdefyBuild.mjs';
import runNextBuild from './runNextBuild.mjs';
import installServer from './installServer.mjs';
async function resetServer(context) {
// TODO: Only install when needed
await installServer(context);
await runLowdefyBuild(context);
// TODO: Only install when needed
await installServer(context);
await runNextBuild(context);
}
export default resetServer;

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

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

@ -14,10 +14,8 @@
limitations under the License.
*/
import setupConfigWatcher from './watchers/setupConfigWatcher.mjs';
async function setupWatchers(context) {
await Promise.all([setupConfigWatcher(context)]);
async function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export default setupWatchers;
export default wait;

View File

@ -14,15 +14,15 @@
limitations under the License.
*/
import runLowdefyBuild from '../runLowdefyBuild.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 context.lowdefyBuild();
context.reloadClients({ type: 'soft' });
};
// TODO: Add ignored paths
return setupWatcher({ callback, watchPaths: [context.directories.config] });
}
export default setupConfigWatcher;
export default configWatcher;

View File

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

View File

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

View File

@ -0,0 +1,64 @@
/*
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';
import envWatcher from './envWatcher.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), envWatcher(context)]);
}
export default startWatchers;

View File

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