mirror of
https://github.com/lowdefy/lowdefy.git
synced 2025-04-06 15:30:30 +08:00
Merge pull request #1055 from lowdefy/dev-server
Development server manager process
This commit is contained in:
commit
d99ad74cfb
@ -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 () => {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
25
packages/server-dev/src/manager/initialBuild.mjs
Normal file
25
packages/server-dev/src/manager/initialBuild.mjs
Normal 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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
56
packages/server-dev/src/manager/processes/startServer.mjs
Normal file
56
packages/server-dev/src/manager/processes/startServer.mjs
Normal 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;
|
@ -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;
|
@ -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);
|
||||
}
|
||||
|
||||
|
55
packages/server-dev/src/manager/spawnKillableProcess.mjs
Normal file
55
packages/server-dev/src/manager/spawnKillableProcess.mjs
Normal 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;
|
@ -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;
|
@ -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;
|
37
packages/server-dev/src/manager/watchers/envWatcher.mjs
Normal file
37
packages/server-dev/src/manager/watchers/envWatcher.mjs
Normal 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;
|
@ -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,
|
||||
});
|
||||
|
64
packages/server-dev/src/manager/watchers/startWatchers.mjs
Normal file
64
packages/server-dev/src/manager/watchers/startWatchers.mjs
Normal 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;
|
@ -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 });
|
||||
|
Loading…
x
Reference in New Issue
Block a user