feat(cli): Add option to configure cli from the lowdefy.yaml file

This commit is contained in:
SamTolmay 2021-08-13 15:29:35 +02:00
parent 3f94242c34
commit e4f62d0cf4
No known key found for this signature in database
GPG Key ID: 655CB3F5AA745CF8
27 changed files with 474 additions and 215 deletions

View File

@ -16,11 +16,9 @@
import path from 'path';
import fse from 'fs-extra';
import startUp from '../../utils/startUp';
import getFederatedModule from '../../utils/getFederatedModule';
async function build({ context, options }) {
await startUp({ context, options, command: 'build' });
async function build({ context }) {
const { default: buildScript } = await getFederatedModule({
module: 'build',
packageName: '@lowdefy/build',
@ -30,10 +28,11 @@ async function build({ context, options }) {
context.print.log(
`Cleaning block meta cache at "${path.resolve(context.cacheDirectory, './meta')}".`
);
await fse.emptyDir(path.resolve(context.cacheDirectory, './meta'));
context.print.info('Starting build.');
await buildScript({
blocksServerUrl: context.blocksServerUrl,
blocksServerUrl: context.options.blocksServerUrl,
cacheDirectory: context.cacheDirectory,
configDirectory: context.baseDirectory,
logger: context.print,

View File

@ -14,11 +14,9 @@
limitations under the License.
*/
import path from 'path';
import build from './build';
// eslint-disable-next-line no-unused-vars
import getFederatedModule from '../../utils/getFederatedModule';
// eslint-disable-next-line no-unused-vars
import startUp from '../../utils/startUp';
jest.mock('../../utils/getFederatedModule', () => {
@ -31,11 +29,12 @@ jest.mock('../../utils/getFederatedModule', () => {
jest.mock('../../utils/startUp');
test('build', async () => {
const cacheDirectory = path.resolve(process.cwd(), '.lowdefy/.cache');
const outputDirectory = path.resolve(process.cwd(), '.lowdefy/build');
await build({ context: {} });
const context = {};
await startUp({ context, options: {}, command: {} });
await build({ context });
const { default: buildScript } = getFederatedModule();
expect(buildScript).toHaveBeenCalledTimes(1);
expect(buildScript.mock.calls[0][0].outputDirectory).toEqual(outputDirectory);
expect(buildScript.mock.calls[0][0].cacheDirectory).toEqual(cacheDirectory);
expect(buildScript.mock.calls[0][0].outputDirectory).toEqual('baseDirectory/outputDirectory');
expect(buildScript.mock.calls[0][0].cacheDirectory).toEqual('baseDirectory/cacheDirectory');
});

View File

@ -20,22 +20,24 @@ import fse from 'fs-extra';
import { readFile, writeFile } from '@lowdefy/node-utils';
import checkChildProcessError from '../../utils/checkChildProcessError';
import startUp from '../../utils/startUp';
import getFederatedModule from '../../utils/getFederatedModule';
import fetchNpmTarball from '../../utils/fetchNpmTarball';
async function fetchNetlifyServer({ context, netlifyDir }) {
async function fetchNetlifyServer({ context }) {
context.print.log('Fetching Lowdefy Netlify server.');
await fetchNpmTarball({
packageName: '@lowdefy/server-netlify',
version: context.lowdefyVersion,
directory: netlifyDir,
directory: context.netlifyDir,
});
context.print.log('Fetched Lowdefy Netlify server.');
}
async function npmInstall({ context, netlifyDir }) {
await fse.copy(path.resolve(netlifyDir, 'package/package.json'), path.resolve('./package.json'));
async function npmInstall({ context }) {
await fse.copy(
path.resolve(context.netlifyDir, 'package/package.json'),
path.resolve('./package.json')
);
await fse.remove(path.resolve('./package-lock.json'));
await fse.remove(path.resolve('./package-lock.json'));
await fse.emptyDir(path.resolve('./node_modules'));
@ -63,9 +65,12 @@ async function fetchBuildScript({ context }) {
return buildScript;
}
async function build({ context, buildScript, netlifyDir }) {
async function build({ context, buildScript }) {
context.print.log('Starting Lowdefy build.');
const outputDirectory = path.resolve(netlifyDir, './package/dist/functions/graphql/build');
const outputDirectory = path.resolve(
context.netlifyDir,
'./package/dist/functions/graphql/build'
);
await buildScript({
blocksServerUrl: context.blocksServerUrl,
cacheDirectory: context.cacheDirectory,
@ -91,17 +96,17 @@ async function buildIndexHtml({ context }) {
context.print.log('Lowdefy index.html build complete.');
}
async function moveBuildArtifacts({ context, netlifyDir }) {
async function moveBuildArtifacts({ context }) {
await fse.copy(
path.resolve(netlifyDir, 'package/dist/shell'),
path.resolve(context.netlifyDir, 'package/dist/shell'),
path.resolve('./.lowdefy/publish')
);
context.print.log(`Netlify publish artifacts moved to "./lowdefy/publish".`);
}
async function moveFunctions({ context, netlifyDir }) {
async function moveFunctions({ context }) {
await fse.copy(
path.resolve(netlifyDir, 'package/dist/functions'),
path.resolve(context.netlifyDir, 'package/dist/functions'),
path.resolve('./.lowdefy/functions')
);
context.print.log(`Netlify functions artifacts moved to "./lowdefy/functions".`);
@ -114,24 +119,20 @@ async function movePublicAssets({ context }) {
context.print.log(`Public assets moved to "./lowdefy/publish/public".`);
}
async function buildNetlify({ context, options }) {
if (process.env.NETLIFY === 'true') {
options.basicPrint = true;
}
await startUp({ context, options, command: 'build-netlify' });
const netlifyDir = path.resolve(context.baseDirectory, './.lowdefy/netlify');
async function buildNetlify({ context }) {
context.netlifyDir = path.resolve(context.baseDirectory, './.lowdefy/netlify');
context.print.info('Starting build.');
const buildScript = await fetchBuildScript({ context });
await build({ context, buildScript, netlifyDir });
await build({ context, buildScript });
context.print.info('Installing Lowdefy server.');
await fetchNetlifyServer({ context, netlifyDir });
await npmInstall({ context, netlifyDir });
await fetchNetlifyServer({ context });
await npmInstall({ context });
context.print.log(`Moving artifacts.`);
await moveBuildArtifacts({ context, netlifyDir });
await moveFunctions({ context, netlifyDir });
await moveBuildArtifacts({ context });
await moveFunctions({ context });
await movePublicAssets({ context });
context.print.log(`Build artifacts.`);

View File

@ -15,10 +15,8 @@
*/
import fse from 'fs-extra';
import startUp from '../../utils/startUp';
async function cleanCache({ context, options }) {
await startUp({ context, options, command: 'clean-cache' });
async function cleanCache({ context }) {
context.print.log(`Cleaning cache at "${context.cacheDirectory}".`);
await fse.emptyDir(context.cacheDirectory);
await context.sendTelemetry();

View File

@ -17,7 +17,6 @@ import path from 'path';
import fse from 'fs-extra';
import cleanCache from './cleanCache';
// eslint-disable-next-line no-unused-vars
import startUp from '../../utils/startUp';
jest.mock('fs-extra', () => {
@ -32,13 +31,15 @@ beforeEach(() => {
});
test('cleanCache', async () => {
await cleanCache({ context: {} });
const cachePath = path.resolve(process.cwd(), './.lowdefy/.cache');
expect(fse.emptyDir.mock.calls).toEqual([[cachePath]]);
const context = {};
await startUp({ context, options: {}, command: {} });
await cleanCache({ context });
expect(fse.emptyDir.mock.calls).toEqual([['baseDirectory/cacheDirectory']]);
});
test('cleanCache baseDir', async () => {
await cleanCache({ context: {}, options: { baseDirectory: 'baseDir' } });
const cachePath = path.resolve(process.cwd(), 'baseDir/.lowdefy/.cache');
expect(fse.emptyDir.mock.calls).toEqual([[cachePath]]);
const context = {};
await startUp({ context, options: { baseDirectory: 'baseDir' }, command: {} });
await cleanCache({ context });
expect(fse.emptyDir.mock.calls).toEqual([['baseDir/cacheDirectory']]);
});

View File

@ -17,8 +17,8 @@ import path from 'path';
import chokidar from 'chokidar';
import BatchChanges from '../../utils/BatchChanges';
function buildWatcher({ build, context, options, reloadFn }) {
const { watch = [], watchIgnore = [] } = options;
function buildWatcher({ build, context, reloadFn }) {
const { watch = [], watchIgnore = [] } = context.options;
const resolvedWatchPaths = watch.map((pathName) => path.resolve(pathName));
const buildCallback = async () => {

View File

@ -30,30 +30,32 @@ async function initialBuild({ context }) {
return build;
}
async function serverSetup({ context, options }) {
async function serverSetup({ context }) {
const gqlServer = await getGraphQL({ context });
return getExpress({ context, gqlServer, options });
return getExpress({ context, gqlServer });
}
async function dev({ context, options }) {
await prepare({ context, options });
async function dev({ context }) {
await prepare({ context });
const initialBuildPromise = initialBuild({ context });
const serverSetupPromise = serverSetup({ context, options });
const serverSetupPromise = serverSetup({ context });
const [build, { expressApp, reloadFn }] = await Promise.all([
initialBuildPromise,
serverSetupPromise,
]);
buildWatcher({ build, context, options, reloadFn });
buildWatcher({ build, context, reloadFn });
envWatcher({ context });
versionWatcher({ context });
context.print.log('Starting Lowdefy development server.');
expressApp.listen(expressApp.get('port'), function () {
context.print.info(`Development server listening on port ${options.port}`);
const port = expressApp.get('port');
expressApp.listen(port, function () {
context.print.info(`Development server listening on port ${port}`);
});
opener(`http://localhost:${options.port}`);
opener(`http://localhost:${port}`);
await context.sendTelemetry({
data: {

View File

@ -21,7 +21,7 @@ import { get } from '@lowdefy/helpers';
import { readFile } from '@lowdefy/node-utils';
import findOpenPort from '../../utils/findOpenPort';
async function getExpress({ context, gqlServer, options }) {
async function getExpress({ context, gqlServer }) {
const serveIndex = async (req, res) => {
let indexHtml = await readFile(path.resolve(__dirname, 'shell/index.html'));
let appConfig = await readFile(path.resolve(context.outputDirectory, 'app.json'));
@ -39,7 +39,8 @@ async function getExpress({ context, gqlServer, options }) {
const app = express();
app.set('port', parseInt(options.port));
// port is initialized to 3000 in prepare function
app.set('port', parseInt(context.options.port));
gqlServer.applyMiddleware({ app, path: '/api/graphql' });

View File

@ -17,13 +17,10 @@ import path from 'path';
import dotenv from 'dotenv';
import fse from 'fs-extra';
import startUp from '../../utils/startUp';
async function prepare({ context, options }) {
async function prepare({ context }) {
dotenv.config({ silent: true });
// Setup
if (!options.port) options.port = 3000;
await startUp({ context, options, command: 'dev' });
if (!context.options.port) context.options.port = 3000;
context.print.log(
`Cleaning block meta cache at "${path.resolve(context.cacheDirectory, './meta')}".`
);

View File

@ -15,11 +15,11 @@
*/
import chokidar from 'chokidar';
import BatchChanges from '../../utils/BatchChanges';
import getConfig from '../../utils/getConfig';
import getLowdefyYaml from '../../utils/getLowdefyYaml';
function versionWatcher({ context }) {
const changeLowdefyFileCallback = async () => {
const { lowdefyVersion } = await getConfig(context);
const { lowdefyVersion } = await getLowdefyYaml(context);
if (lowdefyVersion !== context.lowdefyVersion) {
context.print.warn('Lowdefy version changed. You should restart your development server.');
process.exit();

View File

@ -18,11 +18,9 @@ import path from 'path';
import fse from 'fs-extra';
import { writeFile } from '@lowdefy/node-utils';
import startUp from '../../utils/startUp';
import lowdefyFile from './lowdefyFile';
async function init({ context, options }) {
await startUp({ context, options, command: 'init', lowdefyFileNotRequired: true });
async function init({ context }) {
const lowdefyFilePath = path.resolve('./lowdefy.yaml');
const fileExists = fse.existsSync(lowdefyFilePath);
if (fileExists) {

View File

@ -39,6 +39,7 @@ program
'--blocks-server-url <blocks-server-url>',
'The URL from where Lowdefy blocks will be served.'
)
.option('--disable-telemetry', 'Disable telemetry.')
.option(
'--output-directory <output-directory>',
'Change the directory to which build artifacts are saved. Default is "<base-directory>/.lowdefy/build".'
@ -57,6 +58,7 @@ program
'--blocks-server-url <blocks-server-url>',
'The URL from where Lowdefy blocks will be served.'
)
.option('--disable-telemetry', 'Disable telemetry.')
.action(runCommand(buildNetlify));
program
@ -67,6 +69,7 @@ program
'--base-directory <base-directory>',
'Change base directory. Default is the current working directory.'
)
.option('--disable-telemetry', 'Disable telemetry.')
.action(runCommand(cleanCache));
program
@ -81,6 +84,7 @@ program
'--blocks-server-url <blocks-server-url>',
'The URL from where Lowdefy blocks will be served.'
)
.option('--disable-telemetry', 'Disable telemetry.')
.option('--port <port>', 'Change the port the server is hosted at. Default is 3000.')
.option(
'--watch <paths...>',

View File

@ -14,28 +14,32 @@
limitations under the License.
*/
import path from 'path';
import { cacheDirectoryPath, outputDirectoryPath } from '../directories';
const mockStartUp = jest.fn().mockImplementation(mockStartUpImp);
async function mockStartUp({ context, options = {} }) {
async function mockStartUpImp({ context, options = {} }) {
context.command = 'test';
context.cliVersion = 'cliVersion';
context.appId = 'appId';
context.disableTelemetry = false;
context.lowdefyVersion = 'lowdefyVersion';
context.sendTelemetry = jest.fn();
context.commandLineOptions = options;
context.print = {
info: jest.fn(),
succeed: jest.fn(),
log: jest.fn(),
};
context.baseDirectory = path.resolve(options.baseDirectory || process.cwd());
context.cacheDirectory = path.resolve(context.baseDirectory, cacheDirectoryPath);
if (options.outputDirectory) {
context.outputDirectory = path.resolve(options.outputDirectory);
} else {
context.outputDirectory = path.resolve(context.baseDirectory, outputDirectoryPath);
}
context.baseDirectory = options.baseDirectory || 'baseDirectory';
context.cliConfig = {};
context.lowdefyVersion = 'lowdefyVersion';
context.appId = 'appId';
context.options = options;
context.cacheDirectory = `${context.baseDirectory}/cacheDirectory`;
context.outputDirectory = `${context.baseDirectory}/outputDirectory`;
context.sendTelemetry = jest.fn();
return context;
}

View File

@ -14,11 +14,18 @@
limitations under the License.
*/
import * as directories from './directories';
import path from 'path';
test('directories', () => {
expect(directories).toEqual({
cacheDirectoryPath: './.lowdefy/.cache',
outputDirectoryPath: './.lowdefy/build',
});
});
function getDirectories({ baseDirectory, options }) {
const cacheDirectory = path.resolve(baseDirectory, './.lowdefy/.cache');
let outputDirectory;
if (options.outputDirectory) {
outputDirectory = path.resolve(options.outputDirectory);
} else {
outputDirectory = path.resolve(baseDirectory, './.lowdefy/build');
}
return { cacheDirectory, outputDirectory };
}
export default getDirectories;

View File

@ -0,0 +1,39 @@
/*
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 getDirectories from './getDirectories';
test('default directories', () => {
const { cacheDirectory, outputDirectory } = getDirectories({
baseDirectory: '/test/base',
options: {},
});
expect(cacheDirectory).toEqual('/test/base/.lowdefy/.cache');
expect(outputDirectory).toEqual('/test/base/.lowdefy/build');
});
test('specify outputDirectory in options', () => {
const { cacheDirectory, outputDirectory } = getDirectories({
baseDirectory: '/test/base',
options: {
outputDirectory: '/test/build',
},
});
expect(cacheDirectory).toEqual('/test/base/.lowdefy/.cache');
expect(outputDirectory).toEqual('/test/build');
});

View File

@ -18,14 +18,16 @@ import path from 'path';
import { get, type } from '@lowdefy/helpers';
import { readFile } from '@lowdefy/node-utils';
import YAML from 'js-yaml';
import getCliJson from './getCliJson';
async function getConfig(context) {
const lowdefyYaml = await readFile(path.resolve(context.baseDirectory, 'lowdefy.yaml'));
async function getLowdefyYaml({ baseDirectory, command }) {
const lowdefyYaml = await readFile(path.resolve(baseDirectory, 'lowdefy.yaml'));
if (!lowdefyYaml) {
throw new Error(
`Could not find "lowdefy.yaml" file in specified base directory ${context.baseDirectory}.`
);
if (!['init', 'clean-cache'].includes(command)) {
throw new Error(
`Could not find "lowdefy.yaml" file in specified base directory ${baseDirectory}.`
);
}
return { cliConfig: {} };
}
let lowdefy;
try {
@ -45,12 +47,10 @@ async function getConfig(context) {
)}.`
);
}
const { appId } = await getCliJson(context);
return {
appId,
lowdefyVersion: lowdefy.lowdefy,
disableTelemetry: get(lowdefy, 'cli.disableTelemetry'),
cliConfig: get(lowdefy, 'cli', { default: {} }),
};
}
export default getConfig;
export default getLowdefyYaml;

View File

@ -16,11 +16,7 @@
import path from 'path';
import { readFile } from '@lowdefy/node-utils';
import getConfig from './getConfig';
// eslint-disable-next-line no-unused-vars
import getCliJson from './getCliJson';
jest.mock('./getCliJson', () => () => ({ appId: 'appId' }));
import getLowdefyYaml from './getLowdefyYaml';
jest.mock('@lowdefy/node-utils', () => {
const readFile = jest.fn();
@ -46,8 +42,8 @@ test('get version from yaml file', async () => {
}
return null;
});
const config = await getConfig({ baseDirectory });
expect(config).toEqual({ lowdefyVersion: '1.0.0', appId: 'appId' });
const config = await getLowdefyYaml({ baseDirectory });
expect(config).toEqual({ lowdefyVersion: '1.0.0', cliConfig: {} });
});
test('get version from yaml file, base dir specified', async () => {
@ -59,8 +55,8 @@ test('get version from yaml file, base dir specified', async () => {
}
return null;
});
const config = await getConfig({ baseDirectory: path.resolve(process.cwd(), './baseDir') });
expect(config).toEqual({ lowdefyVersion: '1.0.0', appId: 'appId' });
const config = await getLowdefyYaml({ baseDirectory: path.resolve(process.cwd(), './baseDir') });
expect(config).toEqual({ lowdefyVersion: '1.0.0', cliConfig: {} });
});
test('could not find lowdefy.yaml in cwd', async () => {
@ -72,7 +68,7 @@ test('could not find lowdefy.yaml in cwd', async () => {
lowdefy: 1.0.0
`;
});
await expect(getConfig({ baseDirectory })).rejects.toThrow(
await expect(getLowdefyYaml({ baseDirectory })).rejects.toThrow(
'Could not find "lowdefy.yaml" file in specified base directory'
);
});
@ -87,7 +83,7 @@ test('could not find lowdefy.yaml in base dir', async () => {
`;
});
await expect(
getConfig({ baseDirectory: path.resolve(process.cwd(), './baseDir') })
getLowdefyYaml({ baseDirectory: path.resolve(process.cwd(), './baseDir') })
).rejects.toThrow('Could not find "lowdefy.yaml" file in specified base directory');
});
@ -102,7 +98,7 @@ test('lowdefy.yaml is invalid yaml', async () => {
}
return null;
});
await expect(getConfig({ baseDirectory })).rejects.toThrow(
await expect(getLowdefyYaml({ baseDirectory })).rejects.toThrow(
'Could not parse "lowdefy.yaml" file. Received error '
);
});
@ -118,7 +114,7 @@ test('No version specified', async () => {
}
return null;
});
await expect(getConfig({ baseDirectory })).rejects.toThrow(
await expect(getLowdefyYaml({ baseDirectory })).rejects.toThrow(
'No version specified in "lowdefy.yaml" file. Specify a version in the "lowdefy" field.'
);
});
@ -132,7 +128,7 @@ test('Version is not a string', async () => {
}
return null;
});
await expect(getConfig({ baseDirectory })).rejects.toThrow(
await expect(getLowdefyYaml({ baseDirectory })).rejects.toThrow(
'Version number specified in "lowdefy.yaml" file is not valid. Received 1.'
);
});
@ -146,22 +142,39 @@ test('Version is not a valid version number', async () => {
}
return null;
});
await expect(getConfig({ baseDirectory })).rejects.toThrow(
await expect(getLowdefyYaml({ baseDirectory })).rejects.toThrow(
'Version number specified in "lowdefy.yaml" file is not valid. Received "v1-0-3".'
);
});
test('get disabled telemetry', async () => {
test('get cliConfig', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
lowdefy: 1.0.0
cli:
disableTelemetry: true
watch:
- a
`;
}
return null;
});
const config = await getConfig({ baseDirectory });
expect(config).toEqual({ lowdefyVersion: '1.0.0', disableTelemetry: true, appId: 'appId' });
const config = await getLowdefyYaml({ baseDirectory });
expect(config).toEqual({
lowdefyVersion: '1.0.0',
cliConfig: { disableTelemetry: true, watch: ['a'] },
});
});
test('could not find lowdefy.yaml in base dir, command is "init" or "clean-cache"', async () => {
readFile.mockImplementation(() => null);
let config = await getLowdefyYaml({ command: 'init', baseDirectory });
expect(config).toEqual({
cliConfig: {},
});
config = await getLowdefyYaml({ command: 'clean-cache', baseDirectory });
expect(config).toEqual({
cliConfig: {},
});
});

View File

@ -14,7 +14,14 @@
limitations under the License.
*/
const cacheDirectoryPath = './.lowdefy/.cache';
const outputDirectoryPath = './.lowdefy/build';
function getOptions({ commandLineOptions, cliConfig }) {
// commandLineOptions take precedence over config in lowdefy.yaml
const options = {
...cliConfig,
...commandLineOptions,
};
export { cacheDirectoryPath, outputDirectoryPath };
return options;
}
export default getOptions;

View File

@ -15,8 +15,9 @@
*/
import axios from 'axios';
function getSendTelemetry({ appId, cliVersion, command, disableTelemetry, lowdefyVersion }) {
if (disableTelemetry) {
function getSendTelemetry({ appId, cliVersion, command, lowdefyVersion, options }) {
if (options.disableTelemetry) {
return () => {};
}
async function sendTelemetry({ data = {} } = {}) {

View File

@ -29,7 +29,9 @@ test('disable telemetry', async () => {
const sendTelemetry = getSendTelemetry({
appId,
cliVersion,
disableTelemetry: true,
options: {
disableTelemetry: true,
},
lowdefyVersion,
});
await sendTelemetry({ data: { x: 1 } });
@ -41,6 +43,7 @@ test('send telemetry', async () => {
appId,
cliVersion,
lowdefyVersion,
options: {},
});
await sendTelemetry({ data: { x: 1 } });
expect(axios.request.mock.calls).toEqual([
@ -70,6 +73,7 @@ test('send telemetry should not throw', async () => {
appId,
cliVersion,
lowdefyVersion,
options: {},
});
await sendTelemetry({ data: { x: 1 } });
expect(true).toBe(true);

View File

@ -58,9 +58,9 @@ function createBasicPrint() {
// Memoise print so that error handler can get the same spinner object
let print;
function createPrint({ basic } = {}) {
function createPrint() {
if (print) return print;
if (basic) {
if (process.env.CI === 'true') {
print = createBasicPrint();
return print;
}

View File

@ -101,8 +101,11 @@ describe('memoise', () => {
jest.isolateModules(() => {
createPrint = require('./print').default;
});
const print = createPrint({ basic: true });
const realCI = process.env.CI;
process.env.CI = 'true';
const print = createPrint();
expect(print.type).toEqual('basic');
process.env.CI = realCI;
});
});
describe('ora print', () => {

View File

@ -15,12 +15,14 @@
*/
import errorHandler from './errorHandler';
import startUp from './startUp';
function runCommand(fn) {
async function run(options) {
async function run(options, command) {
const context = {};
try {
const res = await fn({ context, options });
await startUp({ context, options, command });
const res = await fn({ context });
return res;
} catch (error) {
await errorHandler({ context, error });

View File

@ -16,8 +16,10 @@
import errorHandler from './errorHandler';
import runCommand from './runCommand';
import startUp from './startUp';
jest.mock('./errorHandler');
jest.mock('./startUp');
async function wait(ms) {
return new Promise((resolve) => {
@ -29,10 +31,15 @@ beforeEach(() => {
errorHandler.mockReset();
});
const options = { option: true };
const command = {
command: true,
};
test('runCommand with synchronous function', async () => {
const fn = jest.fn(() => 1 + 1);
const wrapped = runCommand(fn);
const res = await wrapped();
const res = await wrapped(options, command);
expect(res).toBe(2);
expect(fn).toHaveBeenCalled();
});
@ -43,16 +50,79 @@ test('runCommand with asynchronous function', async () => {
return 4;
});
const wrapped = runCommand(fn);
const res = await wrapped();
const res = await wrapped(options, command);
expect(res).toBe(4);
expect(fn).toHaveBeenCalled();
});
test('Pass options and context to function', async () => {
test('runCommand calls startUp', async () => {
const fn = jest.fn((...args) => args);
const wrapped = runCommand(fn);
const res = await wrapped({ options: true });
expect(res).toEqual([{ options: { options: true }, context: {} }]);
const res = await wrapped(options, command);
expect(res).toMatchInlineSnapshot(`
Array [
Object {
"context": Object {
"appId": "appId",
"baseDirectory": "baseDirectory",
"cacheDirectory": "baseDirectory/cacheDirectory",
"cliConfig": Object {},
"cliVersion": "cliVersion",
"command": "test",
"commandLineOptions": Object {
"option": true,
},
"lowdefyVersion": "lowdefyVersion",
"options": Object {
"option": true,
},
"outputDirectory": "baseDirectory/outputDirectory",
"print": Object {
"info": [MockFunction],
"log": [MockFunction],
"succeed": [MockFunction],
},
"sendTelemetry": [MockFunction],
},
},
]
`);
expect(startUp.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"command": Object {
"command": true,
},
"context": Object {
"appId": "appId",
"baseDirectory": "baseDirectory",
"cacheDirectory": "baseDirectory/cacheDirectory",
"cliConfig": Object {},
"cliVersion": "cliVersion",
"command": "test",
"commandLineOptions": Object {
"option": true,
},
"lowdefyVersion": "lowdefyVersion",
"options": Object {
"option": true,
},
"outputDirectory": "baseDirectory/outputDirectory",
"print": Object {
"info": [MockFunction],
"log": [MockFunction],
"succeed": [MockFunction],
},
"sendTelemetry": [MockFunction],
},
"options": Object {
"option": true,
},
},
],
]
`);
});
test('Catch error synchronous function', async () => {
@ -60,16 +130,39 @@ test('Catch error synchronous function', async () => {
throw new Error('Error');
});
const wrapped = runCommand(fn);
await wrapped();
await wrapped(options, command);
expect(fn).toHaveBeenCalled();
expect(errorHandler.mock.calls).toEqual([
[
{
context: {},
error: new Error('Error'),
},
],
]);
expect(errorHandler.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"context": Object {
"appId": "appId",
"baseDirectory": "baseDirectory",
"cacheDirectory": "baseDirectory/cacheDirectory",
"cliConfig": Object {},
"cliVersion": "cliVersion",
"command": "test",
"commandLineOptions": Object {
"option": true,
},
"lowdefyVersion": "lowdefyVersion",
"options": Object {
"option": true,
},
"outputDirectory": "baseDirectory/outputDirectory",
"print": Object {
"info": [MockFunction],
"log": [MockFunction],
"succeed": [MockFunction],
},
"sendTelemetry": [MockFunction],
},
"error": [Error: Error],
},
],
]
`);
});
test('Catch error asynchronous function', async () => {
@ -78,14 +171,37 @@ test('Catch error asynchronous function', async () => {
throw new Error('Async Error');
});
const wrapped = runCommand(fn);
await wrapped();
await wrapped(options, command);
expect(fn).toHaveBeenCalled();
expect(errorHandler.mock.calls).toEqual([
[
{
context: {},
error: new Error('Async Error'),
},
],
]);
expect(errorHandler.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"context": Object {
"appId": "appId",
"baseDirectory": "baseDirectory",
"cacheDirectory": "baseDirectory/cacheDirectory",
"cliConfig": Object {},
"cliVersion": "cliVersion",
"command": "test",
"commandLineOptions": Object {
"option": true,
},
"lowdefyVersion": "lowdefyVersion",
"options": Object {
"option": true,
},
"outputDirectory": "baseDirectory/outputDirectory",
"print": Object {
"info": [MockFunction],
"log": [MockFunction],
"succeed": [MockFunction],
},
"sendTelemetry": [MockFunction],
},
"error": [Error: Async Error],
},
],
]
`);
});

View File

@ -15,43 +15,49 @@
*/
import path from 'path';
import { type } from '@lowdefy/helpers';
import checkForUpdatedVersions from './checkForUpdatedVersions';
import getConfig from './getConfig';
import getCliJson from './getCliJson';
import getDirectories from './getDirectories';
import getLowdefyYaml from './getLowdefyYaml';
import getOptions from './getOptions';
import getSendTelemetry from './getSendTelemetry';
import createPrint from './print';
import { cacheDirectoryPath, outputDirectoryPath } from './directories';
import packageJson from '../../package.json';
const { version: cliVersion } = packageJson;
async function startUp({ context, options = {}, command, lowdefyFileNotRequired }) {
context.command = command;
async function startUp({ context, options = {}, command }) {
context.command = command.name();
context.cliVersion = cliVersion;
context.print = createPrint({
basic: options.basicPrint,
});
context.commandLineOptions = options;
context.print = createPrint();
context.baseDirectory = path.resolve(options.baseDirectory || process.cwd());
context.cacheDirectory = path.resolve(context.baseDirectory, cacheDirectoryPath);
if (options.outputDirectory) {
context.outputDirectory = path.resolve(options.outputDirectory);
} else {
context.outputDirectory = path.resolve(context.baseDirectory, outputDirectoryPath);
}
const { cliConfig, lowdefyVersion } = await getLowdefyYaml(context);
context.cliConfig = cliConfig;
context.lowdefyVersion = lowdefyVersion;
context.blocksServerUrl = options.blocksServerUrl;
const { appId } = await getCliJson(context);
context.appId = appId;
context.options = getOptions(context);
const { cacheDirectory, outputDirectory } = getDirectories(context);
context.cacheDirectory = cacheDirectory;
context.outputDirectory = outputDirectory;
if (!lowdefyFileNotRequired) {
const { appId, disableTelemetry, lowdefyVersion } = await getConfig(context);
context.appId = appId;
context.disableTelemetry = disableTelemetry;
context.lowdefyVersion = lowdefyVersion;
context.print.log(`Running 'lowdefy ${command}'. Lowdefy app version ${lowdefyVersion}.`);
} else {
context.print.log(`Running 'lowdefy ${command}'.`);
}
await checkForUpdatedVersions(context);
context.sendTelemetry = getSendTelemetry(context);
if (type.isNone(lowdefyVersion)) {
context.print.log(`Running 'lowdefy ${context.command}'.`);
} else {
context.print.log(
`Running 'lowdefy ${context.command}'. Lowdefy app version ${lowdefyVersion}.`
);
}
return context;
}

View File

@ -16,22 +16,23 @@
import path from 'path';
import startUp from './startUp';
// eslint-disable-next-line no-unused-vars
import checkForUpdatedVersions from './checkForUpdatedVersions';
// eslint-disable-next-line no-unused-vars
import createPrint from './print';
// eslint-disable-next-line no-unused-vars
import getConfig from './getConfig';
import getLowdefyYaml from './getLowdefyYaml';
// eslint-disable-next-line no-unused-vars
import getCliJson from './getCliJson';
// eslint-disable-next-line no-unused-vars
import getSendTelemetry from './getSendTelemetry';
// eslint-disable-next-line no-unused-vars
import packageJson from '../../package.json';
jest.mock(
'./getConfig',
() => async () =>
Promise.resolve({ appId: 'appId', disableTelemetry: true, lowdefyVersion: 'lowdefyVersion' })
jest.mock('./getLowdefyYaml', () =>
jest.fn(async () =>
Promise.resolve({ cliConfig: { cliConfig: true }, lowdefyVersion: 'lowdefyVersion' })
)
);
jest.mock('./getCliJson', () => async () => Promise.resolve({ appId: 'appId' }));
jest.mock('./print', () => {
const error = jest.fn();
const log = jest.fn();
@ -42,55 +43,72 @@ jest.mock('./print', () => {
});
jest.mock('../../package.json', () => ({ version: 'cliVersion' }));
jest.mock('./getSendTelemetry', () => () => 'sendTelemetry');
jest.mock('./checkForUpdatedVersions', () => () => 'checkForUpdatedVersions');
jest.mock('./checkForUpdatedVersions', () => jest.fn(() => 'checkForUpdatedVersions'));
const print = createPrint();
test('startUp, options undefined', async () => {
const context = {};
await startUp({ context, command: 'command' });
expect(context).toEqual({
appId: 'appId',
baseDirectory: path.resolve(process.cwd()),
cacheDirectory: path.resolve(process.cwd(), './.lowdefy/.cache'),
cliVersion: 'cliVersion',
command: 'command',
disableTelemetry: true,
lowdefyVersion: 'lowdefyVersion',
outputDirectory: path.resolve(process.cwd(), './.lowdefy/build'),
sendTelemetry: 'sendTelemetry',
print,
});
});
const command = {
name: () => 'test',
};
test('startUp, options empty', async () => {
const context = {};
await startUp({ context, options: {}, command: 'command' });
await startUp({ context, options: {}, command });
expect(context).toEqual({
appId: 'appId',
baseDirectory: path.resolve(process.cwd()),
cacheDirectory: path.resolve(process.cwd(), './.lowdefy/.cache'),
cliConfig: { cliConfig: true },
cliVersion: 'cliVersion',
command: 'command',
disableTelemetry: true,
command: 'test',
commandLineOptions: {},
lowdefyVersion: 'lowdefyVersion',
options: { cliConfig: true },
outputDirectory: path.resolve(process.cwd(), './.lowdefy/build'),
sendTelemetry: 'sendTelemetry',
print,
sendTelemetry: 'sendTelemetry',
});
expect(checkForUpdatedVersions).toHaveBeenCalledTimes(1);
expect(print.log.mock.calls).toEqual([
["Running 'lowdefy test'. Lowdefy app version lowdefyVersion."],
]);
});
test('startUp, options undefined', async () => {
const context = {};
await startUp({ context, command });
expect(context).toEqual({
appId: 'appId',
baseDirectory: path.resolve(process.cwd()),
cacheDirectory: path.resolve(process.cwd(), './.lowdefy/.cache'),
cliConfig: { cliConfig: true },
cliVersion: 'cliVersion',
command: 'test',
commandLineOptions: {},
lowdefyVersion: 'lowdefyVersion',
options: { cliConfig: true },
outputDirectory: path.resolve(process.cwd(), './.lowdefy/build'),
print,
sendTelemetry: 'sendTelemetry',
});
});
test('startUp, options baseDirectory', async () => {
const context = {};
await startUp({ context, options: { baseDirectory: './baseDirectory' }, command: 'command' });
await startUp({ context, options: { baseDirectory: './baseDirectory' }, command });
expect(context).toEqual({
appId: 'appId',
baseDirectory: path.resolve(process.cwd(), 'baseDirectory'),
cacheDirectory: path.resolve(process.cwd(), 'baseDirectory/.lowdefy/.cache'),
cliConfig: { cliConfig: true },
cliVersion: 'cliVersion',
command: 'command',
disableTelemetry: true,
command: 'test',
commandLineOptions: { baseDirectory: './baseDirectory' },
lowdefyVersion: 'lowdefyVersion',
options: {
cliConfig: true,
baseDirectory: './baseDirectory',
},
outputDirectory: path.resolve(process.cwd(), 'baseDirectory/.lowdefy/build'),
sendTelemetry: 'sendTelemetry',
print,
@ -99,15 +117,20 @@ test('startUp, options baseDirectory', async () => {
test('startUp, options outputDirectory', async () => {
const context = {};
await startUp({ context, options: { outputDirectory: './outputDirectory' }, command: 'command' });
await startUp({ context, options: { outputDirectory: './outputDirectory' }, command });
expect(context).toEqual({
appId: 'appId',
baseDirectory: path.resolve(process.cwd()),
cacheDirectory: path.resolve(process.cwd(), './.lowdefy/.cache'),
cliConfig: { cliConfig: true },
cliVersion: 'cliVersion',
command: 'command',
disableTelemetry: true,
command: 'test',
commandLineOptions: { outputDirectory: './outputDirectory' },
lowdefyVersion: 'lowdefyVersion',
options: {
cliConfig: true,
outputDirectory: './outputDirectory',
},
outputDirectory: path.resolve(process.cwd(), 'outputDirectory'),
sendTelemetry: 'sendTelemetry',
print,
@ -122,33 +145,50 @@ test('startUp, options baseDirectory and outputDirectory', async () => {
baseDirectory: './baseDirectory',
outputDirectory: './outputDirectory',
},
command: 'command',
command,
});
expect(context).toEqual({
appId: 'appId',
baseDirectory: path.resolve(process.cwd(), 'baseDirectory'),
cacheDirectory: path.resolve(process.cwd(), 'baseDirectory/.lowdefy/.cache'),
cliConfig: { cliConfig: true },
cliVersion: 'cliVersion',
command: 'command',
disableTelemetry: true,
command: 'test',
commandLineOptions: {
baseDirectory: './baseDirectory',
outputDirectory: './outputDirectory',
},
lowdefyVersion: 'lowdefyVersion',
options: {
baseDirectory: './baseDirectory',
cliConfig: true,
outputDirectory: './outputDirectory',
},
outputDirectory: path.resolve(process.cwd(), 'outputDirectory'),
sendTelemetry: 'sendTelemetry',
print,
});
});
test('startUp, lowdefyFileNotRequired true', async () => {
test('startUp, no lowdefyVersion returned', async () => {
getLowdefyYaml.mockImplementationOnce(() => ({ cliConfig: {} }));
const context = {};
await startUp({ context, options: {}, command: 'command', lowdefyFileNotRequired: true });
await startUp({ context, options: {}, command });
expect(context).toEqual({
appId: 'appId',
baseDirectory: path.resolve(process.cwd()),
cacheDirectory: path.resolve(process.cwd(), './.lowdefy/.cache'),
cliConfig: {},
cliVersion: 'cliVersion',
command: 'command',
command: 'test',
commandLineOptions: {},
lowdefyVersion: undefined,
options: {},
outputDirectory: path.resolve(process.cwd(), './.lowdefy/build'),
sendTelemetry: 'sendTelemetry',
print,
sendTelemetry: 'sendTelemetry',
});
expect(checkForUpdatedVersions).toHaveBeenCalledTimes(1);
expect(print.log.mock.calls).toEqual([["Running 'lowdefy test'."]]);
});

View File

@ -52,6 +52,7 @@ _ref:
- `--base-directory <base-directory>`: Change base directory. The default is the current working directory.
- `--blocks-server-url <blocks-server-url>`: The URL from where Lowdefy blocks will be served. See below for more information.
- `--disable-telemetry`: Disable telemetry.
- `--output-directory <output-directory>`: Change the directory to which build artifacts are saved. The default is `<base-directory>/.lowdefy/build`.
## build-netlify
@ -61,12 +62,14 @@ _ref:
- `--base-directory <base-directory>`: Change base directory. The default is the current working directory (The base directory should rather be configured in the Netlify build settings).
- `--blocks-server-url <blocks-server-url>`: The URL from where Lowdefy blocks will be served. See below for more information.
- `--disable-telemetry`: Disable telemetry.
## clean-cache
The Lowdefy CLI caches block metadata, and build and server scripts in the `.lowdefy/cache` directory. These cached files can be removed using the `clean-cache` command.
- `--base-directory <base-directory>`: Change base directory. The default is the current working directory.
- `--disable-telemetry`: Disable telemetry.
## dev
@ -74,6 +77,7 @@ _ref:
- `--base-directory <base-directory>`: Change base directory. The default is the current working directory.
- `--blocks-server-url <blocks-server-url>`: The URL from where Lowdefy blocks will be served. See below for more information.
- `--disable-telemetry`: Disable telemetry.
- `--port <port>`: Change the port the server is hosted at. The default is `3000`.
- `--watch <paths...>`: A list of paths to files or directories that should be watched for changes.
- `--watch-ignore <patterns...>`: A list of paths to files or directories that should be ignored by the file watcher. Globs are supported.
@ -91,13 +95,26 @@ _ref:
lowdefy dev --watch-ignore public/**
```
# Configuration
All the CLI options can either be set as command line options, or the `cli` config object in your `lowdefy.yaml` file. Options set as command line options take precedence over options set in the `lowdefy.yaml` file. The config in the `lowdefy.yaml` cannot be referenced using the `_ref` operator, but need to be set in the file itself.
Options set in the `lowdefy.yaml` should be defined in camelCase. The options that can be set are:
- `blocksServerUrl: string`: The URL from where Lowdefy blocks will be served. See below for more information.
- `disableTelemetry: boolean`: Disable telemetry.
- `outputDirectory: string`: Change the directory to which build artifacts are saved. The default is `<base-directory>/.lowdefy/build`.
- `port: number`: Change the port the server is hosted at. The default is `3000`.
- `watch: string[]`: A list of paths to files or directories that should be watched for changes.
- `watchIgnore: string[]`: A list of paths to files or directories that should be ignored by the file watcher. Globs are supported.
The `--base-directory` option cannot be set from the `lowdefy.yaml` file.
# Telemetry
The CLI collects usage and error information to help us fix bugs, prioritize features, and understand how Lowdefy is being used.
All telemetry can be disabled by setting the `disableTelemetry` flag in `cli` config object in your `lowdefy.yaml` file (this cannot be a reference to another file):
All telemetry can be disabled by setting the `disableTelemetry` flag in `cli` config object in your `lowdefy.yaml` file (this cannot be a reference to another file), or by using the `--disable-telemetry` command line flag.:
###### `lowdefy.yaml`
```yaml