Merge pull request #272 from lowdefy/cli

Cli
This commit is contained in:
Gervwyk 2020-12-07 13:04:24 +02:00 committed by GitHub
commit edc3d288c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 672 additions and 217 deletions

46
.pnp.js generated
View File

@ -3833,6 +3833,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["jest", "npm:26.6.3"],
["js-yaml", "npm:3.14.0"],
["opener", "npm:1.5.2"],
["ora", "npm:5.1.0"],
["react", "npm:17.0.1"],
["react-dom", "virtual:22157ea722f8d6428f1fcf0a6f7f6c7d6b902d9c785256c60a65fe6cd0db76ebccc7c1457ee047df0ba6909ff018e300c4f4957a60f5b670089810dfc417af9b#npm:17.0.1"],
["reload", "npm:3.1.1"],
@ -9012,6 +9013,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["cli-spinners", [
["npm:2.5.0", {
"packageLocation": "./.yarn/cache/cli-spinners-npm-2.5.0-31add52b01-a275c35881.zip/node_modules/cli-spinners/",
"packageDependencies": [
["cli-spinners", "npm:2.5.0"]
],
"linkType": "HARD",
}]
]],
["cli-width", [
["npm:2.2.1", {
"packageLocation": "./.yarn/cache/cli-width-npm-2.2.1-4bdb77393c-f7c830bddc.zip/node_modules/cli-width/",
@ -14835,6 +14845,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["is-interactive", [
["npm:1.0.0", {
"packageLocation": "./.yarn/cache/is-interactive-npm-1.0.0-7ff7c6e04a-d79b435e51.zip/node_modules/is-interactive/",
"packageDependencies": [
["is-interactive", "npm:1.0.0"]
],
"linkType": "HARD",
}]
]],
["is-natural-number", [
["npm:4.0.1", {
"packageLocation": "./.yarn/cache/is-natural-number-npm-4.0.1-b5fd86a31d-8b0f8a5f5c.zip/node_modules/is-natural-number/",
@ -16522,6 +16541,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["log-symbols", [
["npm:4.0.0", {
"packageLocation": "./.yarn/cache/log-symbols-npm-4.0.0-7291c4d053-2cbdb0427d.zip/node_modules/log-symbols/",
"packageDependencies": [
["log-symbols", "npm:4.0.0"],
["chalk", "npm:4.1.0"]
],
"linkType": "HARD",
}]
]],
["loglevel", [
["npm:1.7.1", {
"packageLocation": "./.yarn/cache/loglevel-npm-1.7.1-46e39bd115-abee97e346.zip/node_modules/loglevel/",
@ -18451,6 +18480,23 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD",
}]
]],
["ora", [
["npm:5.1.0", {
"packageLocation": "./.yarn/cache/ora-npm-5.1.0-0f7ce18b2d-53aad8d299.zip/node_modules/ora/",
"packageDependencies": [
["ora", "npm:5.1.0"],
["chalk", "npm:4.1.0"],
["cli-cursor", "npm:3.1.0"],
["cli-spinners", "npm:2.5.0"],
["is-interactive", "npm:1.0.0"],
["log-symbols", "npm:4.0.0"],
["mute-stream", "npm:0.0.8"],
["strip-ansi", "npm:6.0.0"],
["wcwidth", "npm:1.0.1"]
],
"linkType": "HARD",
}]
]],
["original", [
["npm:1.0.2", {
"packageLocation": "./.yarn/cache/original-npm-1.0.2-2250635ba0-6918b9d454.zip/node_modules/original/",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -100,7 +100,6 @@ class RefBuilder {
constructor({ context }) {
this.rootPath = 'lowdefy.yaml';
this.configLoader = context.configLoader;
this.logger = context.logger;
this.refContent = {};
this.MAX_RECURSION_DEPTH = context.MAX_RECURSION_DEPTH || 20;
}

View File

@ -23,8 +23,6 @@ async function testSchema({ components, context }) {
await context.logger.warn('Schema not valid.');
const promises = errors.map((err) => context.logger.warn(formatErrorMessage(err, components)));
await promises;
} else {
await context.logger.log('Schema valid.');
}
}

View File

@ -18,24 +18,21 @@ import testSchema from './testSchema';
import testContext from '../test/testContext';
const mockLogWarn = jest.fn();
const mockLog = jest.fn();
const logger = {
warn: mockLogWarn,
log: mockLog,
};
const context = testContext({ logger });
beforeEach(() => {
mockLogWarn.mockReset();
mockLog.mockReset();
});
test('empty components', async () => {
const components = {};
await testSchema({ components, context });
expect(mockLog.mock.calls).toEqual([['Schema valid.']]);
expect().toBe();
});
test('app schema', async () => {
@ -70,7 +67,7 @@ test('app schema', async () => {
],
};
testSchema({ components, context });
expect(mockLog.mock.calls).toEqual([['Schema valid.']]);
expect().toBe();
});
test('invalid schema', async () => {

View File

@ -20,6 +20,7 @@ function testContext({ artifactSetter, configLoader, logger = {}, metaLoader } =
log: () => {},
warn: () => {},
error: () => {},
succeed: () => {},
};
const context = {

View File

@ -15,16 +15,26 @@
*/
import axios from 'axios';
import { type } from '@lowdefy/helpers';
import { type as typeHelper } from '@lowdefy/helpers';
async function fetchMetaUrl(location) {
if (type.isNone(location)) {
async function fetchMetaUrl({ location, type } = {}) {
if (typeHelper.isNone(location)) {
throw new Error('Failed to fetch meta, location is undefined.');
}
if (!type.isString(location.url)) {
throw new Error('Location url definition should be a string.');
if (!typeHelper.isString(location.url)) {
throw new Error(`Block type ${JSON.stringify(type)} url definition should be a string.`);
}
let res;
try {
res = await axios.get(location.url);
} catch (error) {
if (error.response && error.response.status === 404) {
throw new Error(
`Meta for type ${JSON.stringify(type)} could not be found at ${JSON.stringify(location)}.`
);
}
throw error;
}
const res = await axios.get(location.url);
return res.data;
}

View File

@ -18,12 +18,20 @@
import axios from 'axios';
import fetchMetaUrl from './fetchMetaUrl';
const type = 'Type';
jest.mock('axios', () => {
return {
get: (url) => {
if (url === 'valid-url') {
return Promise.resolve({ data: { key: 'value' } });
}
if (url === '404') {
const error = new Error('Test 404');
error.response = {};
error.response.status = 404;
throw error;
}
throw new Error('Invalid url');
},
};
@ -31,7 +39,10 @@ jest.mock('axios', () => {
test('fetchMetaUrl fetches from url', async () => {
const meta = await fetchMetaUrl({
url: 'valid-url',
type,
location: {
url: 'valid-url',
},
});
expect(meta).toEqual({ key: 'value' });
});
@ -39,17 +50,42 @@ test('fetchMetaUrl fetches from url', async () => {
test('fetchMetaUrl request errors', async () => {
await expect(
fetchMetaUrl({
url: 'invalid-url',
type,
location: {
url: 'invalid-url',
},
})
).rejects.toThrow('Invalid url');
});
test('fetchMetaUrl throws if location is undefined', async () => {
test('fetchMetaUrl throws if args are undefined', async () => {
await expect(fetchMetaUrl()).rejects.toThrow('Failed to fetch meta, location is undefined.');
});
test('fetchMetaUrl throws if location is undefined', async () => {
await expect(fetchMetaUrl({ url: 1 })).rejects.toThrow(
'Location url definition should be a string.'
await expect(fetchMetaUrl({ type })).rejects.toThrow(
'Failed to fetch meta, location is undefined.'
);
});
test('fetchMetaUrl throws if location is not a string', async () => {
await expect(
fetchMetaUrl({
type,
location: {
url: 1,
},
})
).rejects.toThrow('Block type "Type" url definition should be a string.');
});
test('fetchMetaUrl throws if response returns a 404 not found', async () => {
await expect(
fetchMetaUrl({
type,
location: {
url: '404',
},
})
).rejects.toThrow('Meta for type "Type" could not be found at {"url":"404"}.');
});

View File

@ -25,6 +25,7 @@ Steps to fetch meta
- return
*/
import { type as typeHelper } from '@lowdefy/helpers';
import createFetchMetaCache from './fetchMetaCache';
import createWriteMetaCache from './writeMetaCache';
import defaultMetaLocations from './defaultMetaLocations';
@ -38,13 +39,14 @@ function createGetMeta({ types, cacheDirectory }) {
const fetchMetaCache = createFetchMetaCache({ cacheDirectory });
const writeMetaCache = createWriteMetaCache({ cacheDirectory });
async function getMeta(type) {
if (!metaLocations[type]) {
const location = metaLocations[type];
if (!location) {
throw new Error(
`Type ${JSON.stringify(type)} is not defined. Specify type url in types array.`
`Block type ${JSON.stringify(type)} is not defined. Specify type url in types array.`
);
}
let meta;
const location = metaLocations[type];
meta = await fetchMetaCache(location);
if (meta)
@ -52,16 +54,17 @@ function createGetMeta({ types, cacheDirectory }) {
type,
meta,
};
meta = await fetchMetaUrl(location);
if (meta) {
await writeMetaCache({ location, meta });
meta = await fetchMetaUrl({ location, type });
await writeMetaCache({ location, meta });
// TODO: implement Ajv schema check. Use testAjvSchema func from @lowdefy/graphql
if (meta && typeHelper.isString(meta.category) && meta.moduleFederation) {
return {
type,
meta,
};
}
throw new Error(
`Meta for type ${type} could not be found at location ${JSON.stringify(location)}.`
`Block type ${JSON.stringify(type)} has invalid block meta at ${JSON.stringify(location)}.`
);
}

View File

@ -45,6 +45,16 @@ const types = {
},
};
const defaultMeta = {
category: 'input',
moduleFederation: {
module: 'Module',
scope: 'scope',
version: '1.0.0',
remoteEntryUrl: `http://localhost:3002/remoteEntry.js`,
},
};
const getMeta = createGetMeta({ types, cacheDirectory: 'cacheDirectory' });
beforeEach(() => {
@ -56,36 +66,28 @@ beforeEach(() => {
test('getMeta cache returns from cache', async () => {
mockFetchMetaCache.mockImplementation((location) => {
if (location && location.url === 'type1Url') {
return {
key: 'value',
};
return defaultMeta;
}
return null;
});
const res = await getMeta('Type1');
expect(res).toEqual({
type: 'Type1',
meta: {
key: 'value',
},
meta: defaultMeta,
});
});
test('getMeta fetches from url and writes to cache', async () => {
mockFetchMetaUrl.mockImplementation((location) => {
mockFetchMetaUrl.mockImplementation(({ location }) => {
if (location && location.url === 'type1Url') {
return {
key: 'value',
};
return defaultMeta;
}
return null;
});
const res = await getMeta('Type1');
expect(res).toEqual({
type: 'Type1',
meta: {
key: 'value',
},
meta: defaultMeta,
});
expect(mockWriteMetaCache.mock.calls).toEqual([
[
@ -93,9 +95,7 @@ test('getMeta fetches from url and writes to cache', async () => {
location: {
url: 'type1Url',
},
meta: {
key: 'value',
},
meta: defaultMeta,
},
],
]);
@ -103,18 +103,25 @@ test('getMeta fetches from url and writes to cache', async () => {
test('getMeta type not in types', async () => {
await expect(getMeta('Undefined')).rejects.toThrow(
'Type "Undefined" is not defined. Specify type url in types array.'
'Block type "Undefined" is not defined. Specify type url in types array.'
);
});
test('getMeta undefined type', async () => {
await expect(getMeta()).rejects.toThrow(
'Type undefined is not defined. Specify type url in types array.'
'Block type undefined is not defined. Specify type url in types array.'
);
});
test('getMeta meta not found in cache or url', async () => {
await expect(getMeta('Type2')).rejects.toThrow(
'Meta for type Type2 could not be found at location {"url":"type2Url"}.'
'Block type "Type2" has invalid block meta at {"url":"type2Url"}.'
);
});
test('getMeta invalid meta', async () => {
mockFetchMetaUrl.mockImplementation(() => ({ invalidMeta: true }));
await expect(getMeta('Type2')).rejects.toThrow(
'Block type "Type2" has invalid block meta at {"url":"type2Url"}.'
);
});

View File

@ -59,6 +59,7 @@
"inquirer": "7.3.3",
"js-yaml": "3.14.0",
"opener": "1.5.2",
"ora": "5.1.0",
"reload": "3.1.1"
},
"devDependencies": {

View File

@ -27,7 +27,8 @@ async function build(options) {
configDirectory: context.baseDirectory,
outputDirectory: context.outputDirectory,
});
context.print.info(`Build artifacts saved at ${context.outputDirectory}.`);
context.print.log(`Build artifacts saved at ${context.outputDirectory}.`);
context.print.succeed(`Build successful.`);
}
export default build;

View File

@ -20,6 +20,8 @@ import getBuildScript from '../../utils/getBuildScript';
import createContext from '../../utils/context';
const info = jest.fn();
const succeed = jest.fn();
const log = jest.fn();
jest.mock('../../utils/getBuildScript', () => {
const buildScript = jest.fn();
@ -45,6 +47,8 @@ test('build', async () => {
createContext.mockImplementation(() => ({
print: {
info,
succeed,
log,
},
baseDirectory,
cacheDirectory,

View File

@ -23,33 +23,39 @@ import getBuildScript from '../../utils/getBuildScript';
import fetchNpmTarball from '../../utils/fetchNpmTarball';
async function buildNetlify(options) {
if (process.env.NETLIFY === 'true') {
options.basicPrint = true;
}
const context = await createContext(options);
const netlifyDir = path.resolve(context.baseDirectory, './.lowdefy/netlify');
context.print.info('Starting Netlify build.');
context.print.info('Fetching lowdefy netlify server.');
context.print.spin('Fetching Lowdefy Netlify server.');
await fetchNpmTarball({
name: '@lowdefy/server-netlify',
version: context.version,
directory: netlifyDir,
});
context.print.log('Fetched Lowdefy Netlify server.');
context.print.info('npm install production.');
context.print.spin('npm install production.');
let proccessOutput = spawnSync('npm', ['install', '--production', '--legacy-peer-deps'], {
cwd: path.resolve(netlifyDir, 'package'),
});
checkChildProcessError({
context,
proccessOutput,
message: 'Failed to npm install netlify server.',
message: 'Failed to npm install Netlify server.',
});
context.print.info(proccessOutput.stdout.toString('utf8'));
context.print.log(proccessOutput.stdout.toString('utf8'));
context.print.info('Fetching lowdefy build script.');
context.print.spin('Fetching Lowdefy build script.');
await getBuildScript(context);
context.print.log('Fetched Lowdefy build script.');
context.print.info('Starting lowdefy build.');
context.print.spin('Starting Lowdefy build.');
const outputDirectory = path.resolve(netlifyDir, './package/dist/functions/graphql/build');
await context.buildScript({
logger: context.print,
@ -57,20 +63,9 @@ async function buildNetlify(options) {
configDirectory: context.baseDirectory,
outputDirectory,
});
context.print.info(`Build artifacts saved at ${outputDirectory}.`);
context.print.info(`Moving output artifacts.`);
proccessOutput = spawnSync('cp', [
'-r',
path.resolve(netlifyDir, 'package/dist/functions'),
path.resolve('./.lowdefy/functions'),
]);
checkChildProcessError({
context,
proccessOutput,
message: 'Failed to move functions artifacts.',
});
context.print.log(`Build artifacts saved at ${outputDirectory}.`);
context.print.log(`Moving output artifacts.`);
proccessOutput = spawnSync('cp', [
'-r',
path.resolve(netlifyDir, 'package/dist/shell'),
@ -81,6 +76,19 @@ async function buildNetlify(options) {
proccessOutput,
message: 'Failed to move publish artifacts.',
});
context.print.log(`Netlify publish artifacts moved to "./lowdefy/publish".`);
proccessOutput = spawnSync('cp', [
'-r',
path.resolve(netlifyDir, 'package/dist/functions'),
path.resolve('./.lowdefy/functions'),
]);
checkChildProcessError({
context,
proccessOutput,
message: 'Failed to move functions artifacts.',
});
context.print.log(`Netlify functions artifacts moved to "./lowdefy/functions".`);
proccessOutput = spawnSync('cp', [
'-r',
@ -93,7 +101,7 @@ async function buildNetlify(options) {
message: 'Failed to move node_modules.',
});
context.print.info(`Netlify build completed successfully.`);
context.print.succeed(`Netlify build completed successfully.`);
}
export default buildNetlify;

View File

@ -26,9 +26,9 @@ async function cleanCache(program) {
}
const print = createPrint();
const cacheDir = path.resolve(baseDirectory, cacheDirectoryPath);
print.info(`Cleaning cache at "${cacheDir}".`);
print.log(`Cleaning cache at "${cacheDir}".`);
await cleanDirectory(cacheDir);
print.info(`Cache cleaned.`);
print.succeed(`Cache cleaned.`);
}
export default cleanCache;

View File

@ -24,9 +24,11 @@ jest.mock('@lowdefy/node-utils', () => {
});
jest.mock('../../utils/print', () => {
const info = jest.fn();
const log = jest.fn();
const succeed = jest.fn();
return () => ({
info,
log,
succeed,
});
});
@ -40,18 +42,14 @@ test('cleanCache', async () => {
await cleanCache({});
const cachePath = path.resolve(process.cwd(), './.lowdefy/.cache');
expect(cleanDirectory.mock.calls).toEqual([[cachePath]]);
expect(print.info.mock.calls).toEqual([
[`Cleaning cache at "${cachePath}".`],
['Cache cleaned.'],
]);
expect(print.log.mock.calls).toEqual([[`Cleaning cache at "${cachePath}".`]]);
expect(print.succeed.mock.calls).toEqual([['Cache cleaned.']]);
});
test('cleanCache baseDir', async () => {
await cleanCache({ baseDirectory: 'baseDir' });
const cachePath = path.resolve(process.cwd(), 'baseDir/.lowdefy/.cache');
expect(cleanDirectory.mock.calls).toEqual([[cachePath]]);
expect(print.info.mock.calls).toEqual([
[`Cleaning cache at "${cachePath}".`],
['Cache cleaned.'],
]);
expect(print.log.mock.calls).toEqual([[`Cleaning cache at "${cachePath}".`]]);
expect(print.succeed.mock.calls).toEqual([['Cache cleaned.']]);
});

View File

@ -35,7 +35,7 @@ async function dev(options) {
await getBuildScript(context);
await getGraphql(context);
context.print.info('Starting development server.');
context.print.log('Starting Lowdefy development server.');
//Graphql
const config = {
@ -62,13 +62,14 @@ async function dev(options) {
// File watcher
const fn = async () => {
context.print.info('Building configuration.');
context.print.log('Building configuration.');
await context.buildScript({
logger: context.print,
cacheDirectory: context.cacheDirectory,
configDirectory: context.baseDirectory,
outputDirectory: path.resolve(context.baseDirectory, outputDirectoryPath),
});
context.print.succeed('Built succesfully.');
reloadReturned.reload();
};
const batchChanges = new BatchChanges({ fn, context });
@ -83,7 +84,7 @@ async function dev(options) {
// Start server
app.listen(app.get('port'), function () {
context.print.log(`Development server listening on port ${options.port}`);
context.print.info(`Development server listening on port ${options.port}`);
});
opener(`http://localhost:${options.port}`);
}

View File

@ -23,11 +23,13 @@ async function getGraphql(context) {
const cleanVersion = context.version.replace(/[-.]/g, '_');
const cachePath = path.resolve(context.cacheDirectory, `scripts/graphql_${cleanVersion}`);
if (!fs.existsSync(path.resolve(cachePath, 'package/dist/remoteEntry.js'))) {
context.print.spin(`Fetching @lowdefy/graphql@${context.version} to cache.`);
await fetchNpmTarball({
name: '@lowdefy/graphql',
version: context.version,
directory: cachePath,
});
context.print.log(`Fetched @lowdefy/build@${context.version} to cache.`);
}
context.graphql = await loadModule(
path.resolve(cachePath, 'package/dist/moduleFederation'),

View File

@ -27,7 +27,7 @@ dotenv.config({ silent: true });
const { description, version } = packageJson;
program.description(description).version(version, '-v, --version');
program.name('lowdefy').description(description).version(version, '-v, --version');
program
.command('build')
@ -75,6 +75,6 @@ program
'Change base directory. Default is the current working directory.'
)
.passCommandToAction(false)
.action(errorHandler(dev, { stayAlive: true }));
.action(errorHandler(dev));
program.parse(process.argv);

View File

@ -0,0 +1,50 @@
/*
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 checkChildProcessError from './checkChildProcessError';
const mockError = jest.fn();
const context = {
print: {
error: mockError,
},
};
test('output status 0', () => {
checkChildProcessError({ context, proccessOutput: { status: 0 }, message: 'Test Error Message' });
// checkChildProcessError should not throw, expect is so that test passes
expect(true).toBe(true);
});
test('output status 1', () => {
const proccessOutput = {
status: 1,
stderr: Buffer.from('Process error message'),
};
expect(() =>
checkChildProcessError({ context, proccessOutput, message: 'Test Error Message' })
).toThrow('Test Error Message');
expect(mockError.mock.calls).toMatchInlineSnapshot(
[['Process error message']],
`
Array [
Array [
"Process error message",
],
]
`
);
});

View File

@ -20,12 +20,14 @@ import createPrint from './print';
import { cacheDirectoryPath, outputDirectoryPath } from './directories';
async function createContext(options = {}) {
const context = {
baseDirectory: path.resolve(options.baseDirectory || process.cwd()),
print: createPrint({ timestamp: true }),
};
context.cacheDirectory = path.resolve(context.baseDirectory, cacheDirectoryPath);
const context = {};
context.print = createPrint({
basic: options.basicPrint,
});
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 {

View File

@ -29,16 +29,32 @@ async function fetchNpmTarball({ name, version, directory }) {
}
throw error;
}
if (!packageInfo || !packageInfo.data) {
throw new Error(`Package "${name}" could not be found at ${registryUrl}.`);
}
if (!packageInfo.data.versions[version]) {
throw new Error(`Invalid version. "${name}" does not have version "${version}".`);
}
const tarball = await axios.get(packageInfo.data.versions[version].dist.tarball, {
responseType: 'arraybuffer',
});
let tarball;
try {
tarball = await axios.get(packageInfo.data.versions[version].dist.tarball, {
responseType: 'arraybuffer',
});
} catch (error) {
if (error.response && error.response.status === 404) {
throw new Error(
`Package "${name}" tarball could not be found at ${packageInfo.data.versions[version].dist.tarball}.`
);
}
throw error;
}
if (!tarball || !tarball.data) {
/// TODO: Check if user has internet connection.
throw new Error(
`Tarball could not be fetched from "${packageInfo.data.versions[version].dist.tarball}". Check internet connection.`
`Package "${name}" tarball could not be found at ${packageInfo.data.versions[version].dist.tarball}.`
);
}
await decompress(tarball.data, directory, {

View File

@ -0,0 +1,154 @@
/*
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.
*/
/* eslint-disable no-unused-vars */
import axios from 'axios';
import decompress from 'decompress';
import decompressTargz from 'decompress-targz';
import fetchNpmTarball from './fetchNpmTarball';
// TODO: not testing decompress
jest.mock('decompress');
jest.mock('decompress-targz');
const directory = 'directory';
jest.mock('axios', () => {
return {
get: (url) => {
if (url === 'https://registry.npmjs.org/valid-package') {
return Promise.resolve({
data: {
versions: {
'1.0.0': {
dist: {
tarball: 'tarball-url',
},
},
v404: {
dist: {
tarball: 'https://registry.npmjs.org/404',
},
},
noData: {
dist: {
tarball: 'https://registry.npmjs.org/no-data',
},
},
undef: {
dist: {
tarball: 'https://registry.npmjs.org/undefined',
},
},
error: {
dist: {
tarball: 'https://registry.npmjs.org/axios-error',
},
},
},
},
});
}
if (url === 'tarball-url') {
return {
data: Buffer.from('tarball data'),
};
}
if (url === 'https://registry.npmjs.org/404') {
const error = new Error('Test 404');
error.response = {};
error.response.status = 404;
throw error;
}
if (url === 'https://registry.npmjs.org/axios-error') {
throw new Error('Axios error');
}
if (url === 'https://registry.npmjs.org/no-data') {
return {};
}
if (url === 'https://registry.npmjs.org/undefined') {
return;
}
},
};
});
test('valid package and version', async () => {
await fetchNpmTarball({ name: 'valid-package', version: '1.0.0', directory });
expect(true).toBe(true);
});
test('version does not exist', async () => {
await expect(
fetchNpmTarball({ name: 'valid-package', version: 'invalid', directory })
).rejects.toThrow('Invalid version. "valid-package" does not have version "invalid"');
});
test('npm return a 404', async () => {
await expect(fetchNpmTarball({ name: '404', version: '1.0.0', directory })).rejects.toThrow(
'Package "404" could not be found at https://registry.npmjs.org/404.'
);
});
test('axios error', async () => {
await expect(
fetchNpmTarball({ name: 'axios-error', version: '1.0.0', directory })
).rejects.toThrow('Axios error');
});
test('empty response', async () => {
await expect(fetchNpmTarball({ name: 'no-data', version: '1.0.0', directory })).rejects.toThrow(
'Package "no-data" could not be found at https://registry.npmjs.org/no-data.'
);
});
test('undefined response', async () => {
await expect(fetchNpmTarball({ name: 'undefined', version: '1.0.0', directory })).rejects.toThrow(
'Package "undefined" could not be found at https://registry.npmjs.org/undefined.'
);
});
test('tarball 404', async () => {
await expect(
fetchNpmTarball({ name: 'valid-package', version: 'v404', directory })
).rejects.toThrow(
'Package "valid-package" tarball could not be found at https://registry.npmjs.org/404.'
);
});
test('tarball axios error', async () => {
await expect(
fetchNpmTarball({ name: 'valid-package', version: 'error', directory })
).rejects.toThrow('Axios error');
});
test('tarball empty response', async () => {
await expect(
fetchNpmTarball({ name: 'valid-package', version: 'noData', directory })
).rejects.toThrow(
'Package "valid-package" tarball could not be found at https://registry.npmjs.org/no-data.'
);
});
test('tarball undefined response', async () => {
await expect(
fetchNpmTarball({ name: 'valid-package', version: 'undef', directory })
).rejects.toThrow(
'Package "valid-package" tarball could not be found at https://registry.npmjs.org/undefined.'
);
});

View File

@ -23,11 +23,13 @@ async function getBuildScript(context) {
const cleanVersion = context.version.replace(/[-.]/g, '_');
const cachePath = path.resolve(context.cacheDirectory, `scripts/build_${cleanVersion}`);
if (!fs.existsSync(path.resolve(cachePath, 'package/dist/remoteEntry.js'))) {
context.print.spin(`Fetching @lowdefy/build@${context.version} to cache.`);
await fetchNpmTarball({
name: '@lowdefy/build',
version: context.version,
directory: cachePath,
});
context.print.log(`Fetched @lowdefy/build@${context.version} to cache.`);
}
const buildScript = await loadModule(path.resolve(cachePath, 'package/dist'), './build');
context.buildScript = buildScript.default;

View File

@ -14,29 +14,48 @@
limitations under the License.
*/
import ora from 'ora';
import chalk from 'chalk';
const printToTerminal = (color, options = {}) => (text) => {
let message;
if (options.timestamp) {
const time = new Date(Date.now());
const h = time.getHours();
const m = time.getMinutes();
const s = time.getSeconds();
const timeString = `${h > 9 ? '' : '0'}${h}:${m > 9 ? '' : '0'}${m}:${s > 9 ? '' : '0'}${s}`;
message = `${chalk.dim(timeString)} - ${color(text)}`;
} else {
message = color(text);
}
// eslint-disable-next-line no-console
console.log(message);
};
function getTime() {
const time = new Date(Date.now());
const h = time.getHours();
const m = time.getMinutes();
const s = time.getSeconds();
return `${h > 9 ? '' : '0'}${h}:${m > 9 ? '' : '0'}${m}:${s > 9 ? '' : '0'}${s}`;
}
const createPrint = (options) => ({
info: printToTerminal(chalk.blue, options),
log: printToTerminal(chalk.green, options),
warn: printToTerminal(chalk.yellow, options),
error: printToTerminal(chalk.red, options),
});
function createOraPrint() {
const spinner = ora({
spinner: 'random',
prefixText: () => chalk.dim(getTime()),
color: 'blue',
});
return {
error: (text) => spinner.fail(chalk.red(text)),
info: (text) => spinner.info(chalk.blue(text)),
log: (text) => spinner.start(text).stopAndPersist({ symbol: '∙' }),
spin: (text) => spinner.start(text),
succeed: (text) => spinner.succeed(chalk.green(text)),
warn: (text) => spinner.warn(chalk.yellow(text)),
};
}
function createBasicPrint() {
const { error, info, log, warn } = console;
return {
error,
info,
log,
spin: log,
succeed: log,
warn,
};
}
function createPrint({ basic } = {}) {
if (basic) return createBasicPrint();
return createOraPrint();
}
export default createPrint;

View File

@ -14,15 +14,48 @@
limitations under the License.
*/
// eslint-disable-next-line no-unused-vars
import ora from 'ora';
import createPrint from './print';
jest.mock('ora', () => {
const mockOraConstructor = jest.fn();
return mockOraConstructor;
});
const mockOraFail = jest.fn();
const mockOraInfo = jest.fn();
const mockOraStart = jest.fn();
const mockOraStopAndPersist = jest.fn();
const mockOraSucceed = jest.fn();
const mockOraWarn = jest.fn();
ora.mockImplementation(() => ({
fail: mockOraFail,
info: mockOraInfo,
start: mockOraStart,
stopAndPersist: mockOraStopAndPersist,
succeed: mockOraSucceed,
warn: mockOraWarn,
}));
mockOraStart.mockImplementation(() => ({ stopAndPersist: mockOraStopAndPersist }));
// mock console
const mockConsoleError = jest.fn();
const mockConsoleInfo = jest.fn();
const mockConsoleLog = jest.fn();
const mockConsoleWarn = jest.fn();
console.error = mockConsoleError;
console.info = mockConsoleInfo;
console.log = mockConsoleLog;
console.warn = mockConsoleWarn;
// Mock timestamps
const mockGetHours = jest.fn();
const mockGetMinutes = jest.fn();
const mockGetSeconds = jest.fn();
// const realLog = console.log;
// const realNow = Date.now;
console.log = mockConsoleLog;
// eslint-disable-next-line no-global-assign
Date = jest.fn(() => ({
getHours: mockGetHours,
@ -33,7 +66,6 @@ Date = jest.fn(() => ({
Date.now = () => {};
beforeEach(() => {
mockConsoleLog.mockReset();
mockGetHours.mockReset();
mockGetMinutes.mockReset();
mockGetSeconds.mockReset();
@ -44,110 +76,138 @@ beforeEach(() => {
// Date.now = realNow;
// });
test('create print', () => {
const print = createPrint();
expect(print).toMatchInlineSnapshot(`
Object {
"error": [Function],
"info": [Function],
"log": [Function],
"warn": [Function],
}
`);
describe('ora print', () => {
test('create print', () => {
const print = createPrint();
expect(print).toMatchInlineSnapshot(`
Object {
"error": [Function],
"info": [Function],
"log": [Function],
"spin": [Function],
"succeed": [Function],
"warn": [Function],
}
`);
expect(ora.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"color": "blue",
"prefixText": [Function],
"spinner": "random",
},
],
]
`);
});
test('timestamp, digits less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
createPrint();
const prefixTextFn = ora.mock.calls[0][0].prefixText;
const res = prefixTextFn();
expect(res).toEqual('01:02:03');
});
test('timestamp, digits more than 10', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
createPrint();
const prefixTextFn = ora.mock.calls[0][0].prefixText;
const res = prefixTextFn();
expect(res).toEqual('11:22:33');
});
test('print error', () => {
const print = createPrint();
print.error('Test error');
expect(mockOraFail.mock.calls).toEqual([['Test error']]);
});
test('print info', () => {
const print = createPrint();
print.info('Test info');
expect(mockOraInfo.mock.calls).toEqual([['Test info']]);
});
test('print log', () => {
const print = createPrint();
print.log('Test log');
expect(mockOraStart.mock.calls).toEqual([['Test log']]);
expect(mockOraStopAndPersist.mock.calls).toEqual([[{ symbol: '∙' }]]);
});
test('print spin', () => {
const print = createPrint();
print.spin('Test spin');
expect(mockOraStart.mock.calls).toEqual([['Test spin']]);
});
test('print succeed', () => {
const print = createPrint();
print.succeed('Test succeed');
expect(mockOraSucceed.mock.calls).toEqual([['Test succeed']]);
});
test('print warn', () => {
const print = createPrint();
print.warn('Test warn');
expect(mockOraWarn.mock.calls).toEqual([['Test warn']]);
});
});
test('print info', () => {
const print = createPrint();
print.info('Test info');
expect(mockConsoleLog.mock.calls).toEqual([['Test info']]);
});
describe('basic print', () => {
test('create print', () => {
const print = createPrint();
expect(print).toMatchInlineSnapshot(`
Object {
"error": [Function],
"info": [Function],
"log": [Function],
"spin": [Function],
"succeed": [Function],
"warn": [Function],
}
`);
});
test('print log', () => {
const print = createPrint();
print.log('Test log');
expect(mockConsoleLog.mock.calls).toEqual([['Test log']]);
});
test('print error', () => {
const print = createPrint({ basic: true });
print.error('Test error');
expect(mockConsoleError.mock.calls).toEqual([['Test error']]);
});
test('print warn', () => {
const print = createPrint();
print.warn('Test warn');
expect(mockConsoleLog.mock.calls).toEqual([['Test warn']]);
});
test('print info', () => {
const print = createPrint({ basic: true });
print.info('Test info');
expect(mockConsoleInfo.mock.calls).toEqual([['Test info']]);
});
test('print error', () => {
const print = createPrint();
print.error('Test error');
expect(mockConsoleLog.mock.calls).toEqual([['Test error']]);
});
test('print log', () => {
const print = createPrint({ basic: true });
print.log('Test log');
expect(mockConsoleLog.mock.calls).toEqual([['Test log']]);
});
test('print info with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.info('Test info');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test info']]);
});
test('print spin', () => {
const print = createPrint({ basic: true });
print.spin('Test spin');
expect(mockConsoleLog.mock.calls).toEqual([['Test spin']]);
});
test('print log with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.log('Test log');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test log']]);
});
test('print succeed', () => {
const print = createPrint({ basic: true });
print.succeed('Test succeed');
expect(mockConsoleLog.mock.calls).toEqual([['Test succeed']]);
});
test('print warn with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.warn('Test warn');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test warn']]);
});
test('print error with timestamp, less than 10', () => {
mockGetHours.mockImplementation(() => 1);
mockGetMinutes.mockImplementation(() => 2);
mockGetSeconds.mockImplementation(() => 3);
const print = createPrint({ timestamp: true });
print.error('Test error');
expect(mockConsoleLog.mock.calls).toEqual([['01:02:03 - Test error']]);
});
test('print info with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.info('Test info');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test info']]);
});
test('print log with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.log('Test log');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test log']]);
});
test('print warn with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.warn('Test warn');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test warn']]);
});
test('print error with timestamp, two digits', () => {
mockGetHours.mockImplementation(() => 11);
mockGetMinutes.mockImplementation(() => 22);
mockGetSeconds.mockImplementation(() => 33);
const print = createPrint({ timestamp: true });
print.error('Test error');
expect(mockConsoleLog.mock.calls).toEqual([['11:22:33 - Test error']]);
test('print warn', () => {
const print = createPrint({ basic: true });
print.warn('Test warn');
expect(mockConsoleWarn.mock.calls).toEqual([['Test warn']]);
});
});

View File

@ -2995,6 +2995,7 @@ __metadata:
jest: 26.6.3
js-yaml: 3.14.0
opener: 1.5.2
ora: 5.1.0
react: 17.0.1
react-dom: 17.0.1
reload: 3.1.1
@ -6860,6 +6861,13 @@ __metadata:
languageName: node
linkType: hard
"cli-spinners@npm:^2.4.0":
version: 2.5.0
resolution: "cli-spinners@npm:2.5.0"
checksum: a275c3588179de0a07579742e1fedb508caa6840516761dac1f8544886d4aa025fc2d536323ac9c325624349203010e149ca8b0028be239fc45ed3a1c1252677
languageName: node
linkType: hard
"cli-width@npm:^2.0.0":
version: 2.2.1
resolution: "cli-width@npm:2.2.1"
@ -11567,6 +11575,13 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"is-interactive@npm:^1.0.0":
version: 1.0.0
resolution: "is-interactive@npm:1.0.0"
checksum: d79b435e5134ccd60dfe035117b1cddd5c5100e90b2d33428adfe1667e26f0114cc1bc7b3ff84a1b107de8ef27f155e3ecc3bb08c0e502a15c66300b4a45d9e5
languageName: node
linkType: hard
"is-natural-number@npm:^4.0.1":
version: 4.0.1
resolution: "is-natural-number@npm:4.0.1"
@ -13081,6 +13096,15 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"log-symbols@npm:^4.0.0":
version: 4.0.0
resolution: "log-symbols@npm:4.0.0"
dependencies:
chalk: ^4.0.0
checksum: 2cbdb0427d1853f2bd36645bff42aaca200902284f28aadacb3c0fa4c8c43fe6bfb71b5d61ab08b67063d066d7c55b8bf5fbb43b03e4a150dbcdd643e9cd1dbf
languageName: node
linkType: hard
"loglevel@npm:^1.6.7, loglevel@npm:^1.6.8":
version: 1.7.1
resolution: "loglevel@npm:1.7.1"
@ -14856,6 +14880,22 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"ora@npm:5.1.0":
version: 5.1.0
resolution: "ora@npm:5.1.0"
dependencies:
chalk: ^4.1.0
cli-cursor: ^3.1.0
cli-spinners: ^2.4.0
is-interactive: ^1.0.0
log-symbols: ^4.0.0
mute-stream: 0.0.8
strip-ansi: ^6.0.0
wcwidth: ^1.0.1
checksum: 53aad8d2996056eebb8f68ae874d101d079c05a8251ab281734b37c0252955c82f758d649b5757e54f867bbc98549545dd88b061033487af0b598d4da92d1a82
languageName: node
linkType: hard
"original@npm:^1.0.0":
version: 1.0.2
resolution: "original@npm:1.0.2"
@ -19946,7 +19986,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard
"wcwidth@npm:^1.0.0":
"wcwidth@npm:^1.0.0, wcwidth@npm:^1.0.1":
version: 1.0.1
resolution: "wcwidth@npm:1.0.1"
dependencies: