feat(cli): add errorBoundary and getLowdefyVersion utils

This commit is contained in:
Sam Tolmay 2020-10-26 15:56:24 +02:00
parent 88a126029c
commit 519e604771
12 changed files with 349 additions and 19 deletions

1
.pnp.js generated
View File

@ -3713,6 +3713,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["commander", "npm:6.1.0"],
["inquirer", "npm:7.3.3"],
["jest", "npm:26.5.3"],
["js-yaml", "npm:3.14.0"],
["webpack", "virtual:1e43113c7dc84a5d03308bf7ffaf00574d351ca16282af6c6c0b9576804fb03914bdf2200961292f439926b2e537dce172d7529f79013ce51b9f2d56e9cd836b#npm:5.1.3"],
["webpack-cli", "virtual:1e43113c7dc84a5d03308bf7ffaf00574d351ca16282af6c6c0b9576804fb03914bdf2200961292f439926b2e537dce172d7529f79013ce51b9f2d56e9cd836b#npm:4.0.0"]
],

View File

@ -1,14 +0,0 @@
types:
Context:
url: http://localhost:3002/meta/Context.json
Button:
url: http://localhost:3002/meta/Button.json
pages:
- id: '1'
type: Context
blocks:
- id: button
type: Button

View File

@ -16,6 +16,7 @@
function testContext({ artifactSetter, configLoader, logger = {}, metaLoader } = {}) {
const defaultLogger = {
info: () => {},
log: () => {},
warn: () => {},
error: () => {},

View File

@ -46,7 +46,8 @@
"chalk": "4.1.0",
"chokidar": "3.4.2",
"commander": "6.1.0",
"inquirer": "7.3.3"
"inquirer": "7.3.3",
"js-yaml": "3.14.0"
},
"devDependencies": {
"@babel/cli": "7.12.1",

View File

@ -16,13 +16,18 @@
import path from 'path';
import buildScript from '@lowdefy/build';
import createPrint from '../print';
import createPrint from '../../utils/print';
import getLowdefyVersion from '../../utils/getLowdefyVersion';
import errorBoundary from '../../utils/errorBoundary';
function build(program) {
async function build(program) {
let baseDirectory = process.cwd();
if (program.baseDirectory) {
baseDirectory = path.resolve(program.baseDirectory);
}
const version = await getLowdefyVersion(program.baseDirectory);
console.log(version);
buildScript({
logger: createPrint({ timestamp: true }),
cacheDirectory: path.resolve(baseDirectory, '.lowdefy/.cache'),
@ -31,4 +36,4 @@ function build(program) {
});
}
export default build;
export default errorBoundary(build);

View File

@ -16,7 +16,7 @@
import program from 'commander';
import packageJson from '../package.json';
import build from './commands/build';
import build from './commands/build/build.js';
const { description, version } = packageJson;

View File

@ -0,0 +1,35 @@
/*
Copyright 2020 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 createPrint from './print';
function errorBoundary(fn, options = {}) {
async function run(...args) {
try {
const res = await fn(...args);
return res;
} catch (error) {
const print = createPrint();
print.error(error.message);
if (!options.stayAlive) {
process.exit();
}
}
}
return run;
}
export default errorBoundary;

View File

@ -0,0 +1,117 @@
/*
Copyright 2020 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 errorBoundary from './errorBoundary';
import createPrint from './print';
jest.mock('./print', () => {
const error = jest.fn();
return () => ({
error,
});
});
const print = createPrint();
async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
beforeEach(() => {
print.error.mockReset();
});
test('Error boundary with synchronous function', async () => {
const fn = jest.fn(() => 1 + 1);
const wrapped = errorBoundary(fn);
const res = await wrapped();
expect(res).toBe(2);
expect(fn).toHaveBeenCalled();
});
test('Error boundary with asynchronous function', async () => {
const fn = jest.fn(async () => {
await wait(3);
return 4;
});
const wrapped = errorBoundary(fn);
const res = await wrapped();
expect(res).toBe(4);
expect(fn).toHaveBeenCalled();
});
test('Pass args to synchronous function', async () => {
const fn = jest.fn((arg1, arg2) => ({ arg1, arg2 }));
const wrapped = errorBoundary(fn);
const res = await wrapped('1', '2');
expect(res).toEqual({ arg1: '1', arg2: '2' });
});
test('Catch error synchronous function, stay alive', async () => {
const fn = jest.fn(() => {
throw new Error('Error');
});
const wrapped = errorBoundary(fn, { stayAlive: true });
const res = await wrapped();
expect(res).toBe(undefined);
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Error']]);
});
test('Catch error asynchronous function, stay alive', async () => {
const fn = jest.fn(async () => {
await wait(3);
throw new Error('Async Error');
});
const wrapped = errorBoundary(fn, { stayAlive: true });
const res = await wrapped();
expect(res).toBe(undefined);
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Async Error']]);
});
test('Catch error synchronous function, exit process', async () => {
const realExit = process.exit;
const mockExit = jest.fn();
process.exit = mockExit;
const fn = jest.fn(() => {
throw new Error('Error');
});
const wrapped = errorBoundary(fn);
await wrapped();
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Error']]);
expect(mockExit).toHaveBeenCalled();
process.exit = realExit;
});
test('Catch error asynchronous function, exit process', async () => {
const realExit = process.exit;
const mockExit = jest.fn();
process.exit = mockExit;
const fn = jest.fn(async () => {
await wait(3);
throw new Error('Async Error');
});
const wrapped = errorBoundary(fn);
await wrapped();
expect(fn).toHaveBeenCalled();
expect(print.error.mock.calls).toEqual([['Async Error']]);
expect(mockExit).toHaveBeenCalled();
process.exit = realExit;
});

View File

@ -0,0 +1,55 @@
/*
Copyright 2020 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 { type } from '@lowdefy/helpers';
import { readFile } from '@lowdefy/node-utils';
import YAML from 'js-yaml';
async function getLowdefyVersion(baseDirectory) {
const lowdefyYaml = await readFile(path.resolve(baseDirectory || process.cwd(), 'lowdefy.yaml'));
if (!lowdefyYaml) {
if (baseDirectory) {
throw new Error(
`Could not find "lowdefy.yaml" file in specified base directory ${baseDirectory}.`
);
}
throw new Error(
`Could not find "lowdefy.yaml" file in current working directory. Change directory to a Lowdefy project, or specify a base directory.`
);
}
let lowdefy;
try {
lowdefy = YAML.safeLoad(lowdefyYaml);
} catch (error) {
throw new Error(`Could not parse "lowdefy.yaml" file. Received error ${error.message}.`);
}
if (!lowdefy.version) {
throw new Error(
`No version specified in "lowdefy.yaml" file. Specify a version in the "version field".`
);
}
if (!type.isString(lowdefy.version) || !lowdefy.version.match(/\d+\.\d+\.\d+(-\w+\.\d+)?/)) {
throw new Error(
`Version number specified in "lowdefy.yaml" file is not valid. Received ${JSON.stringify(
lowdefy.version
)}.`
);
}
return lowdefy.version;
}
export default getLowdefyVersion;

View File

@ -0,0 +1,128 @@
import path from 'path';
import { readFile } from '@lowdefy/node-utils';
import getLowdefyVersion from './getLowdefyVersion';
jest.mock('@lowdefy/node-utils', () => {
const readFile = jest.fn();
return {
readFile,
};
});
beforeEach(() => {
readFile.mockReset();
});
test('get version from yaml file', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
version: 1.0.0
`;
}
return null;
});
const version = await getLowdefyVersion();
expect(version).toEqual('1.0.0');
});
test('get version from yaml file, base dir specified', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'baseDir/lowdefy.yaml')) {
return `
version: 1.0.0
`;
}
return null;
});
const version = await getLowdefyVersion('./baseDir');
expect(version).toEqual('1.0.0');
});
test('could not find lowdefy.yaml in cwd', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return null;
}
return `
version: 1.0.0
`;
});
await expect(getLowdefyVersion()).rejects.toThrow(
'Could not find "lowdefy.yaml" file in current working directory. Change directory to a Lowdefy project, or specify a base directory.'
);
});
test('could not find lowdefy.yaml in base dir', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'baseDir/lowdefy.yaml')) {
return null;
}
return `
version: 1.0.0
`;
});
await expect(getLowdefyVersion('./baseDir')).rejects.toThrow(
'Could not find "lowdefy.yaml" file in specified base directory'
);
});
test('lowdefy.yaml is invalid yaml', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
version: 1.0.0
- a: a
b: b
`;
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
'Could not parse "lowdefy.yaml" file. Received error '
);
});
test('No version specified', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
pages:
- id: page1
type: Context
`;
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
'No version specified in "lowdefy.yaml" file. Specify a version in the "version field".'
);
});
test('Version is not a string', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
version: 1
`;
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
'Version number specified in "lowdefy.yaml" file is not valid. Received 1.'
);
});
test('Version is not a valid version number', async () => {
readFile.mockImplementation((filePath) => {
if (filePath === path.resolve(process.cwd(), 'lowdefy.yaml')) {
return `
version: v1-0-3
`;
}
return null;
});
await expect(getLowdefyVersion()).rejects.toThrow(
'Version number specified in "lowdefy.yaml" file is not valid. Received "v1-0-3".'
);
});

View File

@ -2873,6 +2873,7 @@ __metadata:
commander: 6.1.0
inquirer: 7.3.3
jest: 26.5.3
js-yaml: 3.14.0
webpack: 5.1.3
webpack-cli: 4.0.0
bin: