2
0
mirror of https://github.com/lowdefy/lowdefy.git synced 2025-03-31 15:20:32 +08:00

Merge branch 'v4' into v4-connection-knex

This commit is contained in:
João Correia 2022-02-01 09:07:49 +00:00
commit f697b789d4
44 changed files with 345 additions and 308 deletions

1
.gitignore vendored

@ -21,7 +21,6 @@ packages/server-dev/build/**
.DS_Store
packages/build/src/test/writeFile.txt
packages/build/build/**
packages/plugins/connections/connection-mongodb/mongod-*

@ -62,7 +62,6 @@ test('addDefaultPages, no pages array', async () => {
},
id: 'home',
properties: {
icon: 'HomeOutlined',
title: 'Go to home page',
type: 'Link',
},
@ -109,7 +108,6 @@ test('addDefaultPages, empty pages array', async () => {
},
id: 'home',
properties: {
icon: 'HomeOutlined',
title: 'Go to home page',
type: 'Link',
},
@ -160,7 +158,6 @@ test('addDefaultPages, pages without 404 page', async () => {
},
id: 'home',
properties: {
icon: 'HomeOutlined',
title: 'Go to home page',
type: 'Link',
},

@ -38,6 +38,7 @@ test('buildAuth default', async () => {
roles: {},
},
},
theme: {},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: true } },
@ -59,6 +60,7 @@ test('buildAuth no pages', async () => {
roles: {},
},
},
theme: {},
},
});
});
@ -89,6 +91,7 @@ test('buildAuth all protected, some public', async () => {
roles: {},
},
},
theme: {},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: true } },
@ -124,6 +127,7 @@ test('buildAuth all public, some protected', async () => {
roles: {},
},
},
theme: {},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: false } },
@ -159,6 +163,7 @@ test('buildAuth all public', async () => {
roles: {},
},
},
theme: {},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: true } },
@ -194,6 +199,7 @@ test('buildAuth all protected', async () => {
roles: {},
},
},
theme: {},
},
pages: [
{ id: 'a', type: 'Context', auth: { public: false } },
@ -233,6 +239,7 @@ test('buildAuth roles', async () => {
},
},
},
theme: {},
},
pages: [
{ id: 'page1', type: 'Context', auth: { public: false, roles: ['role1', 'role2'] } },
@ -288,6 +295,7 @@ test('buildAuth roles and protected pages array', async () => {
protected: ['page1'],
},
},
theme: {},
},
pages: [{ id: 'page1', type: 'Context', auth: { public: false, roles: ['role1'] } }],
});
@ -319,6 +327,7 @@ test('buildAuth roles and protected true', async () => {
protected: true,
},
},
theme: {},
},
pages: [{ id: 'page1', type: 'Context', auth: { public: false, roles: ['role1'] } }],
});

@ -17,7 +17,7 @@
import { cleanDirectory } from '@lowdefy/node-utils';
async function cleanBuildDirectory({ context }) {
return cleanDirectory(context.directories.build);
await cleanDirectory(context.directories.build);
}
export default cleanBuildDirectory;

@ -15,10 +15,7 @@
*/
async function writeApp({ components, context }) {
await context.writeBuildArtifact({
filePath: 'app.json',
content: JSON.stringify(components.app || {}, null, 2),
});
await context.writeBuildArtifact('app.json', JSON.stringify(components.app || {}, null, 2));
}
export default writeApp;

@ -34,12 +34,10 @@ test('writeApp', async () => {
await writeApp({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'app.json',
content: `{
'app.json',
`{
"key": "value"
}`,
},
],
]);
});
@ -49,25 +47,11 @@ test('writeApp empty config', async () => {
app: {},
};
await writeApp({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'app.json',
content: `{}`,
},
],
]);
expect(mockWriteBuildArtifact.mock.calls).toEqual([['app.json', `{}`]]);
});
test('writeApp config undefined', async () => {
const components = {};
await writeApp({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'app.json',
content: `{}`,
},
],
]);
expect(mockWriteBuildArtifact.mock.calls).toEqual([['app.json', `{}`]]);
});

@ -15,10 +15,7 @@
*/
async function writeConfig({ components, context }) {
await context.writeBuildArtifact({
filePath: 'config.json',
content: JSON.stringify(components.config || {}, null, 2),
});
await context.writeBuildArtifact('config.json', JSON.stringify(components.config || {}, null, 2));
}
export default writeConfig;

@ -34,12 +34,10 @@ test('writeConfig', async () => {
await writeConfig({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'config.json',
content: `{
'config.json',
`{
"key": "value"
}`,
},
],
]);
});
@ -49,25 +47,11 @@ test('writeConfig empty config', async () => {
config: {},
};
await writeConfig({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'config.json',
content: `{}`,
},
],
]);
expect(mockWriteBuildArtifact.mock.calls).toEqual([['config.json', `{}`]]);
});
test('writeConfig config undefined', async () => {
const components = {};
await writeConfig({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'config.json',
content: `{}`,
},
],
]);
expect(mockWriteBuildArtifact.mock.calls).toEqual([['config.json', `{}`]]);
});

@ -22,10 +22,10 @@ async function writeConnections({ components, context }) {
throw new Error(`Connections is not an array.`);
}
const writePromises = components.connections.map(async (connection) => {
await context.writeBuildArtifact({
filePath: `connections/${connection.connectionId}.json`,
content: JSON.stringify(connection, null, 2),
});
await context.writeBuildArtifact(
`connections/${connection.connectionId}.json`,
JSON.stringify(connection, null, 2)
);
});
return Promise.all(writePromises);
}

@ -40,16 +40,14 @@ test('writeConnections write connection', async () => {
await writeConnections({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'connections/connection1.json',
content: `{
'connections/connection1.json',
`{
"id": "connection:connection1",
"connectionId": "connection1",
"properties": {
"prop": "val"
}
}`,
},
],
]);
});
@ -70,22 +68,18 @@ test('writeConnections multiple connection', async () => {
await writeConnections({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'connections/connection1.json',
content: `{
'connections/connection1.json',
`{
"id": "connection:connection1",
"connectionId": "connection1"
}`,
},
],
[
{
filePath: 'connections/connection2.json',
content: `{
'connections/connection2.json',
`{
"id": "connection:connection2",
"connectionId": "connection2"
}`,
},
],
]);
});

@ -23,10 +23,7 @@ async function writeGlobal({ components, context }) {
if (!type.isObject(components.global)) {
throw new Error('Global is not an object.');
}
await context.writeBuildArtifact({
filePath: 'global.json',
content: JSON.stringify(components.global, null, 2),
});
await context.writeBuildArtifact('global.json', JSON.stringify(components.global, null, 2));
}
export default writeGlobal;

@ -34,12 +34,10 @@ test('writeGlobal', async () => {
await writeGlobal({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'global.json',
content: `{
'global.json',
`{
"key": "value"
}`,
},
],
]);
});
@ -49,27 +47,13 @@ test('writeGlobal empty global', async () => {
global: {},
};
await writeGlobal({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'global.json',
content: `{}`,
},
],
]);
expect(mockWriteBuildArtifact.mock.calls).toEqual([['global.json', `{}`]]);
});
test('writeGlobal global undefined', async () => {
const components = {};
await writeGlobal({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'global.json',
content: `{}`,
},
],
]);
expect(mockWriteBuildArtifact.mock.calls).toEqual([['global.json', `{}`]]);
});
test('writeGlobal global not an object', async () => {

@ -20,10 +20,7 @@ async function writeMenus({ components, context }) {
if (!type.isArray(components.menus)) {
throw new Error('Menus is not an array.');
}
await context.writeBuildArtifact({
filePath: 'menus.json',
content: JSON.stringify(components.menus, null, 2),
});
await context.writeBuildArtifact('menus.json', JSON.stringify(components.menus, null, 2));
}
export default writeMenus;

@ -38,16 +38,14 @@ test('writeMenus', async () => {
await writeMenus({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'menus.json',
content: `[
'menus.json',
`[
{
"id": "menu:default",
"menuId": "default",
"links": []
}
]`,
},
],
]);
});
@ -57,14 +55,7 @@ test('writeMenus empty menus', async () => {
menus: [],
};
await writeMenus({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'menus.json',
content: `[]`,
},
],
]);
expect(mockWriteBuildArtifact.mock.calls).toEqual([['menus.json', `[]`]]);
});
test('writeMenus menus undefined', async () => {

@ -20,10 +20,10 @@ async function writePage({ page, context }) {
if (!type.isObject(page)) {
throw new Error(`Page is not an object. Received ${JSON.stringify(page)}`);
}
await context.writeBuildArtifact({
filePath: `pages/${page.pageId}/${page.pageId}.json`,
content: JSON.stringify(page, null, 2),
});
await context.writeBuildArtifact(
`pages/${page.pageId}/${page.pageId}.json`,
JSON.stringify(page, null, 2)
);
}
async function writePages({ components, context }) {

@ -39,15 +39,13 @@ test('writePages write page', async () => {
await writePages({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'pages/page1/page1.json',
content: `{
'pages/page1/page1.json',
`{
"id": "page:page1",
"pageId": "page1",
"blockId": "page1",
"requests": []
}`,
},
],
]);
});
@ -72,26 +70,22 @@ test('writePages multiple pages', async () => {
await writePages({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'pages/page1/page1.json',
content: `{
'pages/page1/page1.json',
`{
"id": "page:page1",
"pageId": "page1",
"blockId": "page1",
"requests": []
}`,
},
],
[
{
filePath: 'pages/page2/page2.json',
content: `{
'pages/page2/page2.json',
`{
"id": "page:page2",
"pageId": "page2",
"blockId": "page2",
"requests": []
}`,
},
],
]);
});

@ -17,13 +17,13 @@
import generateImportFile from './generateImportFile.js';
async function writeBlockImports({ components, context }) {
await context.writeBuildArtifact({
filePath: 'plugins/blocks.js',
content: generateImportFile({
await context.writeBuildArtifact(
'plugins/blocks.js',
generateImportFile({
types: components.types.blocks,
importPath: 'blocks',
}),
});
})
);
}
export default writeBlockImports;

@ -17,13 +17,13 @@
import generateImportFile from './generateImportFile.js';
async function writeConnectionImports({ components, context }) {
await context.writeBuildArtifact({
filePath: 'plugins/connections.js',
content: generateImportFile({
await context.writeBuildArtifact(
'plugins/connections.js',
generateImportFile({
types: components.types.connections,
importPath: 'connections',
}),
});
})
);
}
export default writeConnectionImports;

@ -28,10 +28,7 @@ export default {
async function writeIconImports({ components, context }) {
const templateFn = nunjucksFunction(template);
await context.writeBuildArtifact({
filePath: 'plugins/icons.js',
content: templateFn({ packages: components.icons }),
});
await context.writeBuildArtifact('plugins/icons.js', templateFn({ packages: components.icons }));
}
export default writeIconImports;

@ -18,20 +18,20 @@ import generateImportFile from './generateImportFile.js';
async function writeOperatorImports({ components, context }) {
// TODO: import _not and _type for validation.
await context.writeBuildArtifact({
filePath: 'plugins/operatorsClient.js',
content: generateImportFile({
await context.writeBuildArtifact(
'plugins/operatorsClient.js',
generateImportFile({
types: components.types.operators.client,
importPath: 'operators/client',
}),
});
await context.writeBuildArtifact({
filePath: 'plugins/operatorsServer.js',
content: generateImportFile({
})
);
await context.writeBuildArtifact(
'plugins/operatorsServer.js',
generateImportFile({
types: components.types.operators.server,
importPath: 'operators/server',
}),
});
})
);
}
export default writeOperatorImports;

@ -24,10 +24,10 @@ const template = `@import '@lowdefy/layout/style.less';
async function writeStyleImports({ components, context }) {
const templateFn = nunjucksFunction(template);
await context.writeBuildArtifact({
filePath: 'plugins/styles.less',
content: templateFn({ styles: components.styles }),
});
await context.writeBuildArtifact(
'plugins/styles.less',
templateFn({ styles: components.styles })
);
}
export default writeStyleImports;

@ -19,10 +19,10 @@ import { type } from '@lowdefy/helpers';
async function writeRequestsOnPage({ page, context }) {
return Promise.all(
page.requests.map(async (request) => {
await context.writeBuildArtifact({
filePath: `pages/${page.pageId}/requests/${request.requestId}.json`,
content: JSON.stringify(request, null, 2),
});
await context.writeBuildArtifact(
`pages/${page.pageId}/requests/${request.requestId}.json`,
JSON.stringify(request, null, 2)
);
delete request.properties;
delete request.type;
delete request.connectionId;

@ -49,9 +49,8 @@ test('writeRequests write request', async () => {
await writeRequests({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'pages/page1/requests/request1.json',
content: `{
'pages/page1/requests/request1.json',
`{
"id": "request:page1:request1",
"requestId": "request1",
"pageId": "page1",
@ -65,7 +64,6 @@ test('writeRequests write request', async () => {
"key": "value"
}
}`,
},
],
]);
});
@ -104,9 +102,8 @@ test('writeRequests write multiple requests on a page', async () => {
await writeRequests({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'pages/page1/requests/request1.json',
content: `{
'pages/page1/requests/request1.json',
`{
"id": "request:page1:request1",
"requestId": "request1",
"pageId": "page1",
@ -120,12 +117,10 @@ test('writeRequests write multiple requests on a page', async () => {
"key": "value"
}
}`,
},
],
[
{
filePath: 'pages/page1/requests/request2.json',
content: `{
'pages/page1/requests/request2.json',
`{
"id": "request:page1:request2",
"requestId": "request2",
"pageId": "page1",
@ -139,7 +134,6 @@ test('writeRequests write multiple requests on a page', async () => {
"key": "value"
}
}`,
},
],
]);
});
@ -184,9 +178,8 @@ test('writeRequests write requests on a for multiple pages', async () => {
await writeRequests({ components, context });
expect(mockWriteBuildArtifact.mock.calls).toEqual([
[
{
filePath: 'pages/page1/requests/request1.json',
content: `{
'pages/page1/requests/request1.json',
`{
"id": "request:page1:request1",
"requestId": "request1",
"pageId": "page1",
@ -200,12 +193,10 @@ test('writeRequests write requests on a for multiple pages', async () => {
"key": "value"
}
}`,
},
],
[
{
filePath: 'pages/page2/requests/request1.json',
content: `{
'pages/page2/requests/request1.json',
`{
"id": "request:page2:request1",
"requestId": "request1",
"pageId": "page2",
@ -219,7 +210,6 @@ test('writeRequests write requests on a for multiple pages', async () => {
"key": "value"
}
}`,
},
],
]);
});

@ -15,10 +15,7 @@
*/
async function writeTypes({ components, context }) {
await context.writeBuildArtifact({
filePath: 'types.json',
content: JSON.stringify(components.types, null, 2),
});
await context.writeBuildArtifact('types.json', JSON.stringify(components.types, null, 2));
}
export default writeTypes;

@ -19,8 +19,8 @@
import { readFile } from '@lowdefy/node-utils';
import createCounter from './utils/createCounter.js';
import createReadConfigFile from './utils/files/readConfigFile.js';
import createWriteBuildArtifact from './utils/files/writeBuildArtifact.js';
import createReadConfigFile from './utils/readConfigFile.js';
import createWriteBuildArtifact from './utils/writeBuildArtifact.js';
import addDefaultPages from './build/addDefaultPages/addDefaultPages.js';
import buildAuth from './build/buildAuth/buildAuth.js';

@ -1,41 +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 fs from 'fs';
import path from 'path';
import createWriteBuildArtifact from './writeBuildArtifact.js';
const directories = { build: path.resolve(process.cwd(), 'src/test/fileSetter') };
test('writeFile', async () => {
const filePath = path.resolve(directories.build, 'writeFile.txt');
try {
fs.unlinkSync(filePath);
} catch (error) {
//pass
}
expect(fs.existsSync(filePath)).toBe(false);
const writeBuildArtifact = createWriteBuildArtifact({ directories });
await writeBuildArtifact({
filePath: 'writeFile.txt',
content: 'Test fileSetter file',
});
const res = fs.readFileSync(filePath, 'utf8');
expect(res).toEqual('Test fileSetter file');
try {
fs.unlinkSync(filePath);
} catch (error) {
//pass
}
});

@ -18,8 +18,8 @@ import path from 'path';
import { writeFile } from '@lowdefy/node-utils';
function createWriteBuildArtifact({ directories }) {
async function writeBuildArtifact({ filePath, content }) {
return writeFile(path.resolve(directories.build, filePath), content);
async function writeBuildArtifact(filePath, content) {
await writeFile(path.join(directories.build, filePath), content);
}
return writeBuildArtifact;
}

@ -0,0 +1,33 @@
/*
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.
*/
jest.mock('@lowdefy/node-utils', () => {
return {
writeFile: jest.fn(),
};
});
const directories = { build: '/build' };
test('Write build artifact.', async () => {
const nodeUtils = await import('@lowdefy/node-utils');
nodeUtils.readFile.mockImplementation(() => Promise.resolve(null));
const createWriteBuildArtifact = (await import('./writeBuildArtifact.js')).default;
const writeBuildArtifact = createWriteBuildArtifact({ directories });
await writeBuildArtifact('artifact.txt', 'Test artifact content');
expect(nodeUtils.writeFile.mock.calls).toEqual([
['/build/artifact.txt', 'Test artifact content'],
]);
});

@ -23,11 +23,12 @@ module.exports = withLess({
}
return config;
},
swcMinify: true,
compress: false,
outputFileTracing: false,
poweredByHeader: false,
// productionBrowserSourceMaps: true
// experimental: {
// concurrentFeatures: true,
// },
generateEtags: false,
optimizeFonts: false,
eslint: {
ignoreDuringBuilds: true,
},

@ -16,6 +16,7 @@
/* eslint-disable no-console */
import path from 'path';
import { createRequire } from 'module';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
@ -30,10 +31,16 @@ import shutdownServer from './processes/shutdownServer.mjs';
import startWatchers from './processes/startWatchers.mjs';
const argv = yargs(hideBin(process.argv)).argv;
const require = createRequire(import.meta.url);
async function getContext() {
const { verbose = false } = argv;
const context = {
bin: {
// TODO: The string replace is a little hacky and will fail if the location of the bin changes,
lowdefyBuild: require.resolve('@lowdefy/build').replace('index.js', 'scripts/run.js'),
next: require.resolve('next').replace('server/next.js', 'bin/next'),
},
directories: {
build: path.resolve(process.cwd(), './build'),
config: path.resolve(

@ -16,12 +16,12 @@
import { spawnProcess } from '@lowdefy/node-utils';
function lowdefyBuild({ packageManager, directories }) {
function lowdefyBuild({ bin, directories }) {
return async () => {
await spawnProcess({
command: 'node',
args: [bin.lowdefyBuild],
logger: console,
args: ['run', 'build:lowdefy'],
command: packageManager,
processOptions: {
env: {
...process.env,

@ -16,14 +16,14 @@
import { spawnProcess } from '@lowdefy/node-utils';
function nextBuild({ packageManager, verbose }) {
function nextBuild(context) {
return async () => {
console.log('Building app...');
await spawnProcess({
logger: console,
args: ['run', 'build:next'],
command: packageManager,
silent: !verbose,
command: 'node',
args: [context.bin.next, 'build'],
silent: !context.verbose,
});
};
}

@ -20,8 +20,8 @@ import { readFile } from '@lowdefy/node-utils';
function readDotEnv(context) {
return async () => {
const dotEnvPath = path.join(context.directories.config, '.env');
context.serverEnv = dotenv.parse(await readFile(dotEnvPath));
const dotEnv = await readFile(path.join(context.directories.config, '.env'));
context.serverEnv = dotenv.parse(dotEnv || '');
};
}

@ -14,16 +14,14 @@
limitations under the License.
*/
import startServerProcess from './startServerProcess.mjs';
import startNextServer from './startNextServer.mjs';
function shutdownServer(context) {
return async () => {
if (context.serverProcess) {
console.log('Restarting server...');
context.serverProcess.kill();
startServerProcess(context);
}
function restartServer(context) {
return () => {
context.shutdownServer(); // Is this needed here?
console.log('Restarting server...');
startNextServer(context);
};
}
export default shutdownServer;
export default restartServer;

@ -15,10 +15,19 @@
*/
function shutdownServer(context) {
return async () => {
if (context.serverProcess) {
console.log('Shutting down server...');
context.serverProcess.kill();
return () => {
if (context.nextServer) {
// console.log(
// `Existing server ${context.nextServer.pid}, killed: ${context.nextServer.killed}`
// );
if (!context.nextServer.killed) {
console.log('Shutting down server...');
context.nextServer.kill();
// console.log(
// `Killed server ${context.nextServer.pid}, killed: ${context.nextServer.killed}`
// );
}
context.nextServer = null;
}
};
}

@ -17,10 +17,12 @@
import spawnProcess from '../utils/spawnProcess.mjs';
function startServerProcess(context) {
context.serverProcess = spawnProcess({
context.shutdownServer();
const nextServer = spawnProcess({
logger: console,
command: context.packageManager,
args: ['run', 'next', 'start'],
command: 'node',
args: [context.bin.next, 'start'],
silent: false,
processOptions: {
env: {
@ -30,12 +32,14 @@ function startServerProcess(context) {
},
},
});
context.serverProcess.on('exit', (code) => {
if (code !== 0) {
context.serverProcessPromise.reject(new Error('Server error.'));
}
context.serverProcessPromise.resolve();
// console.log(`Started server ${nextServer.pid}.`);
// nextServer.on('exit', (code, signal) => {
// console.log(`nextServer exit ${nextServer.pid}, signal: ${signal}, code: ${code}`);
// });
nextServer.on('error', (error) => {
console.log(error);
});
context.nextServer = nextServer;
}
export default startServerProcess;

@ -15,14 +15,14 @@
*/
/* eslint-disable no-console */
import startServerProcess from './startServerProcess.mjs';
import startNextServer from './startNextServer.mjs';
async function startServer(context) {
return new Promise((resolve, reject) => {
context.serverProcessPromise = { resolve, reject };
try {
startServerProcess(context);
startNextServer(context);
} catch (error) {
console.log(error);
reject(error);
}
});

@ -14,50 +14,17 @@
limitations under the License.
*/
import configWatcher from '../watchers/configWatcher.mjs';
import envWatcher from '../watchers/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.
*/
import lowdefyBuildWatcher from '../watchers/lowdefyBuildWatcher.mjs';
import nextBuildWatcher from '../watchers/nextBuildWatcher.mjs';
function startWatchers(context) {
return async () => {
await Promise.all([configWatcher(context), envWatcher(context)]);
await Promise.all([
envWatcher(context),
lowdefyBuildWatcher(context),
nextBuildWatcher(context),
]);
};
}

@ -20,6 +20,59 @@ import opener from 'opener';
import getContext from './getContext.mjs';
import startServer from './processes/startServer.mjs';
/*
The run script does the following:
- Run the initial Lowdefy build, install plugins, and next build and read .env
- Start file watchers to reload config and restart server if necessary
- Start the server
- Open a browser window.
Three watchers are started:
## Lowdefy build watcher
Watches:
- <config-dir>,
- <watch-dirs>
- !<ignore-dirs>
The Lowdefy build watcher watches the Lowdefy config files for changes
and runs Lowdefy build when they change, and triggers a soft reload.
If lowdefy version in lowdefy.yaml
is changed, the server warns and exits.
## .env watcher
If the .env file is changed, the new file is parsed, and the server restarted with the new env
and the server hard reloads.
## Next build watcher
The Next build watcher watches for any files where the app should be rebuilt and restarted.
It watches:
- <build-dir>/plugins/**
- <build-dir>/config.json
- <server-dir>/package.json
If app theme or config changes:
- <build-dir>/config.json changes, rebuild and restart server.
If new plugin type in an existing plugin package is used:
- <build-dir>/plugins/** changes, rebuild next and restart server.
If new plugin type in a new plugin package is used:
- <server-dir>/package.json changes, run npm install, rebuild next and restart server.
# Reload mechanism
The web client creates a Server Sent Events connection with the server on the /api/reload route.
The server watches the <build-dir>/reload file, which is written every time the server should reload,
and sends an event to the client to reload the config. The client then uses a SWR cache mutation to
fetch the new config.
If the server is restarted, the event stream is closed because the original server was shut down. The client starts
pinging the /api/ping route, until it detects a new server has started, and then reloads the window.
*/
async function run() {
const context = await getContext();
await context.initialBuild();
@ -30,6 +83,7 @@ async function run() {
opener(`http://localhost:${context.port}`);
await serverPromise;
} catch (error) {
console.log(error);
context.shutdownServer();
throw error;
}

@ -18,7 +18,7 @@
import path from 'path';
import setupWatcher from '../utils/setupWatcher.mjs';
async function envWatcher(context) {
function envWatcher(context) {
const callback = async () => {
console.warn('.env file changed.');
await context.readDotEnv();

@ -18,14 +18,15 @@
import getLowdefyVersion from '../utils/getLowdefyVersion.mjs';
import setupWatcher from '../utils/setupWatcher.mjs';
async function configWatcher(context) {
function lowdefyBuildWatcher(context) {
const callback = async (filePaths) => {
const lowdefyYamlModified = filePaths
.flat()
.some((filePath) => filePath.includes('lowdefy.yaml') || filePath.includes('lowdefy.yml'));
if (lowdefyYamlModified) {
const lowdefyVersion = await getLowdefyVersion(context);
if (lowdefyVersion !== context.version || lowdefyVersion === 'local') {
if (lowdefyVersion !== context.version && lowdefyVersion !== 'local') {
context.shutdownServer();
console.warn('Lowdefy version changed. You should restart your development server.');
process.exit();
}
@ -35,7 +36,10 @@ async function configWatcher(context) {
context.reloadClients();
};
// TODO: Add ignored and watch paths
return setupWatcher({ callback, watchPaths: [context.directories.config] });
return setupWatcher({
callback,
watchPaths: [context.directories.config],
});
}
export default configWatcher;
export default lowdefyBuildWatcher;

@ -0,0 +1,93 @@
/*
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.
*/
/* eslint-disable no-console */
import crypto from 'crypto';
import path from 'path';
import { readFile } from '@lowdefy/node-utils';
import setupWatcher from '../utils/setupWatcher.mjs';
const hashes = {};
const watchedFiles = [
'build/config.json',
'build/plugins/blocks.js',
'build/plugins/connections.js',
'build/plugins/icons.js',
'build/plugins/operatorsClient.js',
'build/plugins/operatorsServer.js',
'build/plugins/styles.less',
'package.json',
];
async function sha1(filePath) {
const content = await readFile(filePath);
return crypto
.createHash('sha1')
.update(content || '')
.digest('hex');
}
async function nextBuildWatcher(context) {
// Initialize hashes so that app does not rebuild the first time
// Lowdefy build is run.
await Promise.all(
watchedFiles.map(async (filePath) => {
const fullPath = path.resolve(context.directories.server, filePath);
hashes[fullPath] = await sha1(fullPath);
})
);
const callback = async (filePaths) => {
let install = false;
let build = false;
await Promise.all(
filePaths.flat().map(async (filePath) => {
const hash = await sha1(filePath);
if (hashes[filePath] === hash) {
return;
}
build = true;
if (filePath.endsWith('package.json')) {
install = true;
}
hashes[filePath] = hash;
})
);
if (!build) {
return;
}
context.shutdownServer();
if (install) {
await context.installPlugins();
}
await context.nextBuild();
context.restartServer();
};
return setupWatcher({
callback,
watchPaths: [
path.join(context.directories.build, 'plugins'),
path.join(context.directories.build, 'config.json'),
path.join(context.directories.server, 'package.json'),
],
});
}
export default nextBuildWatcher;