diff --git a/.pnp.js b/.pnp.js index 98f78ef1f..2272c4d4a 100755 --- a/.pnp.js +++ b/.pnp.js @@ -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/", diff --git a/.yarn/cache/cli-spinners-npm-2.5.0-31add52b01-a275c35881.zip b/.yarn/cache/cli-spinners-npm-2.5.0-31add52b01-a275c35881.zip new file mode 100644 index 000000000..3fefd94bd Binary files /dev/null and b/.yarn/cache/cli-spinners-npm-2.5.0-31add52b01-a275c35881.zip differ diff --git a/.yarn/cache/is-interactive-npm-1.0.0-7ff7c6e04a-d79b435e51.zip b/.yarn/cache/is-interactive-npm-1.0.0-7ff7c6e04a-d79b435e51.zip new file mode 100644 index 000000000..10b349514 Binary files /dev/null and b/.yarn/cache/is-interactive-npm-1.0.0-7ff7c6e04a-d79b435e51.zip differ diff --git a/.yarn/cache/log-symbols-npm-4.0.0-7291c4d053-2cbdb0427d.zip b/.yarn/cache/log-symbols-npm-4.0.0-7291c4d053-2cbdb0427d.zip new file mode 100644 index 000000000..44db8388a Binary files /dev/null and b/.yarn/cache/log-symbols-npm-4.0.0-7291c4d053-2cbdb0427d.zip differ diff --git a/.yarn/cache/ora-npm-5.1.0-0f7ce18b2d-53aad8d299.zip b/.yarn/cache/ora-npm-5.1.0-0f7ce18b2d-53aad8d299.zip new file mode 100644 index 000000000..15c2c09e8 Binary files /dev/null and b/.yarn/cache/ora-npm-5.1.0-0f7ce18b2d-53aad8d299.zip differ diff --git a/packages/build/src/build/buildRefs.js b/packages/build/src/build/buildRefs.js index 1379511a8..a7df8d1d4 100644 --- a/packages/build/src/build/buildRefs.js +++ b/packages/build/src/build/buildRefs.js @@ -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; } diff --git a/packages/build/src/build/testSchema.js b/packages/build/src/build/testSchema.js index 4d079a64e..4c57444b6 100644 --- a/packages/build/src/build/testSchema.js +++ b/packages/build/src/build/testSchema.js @@ -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.'); } } diff --git a/packages/build/src/build/testSchema.test.js b/packages/build/src/build/testSchema.test.js index dfb0f47ef..e0f2e6455 100644 --- a/packages/build/src/build/testSchema.test.js +++ b/packages/build/src/build/testSchema.test.js @@ -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 () => { diff --git a/packages/build/src/test/testContext.js b/packages/build/src/test/testContext.js index 09a2716b1..f8a2436fe 100644 --- a/packages/build/src/test/testContext.js +++ b/packages/build/src/test/testContext.js @@ -20,6 +20,7 @@ function testContext({ artifactSetter, configLoader, logger = {}, metaLoader } = log: () => {}, warn: () => {}, error: () => {}, + succeed: () => {}, }; const context = { diff --git a/packages/build/src/utils/meta/fetchMetaUrl.js b/packages/build/src/utils/meta/fetchMetaUrl.js index 86e9576d3..b26c0d37d 100644 --- a/packages/build/src/utils/meta/fetchMetaUrl.js +++ b/packages/build/src/utils/meta/fetchMetaUrl.js @@ -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; } diff --git a/packages/build/src/utils/meta/fetchMetaUrl.test.js b/packages/build/src/utils/meta/fetchMetaUrl.test.js index 6b557b542..dc4a47732 100644 --- a/packages/build/src/utils/meta/fetchMetaUrl.test.js +++ b/packages/build/src/utils/meta/fetchMetaUrl.test.js @@ -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"}.'); +}); diff --git a/packages/build/src/utils/meta/getMeta.js b/packages/build/src/utils/meta/getMeta.js index 0e7f6304a..8928e4417 100644 --- a/packages/build/src/utils/meta/getMeta.js +++ b/packages/build/src/utils/meta/getMeta.js @@ -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)}.` ); } diff --git a/packages/build/src/utils/meta/getMeta.test.js b/packages/build/src/utils/meta/getMeta.test.js index 31dc52704..3aed58c36 100644 --- a/packages/build/src/utils/meta/getMeta.test.js +++ b/packages/build/src/utils/meta/getMeta.test.js @@ -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"}.' ); }); diff --git a/packages/cli/package.json b/packages/cli/package.json index a7fba04b0..6b34883ab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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": { diff --git a/packages/cli/src/commands/build/build.js b/packages/cli/src/commands/build/build.js index 4012f72b6..5e723dbef 100644 --- a/packages/cli/src/commands/build/build.js +++ b/packages/cli/src/commands/build/build.js @@ -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; diff --git a/packages/cli/src/commands/build/build.test.js b/packages/cli/src/commands/build/build.test.js index 6ce8f8713..3efd8026d 100644 --- a/packages/cli/src/commands/build/build.test.js +++ b/packages/cli/src/commands/build/build.test.js @@ -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, diff --git a/packages/cli/src/commands/buildNetlify/buildNetlify.js b/packages/cli/src/commands/buildNetlify/buildNetlify.js index 3b95614e8..96c08e5b3 100644 --- a/packages/cli/src/commands/buildNetlify/buildNetlify.js +++ b/packages/cli/src/commands/buildNetlify/buildNetlify.js @@ -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; diff --git a/packages/cli/src/commands/cleanCache/cleanCache.js b/packages/cli/src/commands/cleanCache/cleanCache.js index 52ee3153a..ddb0d04e5 100644 --- a/packages/cli/src/commands/cleanCache/cleanCache.js +++ b/packages/cli/src/commands/cleanCache/cleanCache.js @@ -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; diff --git a/packages/cli/src/commands/cleanCache/cleanCache.test.js b/packages/cli/src/commands/cleanCache/cleanCache.test.js index 69babb06c..a6936c24f 100644 --- a/packages/cli/src/commands/cleanCache/cleanCache.test.js +++ b/packages/cli/src/commands/cleanCache/cleanCache.test.js @@ -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.']]); }); diff --git a/packages/cli/src/commands/dev/dev.js b/packages/cli/src/commands/dev/dev.js index 8555001c5..e6aacc860 100644 --- a/packages/cli/src/commands/dev/dev.js +++ b/packages/cli/src/commands/dev/dev.js @@ -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}`); } diff --git a/packages/cli/src/commands/dev/getGraphql.js b/packages/cli/src/commands/dev/getGraphql.js index a6ba0bff2..a443c7d0f 100644 --- a/packages/cli/src/commands/dev/getGraphql.js +++ b/packages/cli/src/commands/dev/getGraphql.js @@ -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'), diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 591fffb2c..25796154e 100755 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -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); diff --git a/packages/cli/src/utils/checkChildProcessError.test.js b/packages/cli/src/utils/checkChildProcessError.test.js new file mode 100644 index 000000000..8672bb49e --- /dev/null +++ b/packages/cli/src/utils/checkChildProcessError.test.js @@ -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", + ], + ] + ` + ); +}); diff --git a/packages/cli/src/utils/context.js b/packages/cli/src/utils/context.js index d8bc29501..83b82c3d6 100644 --- a/packages/cli/src/utils/context.js +++ b/packages/cli/src/utils/context.js @@ -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 { diff --git a/packages/cli/src/utils/fetchNpmTarball.js b/packages/cli/src/utils/fetchNpmTarball.js index e79910625..557e08e96 100644 --- a/packages/cli/src/utils/fetchNpmTarball.js +++ b/packages/cli/src/utils/fetchNpmTarball.js @@ -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, { diff --git a/packages/cli/src/utils/fetchNpmTarball.test.js b/packages/cli/src/utils/fetchNpmTarball.test.js new file mode 100644 index 000000000..ee132d02e --- /dev/null +++ b/packages/cli/src/utils/fetchNpmTarball.test.js @@ -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.' + ); +}); diff --git a/packages/cli/src/utils/getBuildScript.js b/packages/cli/src/utils/getBuildScript.js index f2c1ebe53..d29a6a1da 100644 --- a/packages/cli/src/utils/getBuildScript.js +++ b/packages/cli/src/utils/getBuildScript.js @@ -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; diff --git a/packages/cli/src/utils/print.js b/packages/cli/src/utils/print.js index 1441f409c..52156e4e0 100644 --- a/packages/cli/src/utils/print.js +++ b/packages/cli/src/utils/print.js @@ -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; diff --git a/packages/cli/src/utils/print.test.js b/packages/cli/src/utils/print.test.js index 79db09e89..79840ccb5 100644 --- a/packages/cli/src/utils/print.test.js +++ b/packages/cli/src/utils/print.test.js @@ -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']]); + }); }); diff --git a/yarn.lock b/yarn.lock index c0242f8b3..f7cced749 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: