From 6f833e34511450849d21042a65a0ed3ad3b1d216 Mon Sep 17 00:00:00 2001 From: mo4islona Date: Wed, 28 Feb 2024 01:48:05 +0400 Subject: [PATCH] feat: use axios and retries --- package.json | 6 +- src/api/api.ts | 94 ++++++++------- src/api/profile.ts | 9 +- src/api/upload.ts | 16 +-- src/command.ts | 12 +- src/commands/deploy.ts | 77 ++++++------- src/commands/init.ts | 2 +- src/rest-client/baseUrl.ts | 3 - src/rest-client/request.ts | 56 --------- src/rest-client/routes/pipeline.ts | 46 -------- src/utils.ts | 178 +---------------------------- src/utils.unit.spec.ts | 62 ---------- yarn.lock | 41 +++++-- 13 files changed, 140 insertions(+), 462 deletions(-) delete mode 100644 src/rest-client/baseUrl.ts delete mode 100644 src/rest-client/request.ts delete mode 100644 src/rest-client/routes/pipeline.ts delete mode 100644 src/utils.unit.spec.ts diff --git a/package.json b/package.json index c38f091..92cdae3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@subsquid/cli", "description": "squid cli tool", - "version": "2.9.0-beta.1", + "version": "2.9.0-beta.2", "license": "GPL-3.0-or-later", "repository": "git@github.com:subsquid/squid-cli.git", "publishConfig": { @@ -76,6 +76,8 @@ "@types/lodash": "^4.14.202", "@types/targz": "^1.0.4", "async-retry": "^1.3.3", + "axios": "^1.6.7", + "axios-retry": "^4.0.0", "blessed-contrib": "^4.11.0", "chalk": "^4.1.2", "cli-select": "^1.1.2", @@ -92,7 +94,6 @@ "lodash": "^4.17.21", "ms": "^2.1.3", "neo-blessed": "^0.2.0", - "node-fetch": "^2.6.7", "pretty-bytes": "^5.6.0", "qs": "^6.11.2", "reblessed": "^0.2.1", @@ -114,7 +115,6 @@ "@types/js-yaml": "^4.0.9", "@types/ms": "^0.7.34", "@types/node": "^20.11.17", - "@types/node-fetch": "^2.6.11", "@types/qs": "^6.9.11", "@types/split2": "^3.2.1", "@typescript-eslint/eslint-plugin": "^6.21.0", diff --git a/src/api/api.ts b/src/api/api.ts index ddd3982..b878e5f 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1,14 +1,23 @@ import path from 'path'; +import axios, { Method } from 'axios'; +import axiosRetry, { IAxiosRetryConfig, isNetworkOrIdempotentRequestError } from 'axios-retry'; import chalk from 'chalk'; import { pickBy } from 'lodash'; -import fetch from 'node-fetch'; -import qs from 'qs'; +import ms from 'ms'; import { getConfig } from '../config'; const API_DEBUG = process.env.API_DEBUG === 'true'; +const DEFAULT_RETRY: IAxiosRetryConfig = { + retries: 10, + retryDelay: axiosRetry.exponentialDelay, + retryCondition: isNetworkOrIdempotentRequestError, +}; + +axiosRetry(axios, DEFAULT_RETRY); + let version = 'unknown'; try { // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -17,7 +26,7 @@ try { export class ApiError extends Error { constructor( - public status: number, + public request: { status: number; method: string; url: string }, public body: { error: string; message?: string; @@ -43,81 +52,78 @@ export async function api({ path, data, query = {}, + headers = {}, auth, responseType = 'json', abortController, + retry, }: { - method: 'get' | 'post' | 'put' | 'delete' | 'patch'; + method: Method; path: string; query?: Record; data?: unknown; + headers?: Record; auth?: { apiUrl: string; credentials: string }; responseType?: 'json' | 'stream'; abortController?: AbortController; + retry?: number; }): Promise<{ body: T }> { const config = auth || getConfig(); - const sanitizedQuery = pickBy(query, (v) => v); - const queryString = Object.keys(sanitizedQuery).length ? `?${qs.stringify(sanitizedQuery)}` : ''; - - const url = !path.startsWith('https') ? `${config.apiUrl}${path}${queryString}` : `${path}${queryString}`; + const started = Date.now(); + // add the API_URL to the path if it's not a full url + const url = !path.startsWith('https') ? `${config.apiUrl}${path}` : path; - const headers = { - 'Content-Type': 'application/json', - authorization: `token ${config.credentials}`, + const finalHeaders = { + authorization: url.startsWith(config.apiUrl) ? `token ${config.credentials}` : null, 'X-CLI-Version': version, + ...headers, }; - if (API_DEBUG) { - console.log( - chalk.dim(new Date().toISOString()), - chalk.cyan`[HTTP REQUEST]`, - chalk.dim(method?.toUpperCase()), - url, - chalk.dim(JSON.stringify({ headers })), - ); - if (data) { - console.log(chalk.dim(JSON.stringify(data))); - } - } - const response = await fetch(url, { + const response = await axios(url, { method, - headers, - body: data ? JSON.stringify(data) : undefined, + headers: finalHeaders, + data, timeout: responseType === 'stream' ? 0 : undefined, + responseType, + params: pickBy(query, (v) => v), signal: abortController ? (abortController.signal as any) : undefined, + validateStatus: () => true, + 'axios-retry': retry + ? { + ...DEFAULT_RETRY, + retries: retry, + } + : undefined, }); - let body; - if (responseType === 'json') { - const rawBody = await response.text(); - try { - body = responseType === 'json' ? JSON.parse(rawBody) : response.body; - } catch (e) { - body = rawBody; - } - } else { - body = response.body; - } - if (API_DEBUG) { console.log( chalk.dim(new Date().toISOString()), - chalk.cyan`[HTTP RESPONSE]`, - url, + chalk.cyan`[${method.toUpperCase()}]`, + response.config.url, chalk.cyan(response.status), + ms(Date.now() - started), chalk.dim(JSON.stringify({ headers: response.headers })), ); - if (body && responseType === 'json') { - console.log(chalk.dim(JSON.stringify(body, null, 2))); + if (response.data && responseType === 'json') { + console.log(chalk.dim(JSON.stringify(response.data))); } } switch (response.status) { case 200: case 201: - return { body }; + case 204: + return { body: response.data }; default: - throw new ApiError(response.status, body as any); + throw new ApiError( + { + method: method.toUpperCase(), + url: response.config.url || 'Unknown URL', + status: response.status, + }, + response.data, + ); } } diff --git a/src/api/profile.ts b/src/api/profile.ts index a8dedef..c940815 100644 --- a/src/api/profile.ts +++ b/src/api/profile.ts @@ -22,7 +22,14 @@ export async function profile({ }); if (!body.payload) { - throw new ApiError(401, { error: 'username is missing' }); + throw new ApiError( + { + status: 401, + method: 'get', + url: '/user', + }, + { error: 'Credentials are missing or invalid' }, + ); } return body.payload; diff --git a/src/api/upload.ts b/src/api/upload.ts index c41080b..ca4937c 100644 --- a/src/api/upload.ts +++ b/src/api/upload.ts @@ -1,9 +1,8 @@ import fs from 'fs'; import FormData from 'form-data'; -import fetch from 'node-fetch'; -import { ApiError } from './api'; +import { api } from './api'; import { getUploadUrl } from './squids'; export async function uploadFile(orgCode: string, path: string): Promise<{ error: string | null; fileUrl?: string }> { @@ -27,19 +26,14 @@ export async function uploadFile(orgCode: string, path: string): Promise<{ error body.append('file', fileStream, { knownLength: size }); - const res = await fetch(uploadUrl, { - method: 'POST', + await api({ + path: uploadUrl, + method: 'post', headers: { ...body.getHeaders(), }, - body, + data: body, }); - if (res.status !== 204) { - throw new ApiError(400, { - error: await res.text(), - }); - } - return { error: null, fileUrl }; } diff --git a/src/command.ts b/src/command.ts index 6db7e82..205d8b0 100644 --- a/src/command.ts +++ b/src/command.ts @@ -30,10 +30,10 @@ export abstract class CliCommand extends Command { } async catch(error: any) { - const { status, body } = error; + const { request, body } = error; if (error instanceof ApiError) { - switch (status) { + switch (request.status) { case 401: return this.error( `Authentication failure. Please obtain a new deployment key at https://app.subsquid.io and follow the instructions`, @@ -47,10 +47,10 @@ export abstract class CliCommand extends Command { } return this.error(body?.error || body?.message || `Validation error ${body}`); case 404: + const url = `${chalk.bold(request.method)} ${chalk.bold(request.url)}`; + return this.error( - `Unknown API endpoint. Check that your are using the latest version of the Squid CLI. Message: ${ - body?.error || body?.message || 'API url not found' - }`, + `Unknown API endpoint ${url}. Check that your are using the latest version of the Squid CLI. If the problem persists, please contact support.`, ); case 405: @@ -64,7 +64,7 @@ export abstract class CliCommand extends Command { [ `Unknown network error occurred`, `==================`, - `Status: ${status}`, + `Status: ${request.status}`, `Body:\n${JSON.stringify(body)}`, ].join('\n'), ); diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 0e1355b..cf85eb0 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,5 +1,5 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import { promisify } from 'util'; import { Args, Flags, ux as CliUx } from '@oclif/core'; @@ -8,6 +8,7 @@ import chalk from 'chalk'; import { globSync } from 'glob'; import ignore from 'ignore'; import inquirer from 'inquirer'; +import prettyBytes from 'pretty-bytes'; import targz from 'targz'; import { deploySquid, uploadFile } from '../api'; @@ -174,9 +175,9 @@ export default class Deploy extends DeployCommand { private async pack({ buildDir, squidDir, archiveName }: { buildDir: string; squidDir: string; archiveName: string }) { CliUx.ux.action.start(`◷ Compressing the squid to ${archiveName} `); - const squidignore = createSquidIgnore(squidDir); + const squidIgnore = createSquidIgnore(squidDir); - if (!hasPackageJson(squidDir) || squidignore?.ignores(PACKAGE_JSON)) { + if (!hasPackageJson(squidDir) || squidIgnore?.ignores(PACKAGE_JSON)) { return this.showError( [ `The ${PACKAGE_JSON} file was not found in the squid directory`, @@ -211,41 +212,18 @@ export default class Deploy extends DeployCommand { src: squidDir, dest: squidArtifact, tar: { - // if squidignore does not exist, we fallback to the old ignore approach - ignore: squidignore - ? (name) => { - const relativePath = path.relative(path.resolve(squidDir), path.resolve(name)); - - if (squidignore.ignores(relativePath)) { - this.log(chalk.dim(`-- ignoring ${relativePath}`)); - return true; - } else { - this.log(chalk.dim(`adding ${relativePath}`)); - filesCount++; - return false; - } - } - : (name) => { - const relativePath = path.relative(path.resolve(squidDir), path.resolve(name)); - - switch (relativePath) { - case 'node_modules': - case 'builds': - case 'lib': - case 'Dockerfile': - // FIXME: .env ? - case '.git': - case '.github': - case '.idea': - this.log(chalk.dim(`-- ignoring ${relativePath}`)); - return true; - default: - this.log(chalk.dim(`adding ${relativePath}`)); - - filesCount++; - return false; - } - }, + ignore: (name) => { + const relativePath = path.relative(path.resolve(squidDir), path.resolve(name)); + + if (squidIgnore.ignores(relativePath)) { + this.log(chalk.dim(`-- ignoring ${relativePath}`)); + return true; + } else { + this.log(chalk.dim(`adding ${relativePath}`)); + filesCount++; + return false; + } + }, }, }); @@ -256,7 +234,9 @@ export default class Deploy extends DeployCommand { ); } - CliUx.ux.action.stop(`${filesCount} file(s) ✔️`); + const squidArtifactStats = fs.statSync(squidArtifact); + + CliUx.ux.action.stop(`${filesCount} files, ${prettyBytes(squidArtifactStats.size)} ✔️`); return squidArtifact; } @@ -290,7 +270,10 @@ function hasLockFile(squidDir: string, lockFile?: string) { } function createSquidIgnore(squidDir: string) { - const ig = ignore(); + const ig = ignore().add( + // default ignore patterns + ['node_modules', '.git'], + ); const ignoreFilePaths = globSync(['.squidignore', '**/.squidignore'], { cwd: squidDir, @@ -298,8 +281,16 @@ function createSquidIgnore(squidDir: string) { posix: true, }); - if (ignoreFilePaths.length === 0) { - return undefined; + if (!ignoreFilePaths.length) { + return ig.add([ + // squid uploaded archives directory + '/builds', + // squid built files + '/lib', + // IDE files + '.idea', + '.vscode', + ]); } for (const ignoreFilePath of ignoreFilePaths) { diff --git a/src/commands/init.ts b/src/commands/init.ts index 618bc54..656534e 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,7 +1,7 @@ import { promises as asyncFs } from 'fs'; import path from 'path'; -import { Args, ux as CliUx, Flags } from '@oclif/core'; +import { Args, Flags, ux as CliUx } from '@oclif/core'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { simpleGit } from 'simple-git'; diff --git a/src/rest-client/baseUrl.ts b/src/rest-client/baseUrl.ts deleted file mode 100644 index 1579c1d..0000000 --- a/src/rest-client/baseUrl.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { getConfigField } from '../config'; - -export const baseUrl = getConfigField('apiUrl'); diff --git a/src/rest-client/request.ts b/src/rest-client/request.ts deleted file mode 100644 index a6d6d10..0000000 --- a/src/rest-client/request.ts +++ /dev/null @@ -1,56 +0,0 @@ -import chalk from 'chalk'; -import * as fetch from 'node-fetch'; - -const debug = process.env.API_DEBUG === 'true'; - -export async function request(apiUrl: string, fetchContext: fetch.RequestInit | undefined): Promise { - const { headers, body, method } = fetchContext || {}; - if (debug) { - console.log( - chalk.cyan`[HTTP REQUEST]`, - chalk.dim(method?.toUpperCase()), - apiUrl, - chalk.dim(JSON.stringify({ headers })), - ); - if (body) { - console.log(chalk.dim(body)); - } - } - const response = await fetch.default(apiUrl, fetchContext); - const responseBody = await response.clone().json(); - if (debug) { - console.log( - chalk.cyan`[HTTP RESPONSE]`, - apiUrl, - chalk.cyan(response.status), - chalk.dim(JSON.stringify({ headers: response.headers })), - ); - if (responseBody) { - console.log(chalk.dim(JSON.stringify(responseBody, null, 2))); - } - } - - if (response.status === 200) { - return response; - } else if (response.status === 401) { - throw new Error( - `Authentication failure. Please obtain a new deployment key at https://app.subsquid.io and follow the instructions`, - ); - } else if (response.status === 400 && responseBody.errors?.length !== 0) { - let validationErrorString = 'An error occurred processing the request:\n'; - for (const error of responseBody.errors) { - for (const constraint of Object.values(error.constraints)) { - validationErrorString += `${constraint}\n`; - } - } - throw new Error(validationErrorString); - } else { - if (response.status < 500) { - throw new Error(responseBody.label || responseBody.message); - } - - throw new Error( - `Squid server error. Please come back later. If the error persists please open an issue at https://github.com/subsquid/squid and report to t.me/HydraDevs`, - ); - } -} diff --git a/src/rest-client/routes/pipeline.ts b/src/rest-client/routes/pipeline.ts deleted file mode 100644 index a3e11d2..0000000 --- a/src/rest-client/routes/pipeline.ts +++ /dev/null @@ -1,46 +0,0 @@ -import qs from 'qs'; - -import { getCreds } from '../../config'; -import { baseUrl } from '../baseUrl'; -import { request } from '../request'; - -export type DeployPipelineResponse = { - id: string; - squidName: string; - version: string; - status: DeployPipelineStatusEnum; - isErrorOccurred: boolean; - logs: string[]; - comment: string; - clientId: number; - updatedAt: number; - createdAt: number; -}; - -export enum DeployPipelineStatusEnum { - CREATED = 'CREATED', - IMAGE_BUILDING = 'IMAGE_BUILDING', - IMAGE_PUSHING = 'IMAGE_PUSHING', - DEPLOYING = 'DEPLOYING', - OK = 'OK', -} - -export async function getDeployPipeline( - squidName: string, - versionName: string, -): Promise { - const apiUrl = `${baseUrl}/client/squid/${squidName}/pipeline`; - const params = qs.stringify({ name: versionName }); - const response = await request(`${apiUrl}?${params}`, { - method: 'get', - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'Content-Type': 'application/json', - authorization: `token ${getCreds()}`, - }, - }); - const responseBody = await response.json(); - if (response.status === 200) { - return responseBody; - } -} diff --git a/src/utils.ts b/src/utils.ts index 86fc6c9..8f47913 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,18 +1,5 @@ -import { existsSync, readFileSync } from 'fs'; - -import { Command, ux as CliUx } from '@oclif/core'; +import { Command } from '@oclif/core'; import { ConfigNotFound, getConfig } from '@subsquid/commands'; -import { dim } from 'chalk'; -import cliSelect from 'cli-select'; -import { parse } from 'dotenv'; -import { DefaultLogFields, LogOptions, RemoteWithRefs, SimpleGit } from 'simple-git'; - -import { streamSquidLogs } from './api'; -import { DeployPipelineStatusEnum, getDeployPipeline } from './rest-client/routes/pipeline'; - -function buildPipelineErrorMessage(text: string, errorMessage: string): string { - return `${text} ${errorMessage ? `: ${errorMessage}` : ''}`; -} export async function getSquidCommands() { try { @@ -26,105 +13,6 @@ export async function getSquidCommands() { } } -export async function pollDeployPipelines({ - orgCode, - squidName, - versionName, - deploymentUrl, - command, - verbose = true, -}: { - orgCode: string; - squidName: string; - versionName: string; - deploymentUrl: string; - command: Command; - verbose?: boolean; -}): Promise { - let lastStatus: string; - - await doUntil( - async () => { - const pipeline = await getDeployPipeline(squidName, versionName); - - const traceDebug = ` - ------ - Please report to t.me/HydraDevs - ${dim('Squid:')} ${squidName} - ${dim('Version:')} ${versionName} - ${dim('Deploy:')} ${pipeline?.id} - `; - let isLogPrinted = false; - - if (!pipeline) return true; - - if (pipeline.status !== lastStatus) { - lastStatus = pipeline.status; - CliUx.ux.action.stop('✔️'); - } - - const printDebug = () => { - if (verbose && !isLogPrinted) { - command.log(dim([...pipeline.logs, pipeline.comment].filter((v) => v).join('\n'))); - isLogPrinted = true; - } - }; - - switch (pipeline?.status) { - case DeployPipelineStatusEnum.CREATED: - CliUx.ux.action.start('◷ Preparing your squid'); - if (pipeline.isErrorOccurred) { - printDebug(); - command.error(buildPipelineErrorMessage(`❌ An error occurred while building the squid`, traceDebug)); - return true; - } - return false; - case DeployPipelineStatusEnum.IMAGE_BUILDING: - CliUx.ux.action.start('◷ Building your squid'); - if (pipeline.isErrorOccurred) { - printDebug(); - command.error(buildPipelineErrorMessage(`❌ An error occurred while building the squid`, traceDebug)); - return true; - } - return false; - case DeployPipelineStatusEnum.IMAGE_PUSHING: - CliUx.ux.action.start('◷ Publishing your squid'); - if (pipeline.isErrorOccurred) { - printDebug(); - command.error(buildPipelineErrorMessage(`❌ An error occurred while publishing the squid`, traceDebug)); - return true; - } - return false; - case DeployPipelineStatusEnum.DEPLOYING: - CliUx.ux.action.start('◷ Deploying your squid'); - if (pipeline.isErrorOccurred) { - printDebug(); - command.error(buildPipelineErrorMessage(`❌ An error occurred while deploying the squid`, traceDebug)); - return true; - } - return false; - case DeployPipelineStatusEnum.OK: - printDebug(); - - command.log(`Squid is running up. Your squid will be shortly available at ${deploymentUrl} `); - - CliUx.ux.action.start(`Streaming logs from the squid`); - - await streamSquidLogs({ orgCode, squidName, versionName, onLog: (l) => command.log(l) }); - - return true; - default: - printDebug(); - command.error( - '❌ An unexpected error occurred. Please report to Discord https://discord.gg/KRvRcBdhEE or SquidDevs https://t.me/HydraDevs', - ); - return true; - } - }, - { pause: 3000 }, - ); -} - export async function doUntil(fn: () => Promise, { pause }: { pause: number }) { while (true) { const done = await fn(); @@ -135,45 +23,6 @@ export async function doUntil(fn: () => Promise, { pause }: { pause: nu } } -export async function buildRemoteUrlFromGit(git: SimpleGit, command: Command): Promise { - let remoteUrl: RemoteWithRefs; - const remotes = await git.getRemotes(true); - if (remotes.length === 0) { - command.error(`The remotes were not found`, { code: '1' }); - } else if (remotes.length === 1) { - remoteUrl = remotes[0]; - } else { - const selected = await cliSelect({ - cleanup: false, - values: remotes.map((remote) => remote.name), - }).catch(() => { - command.error('Canceled', { code: '1' }); - }); - remoteUrl = remotes.find((remote) => remote.name === selected.value) as RemoteWithRefs; - } - await git.listRemote([remoteUrl.name]).catch(() => { - command.error(`Remote url with name ${remoteUrl.name} not exists`, { - code: '1', - }); - }); - const branch = (await git.branch()).current; - const status = await git.status(); - if (status.files && status.files.length) { - command.error(`There are unstaged or uncommitted changes`); - } - await git.fetch(); - const remoteBranchRefs = await git.listRemote([`${remoteUrl.name}`, `${branch}`]); - if (remoteBranchRefs === '') { - command.error(`Remote branch "${remoteUrl.name}/${branch}" not exists`); - } - const localCommit = await git.log(['-n', 1, branch] as LogOptions); - const remoteCommit = await git.log(['-n', 1, `${remoteUrl.name}/${branch}`] as LogOptions); - if (!localCommit.latest || !remoteCommit.latest || localCommit.latest.hash !== remoteCommit.latest.hash) { - command.error(`Head origin commit is not the same as the local origin commit`); - } - return `${remoteUrl.refs.fetch}${remoteUrl.refs.fetch.endsWith('.git') ? '' : '.git'}#${remoteCommit.latest.hash}`; -} - export function parseNameAndVersion( nameAndVersion: string, command: Command, @@ -188,28 +37,3 @@ export function parseNameAndVersion( const versionName = nameAndVersion.split('@')[1]; return { squidName, versionName }; } - -export function getEnv(e: string): Record { - const variable = parse(Buffer.from(e)); - if (Object.keys(variable).length == 0) { - throw new Error(`❌ An error occurred during parsing variable "${e}"`); - } - return variable; -} - -function parseEnvFile(path: string): Record { - if (!existsSync(path)) return {}; - const envFile = parse(readFileSync(path)); - return envFile; -} - -export function parseEnvs(envFlags: string[] | undefined, envFilePath: string | undefined) { - let envs: Record = {}; - - envFlags?.forEach((e: string) => { - envs = { ...envs, ...getEnv(e) }; - }); - - const fileEnvs = envFilePath != undefined ? parseEnvFile(envFilePath) : {}; - return { ...envs, ...fileEnvs }; -} diff --git a/src/utils.unit.spec.ts b/src/utils.unit.spec.ts deleted file mode 100644 index 912fb8f..0000000 --- a/src/utils.unit.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { writeFileSync, unlinkSync } from 'fs'; - -import { getEnv, parseEnvs } from './utils'; - -type EnvTests = [string, Record][]; - -const SPACES = ' '; - -function getSpacesAndCommentsTestValues(): EnvTests { - const values: EnvTests = []; - for (let i = 0; i < 16; i++) { - const b = i.toString(2).padStart(4, '0'); - values.push([ - `${SPACES.repeat(Number(b[3]))}MY_ENV${SPACES.repeat(Number(b[2]))}` + - `=${SPACES.repeat(Number(b[1]))}value${SPACES.repeat(Number(b[0]))}`, - { MY_ENV: 'value' }, - ]); - } - return values; -} - -const spacesAndCommentsTests: EnvTests = getSpacesAndCommentsTestValues(); -const quotesTest: EnvTests = [ - ["MY_ENV='value with spaces'", { MY_ENV: 'value with spaces' }], - ['MY_ENV="value with spaces"', { MY_ENV: 'value with spaces' }], -]; - -test('spaces and comments should not affect the env parsing result', () => { - spacesAndCommentsTests.forEach((v) => { - expect(getEnv(v[0])).toStrictEqual(v[1]); - }); -}); - -test('quotes must define the boundaries of the env value', () => { - quotesTest.forEach((v) => { - expect(getEnv(v[0])).toStrictEqual(v[1]); - }); -}); - -test('env values must parse from file and merge', () => { - const PATH_TO_TEST_FILE = '.env.unit-test'; - const testValues = - 'MY_ENV_1=value_1\n MY_ENV_2 = value_2 \nMY_ENV_3 = value_3\nMY_ENV_4 = value_4\n\n#comment\nMY_ENV_5=value_5 #comment'; - const testResults: Record = { - MY_ENV_1: 'value_1', - MY_ENV_2: 'value_2', - MY_ENV_3: 'value_3', - MY_ENV_4: 'value_4', - MY_ENV_5: 'value_5', - }; - const mergeTestValues: string[] = ['MY_ENV=value']; - const mergeTestResults: Record = { - ...testResults, - MY_ENV: 'value', - }; - writeFileSync(PATH_TO_TEST_FILE, testValues); - const results = parseEnvs(undefined, PATH_TO_TEST_FILE); - const mergeResults = parseEnvs(mergeTestValues, PATH_TO_TEST_FILE); - unlinkSync(PATH_TO_TEST_FILE); - expect(results).toStrictEqual(testResults); - expect(mergeResults).toStrictEqual(mergeTestResults); -}); diff --git a/yarn.lock b/yarn.lock index 298f06f..bcf689f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1127,14 +1127,6 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node-fetch@^2.6.11": - version "2.6.11" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" - integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== - dependencies: - "@types/node" "*" - form-data "^4.0.0" - "@types/node@*": version "18.7.15" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.15.tgz#20ae1ec80c57ee844b469f968a1cd511d4088b29" @@ -1557,6 +1549,22 @@ available-typed-arrays@^1.0.6: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz#ac812d8ce5a6b976d738e1c45f08d0b00bc7d725" integrity sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg== +axios-retry@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-4.0.0.tgz#d5cb8ea1db18e05ce6f08aa5fe8b2663bba48e60" + integrity sha512-F6P4HVGITD/v4z9Lw2mIA24IabTajvpDZmKa6zq/gGwn57wN5j1P3uWrAV0+diqnW6kTM2fTqmWNfgYWGmMuiA== + dependencies: + is-retry-allowed "^2.2.0" + +axios@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -2806,6 +2814,11 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -3467,6 +3480,11 @@ is-retry-allowed@^1.1.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== +is-retry-allowed@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" + integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -4367,7 +4385,7 @@ node-emoji@^1.11.0: dependencies: lodash "^4.17.21" -node-fetch@^2.6.6, node-fetch@^2.6.7: +node-fetch@^2.6.6: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -4801,6 +4819,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"