diff --git a/package-lock.json b/package-lock.json index 3e66c802a..9d64c6b7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@commitlint/cli": "^18.6.0", "@commitlint/config-conventional": "^18.6.0", - "@grnsft/if-core": "^0.0.9", + "@grnsft/if-core": "^0.0.10", "axios": "^1.7.2", "csv-parse": "^5.5.6", "csv-stringify": "^6.4.6", @@ -24,6 +24,7 @@ "zod": "^3.22.4" }, "bin": { + "if-check": "build/check.js", "if-diff": "build/diff.js", "if-env": "build/env.js", "if-run": "build/index.js" @@ -1183,9 +1184,9 @@ } }, "node_modules/@grnsft/if-core": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@grnsft/if-core/-/if-core-0.0.9.tgz", - "integrity": "sha512-F0niYe1j+NfhH+okz5sjP/CD7w/9BeXXe13bHkCsdOnk0WfXrq0DGKwpqh9TiLbKc9f1P3/XuojeFANMQu5nig==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@grnsft/if-core/-/if-core-0.0.10.tgz", + "integrity": "sha512-WHCdr7H/dFO9gT5fbjrthjOU+4RoLZ5P1F84pbGwJiKLmcU7dvYRuNQKDVIQQ7YJfZl76KSaS7sYgqA+QG8Wpw==", "dependencies": { "typescript": "^5.1.6" }, diff --git a/package.json b/package.json index e9f2f2777..0d965de37 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "bin": { "if-diff": "./build/diff.js", "if-run": "./build/index.js", - "if-env": "./build/env.js" + "if-env": "./build/env.js", + "if-check": "./build/check.js" }, "bugs": { "url": "https://github.com/Green-Software-Foundation/if/issues/new?assignees=&labels=feedback&projects=&template=feedback.md&title=Feedback+-+" @@ -17,7 +18,7 @@ "dependencies": { "@commitlint/cli": "^18.6.0", "@commitlint/config-conventional": "^18.6.0", - "@grnsft/if-core": "^0.0.9", + "@grnsft/if-core": "^0.0.10", "axios": "^1.7.2", "csv-parse": "^5.5.6", "csv-stringify": "^6.4.6", @@ -75,6 +76,7 @@ "coverage": "jest --verbose --coverage --testPathPattern=src/__tests__/unit", "fix": "gts fix", "fix:package": "fixpack", + "if-check": "cross-env CURRENT_DIR=$(node -p \"process.env.INIT_CWD\") npx ts-node src/check.ts", "if-diff": "npx ts-node src/diff.ts", "if-env": "cross-env CURRENT_DIR=$(node -p \"process.env.INIT_CWD\") npx ts-node src/env.ts", "if-run": "npx ts-node src/index.ts", diff --git a/src/__mocks__/fs/index.ts b/src/__mocks__/fs/index.ts index 3853ad7ee..6d4f61b8b 100644 --- a/src/__mocks__/fs/index.ts +++ b/src/__mocks__/fs/index.ts @@ -135,3 +135,53 @@ export const stat = async (filePath: string) => { throw new Error('File not found.'); } }; + +export const access = async (directoryPath: string) => { + if (directoryPath === 'true') { + return true; + } else { + throw new Error('Directory not found.'); + } +}; + +export const unlink = async (filePath: string) => { + if (filePath === 'true') { + return; + } else { + throw new Error('File not found.'); + } +}; + +export const readdir = (directoryPath: string) => { + if (directoryPath.includes('mock-empty-directory')) { + return []; + } + + if (directoryPath.includes('mock-directory')) { + return ['file1.yaml', 'file2.yml', 'file3.txt']; + } + + if (directoryPath.includes('mock-sub-directory')) { + return ['subdir/file2.yml', 'file1.yaml']; + } + + return []; +}; + +export const lstat = (filePath: string) => { + if ( + filePath.includes('mock-directory') || + filePath.includes('mock-sub-directory/subdir') + ) { + return { + isDirectory: () => true, + }; + } + + if (filePath.includes('mock-file')) { + return { + isDirectory: () => false, + }; + } + return; +}; diff --git a/src/__mocks__/mock-manifest.yaml b/src/__mocks__/mock-manifest.yaml new file mode 100644 index 000000000..1e38c1b13 --- /dev/null +++ b/src/__mocks__/mock-manifest.yaml @@ -0,0 +1,72 @@ +name: template manifest +description: auto-generated template +tags: null +initialize: + plugins: + memory-energy-from-memory-util: + path: builtin + method: Coefficient + global-config: + input-parameter: memory/utilization + coefficient: 0.0001 + output-parameter: memory/energy + outputs: + - yaml +execution: + command: >- + /Users/manushak/.npm/_npx/1bf7c3c15bf47d04/node_modules/.bin/ts-node + /Users/manushak/Documents/Projects/Green-Software/if/src/index.ts -m + ./src/env-template.yml -o ./manifests/outputs/template + environment: + if-version: 0.4.0 + os: macOS + os-version: 13.6.6 + node-version: 20.12.2 + date-time: 2024-06-18T08:39:55.771Z (UTC) + dependencies: + - "@babel/core@7.22.10" + - "@babel/preset-typescript@7.23.3" + - "@commitlint/cli@18.6.0" + - "@commitlint/config-conventional@18.6.0" + - "@grnsft/if-core@0.0.3" + - "@jest/globals@29.7.0" + - "@types/jest@29.5.8" + - "@types/js-yaml@4.0.9" + - "@types/luxon@3.4.2" + - "@types/node@20.9.0" + - axios-mock-adapter@1.22.0 + - axios@1.7.2 + - cross-env@7.0.3 + - csv-parse@5.5.6 + - csv-stringify@6.4.6 + - fixpack@4.0.0 + - gts@5.2.0 + - husky@8.0.3 + - jest@29.7.0 + - js-yaml@4.1.0 + - lint-staged@15.2.2 + - luxon@3.4.4 + - release-it@16.3.0 + - rimraf@5.0.5 + - ts-command-line-args@2.5.1 + - ts-jest@29.1.1 + - typescript-cubic-spline@1.0.1 + - typescript@5.2.2 + - winston@3.11.0 + - zod@3.22.4 + status: success +tree: + children: + child: + pipeline: + - memory-energy-from-memory-util + config: null + inputs: + - timestamp: 2023-12-12T00:00:00.000Z + duration: 3600 + memory/utilization: 10 + outputs: + - timestamp: 2023-12-12T00:00:00.000Z + duration: 3600 + memory/utilization: 10 + memory/energy: 0.001 diff --git a/src/__tests__/unit/util/args.test.ts b/src/__tests__/unit/util/args.test.ts index 03752acc4..f4238cbce 100644 --- a/src/__tests__/unit/util/args.test.ts +++ b/src/__tests__/unit/util/args.test.ts @@ -7,6 +7,12 @@ jest.mock('../../../util/fs', () => ({ } return false; }, + isDirectoryExists: () => { + if (process.env.directoryExists === 'true') { + return true; + } + return false; + }, })); jest.mock('ts-command-line-args', () => ({ @@ -70,8 +76,6 @@ jest.mock('ts-command-line-args', () => ({ case 'diff-throw': throw 'mock-error'; /** If-env mocks */ - // case 'env-manifest-is-missing': - // return; case 'manifest-install-provided': return { install: true, @@ -85,6 +89,13 @@ jest.mock('ts-command-line-args', () => ({ throw new Error('mock-error'); case 'env-throw': throw 'mock-error'; + /** If-check */ + case 'manifest-is-provided': + return {manifest: 'mock-manifest.yaml'}; + case 'directory-is-provided': + return {directory: '/mock-directory'}; + case 'flags-are-not-provided': + return {manifest: undefined, directory: undefined}; default: return { manifest: 'mock-manifest.yaml', @@ -99,13 +110,19 @@ import {ERRORS} from '@grnsft/if-core/utils'; import { parseIEProcessArgs, + parseIfCheckArgs, parseIfDiffArgs, parseIfEnvArgs, } from '../../../util/args'; import {STRINGS} from '../../../config'; -const {CliSourceFileError, ParseCliParamsError} = ERRORS; +const { + CliSourceFileError, + ParseCliParamsError, + InvalidDirectoryError, + MissingCliFlagsError, +} = ERRORS; const { MANIFEST_IS_MISSING, @@ -113,6 +130,8 @@ const { INVALID_TARGET, SOURCE_IS_NOT_YAML, MANIFEST_NOT_FOUND, + DIRECTORY_NOT_FOUND, + IF_CHECK_FLAGS_MISSING, } = STRINGS; describe('util/args: ', () => { @@ -402,5 +421,102 @@ describe('util/args: ', () => { }); }); + describe('parseIfCheckArgs(): ', () => { + it('executes when `manifest` is provided.', async () => { + process.env.fileExists = 'true'; + process.env.result = 'manifest-is-provided'; + const response = await parseIfCheckArgs(); + + expect.assertions(1); + + expect(response).toEqual({manifest: 'mock-manifest.yaml'}); + }); + + it('executes when the `directory` is provided.', async () => { + process.env.directoryExists = 'true'; + process.env.result = 'directory-is-provided'; + + const response = await parseIfCheckArgs(); + + expect.assertions(1); + + expect(response).toEqual({directory: '/mock-directory'}); + }); + + it('throws an error when the `directory` does not exist.', async () => { + process.env.directoryExists = 'false'; + process.env.result = 'directory-is-provided'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + expect(error).toEqual(new InvalidDirectoryError(DIRECTORY_NOT_FOUND)); + } + }); + + it('throws an error when both `manifest` and `directory` flags are not provided.', async () => { + process.env.result = 'flags-are-not-provided'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + expect(error).toEqual(new MissingCliFlagsError(IF_CHECK_FLAGS_MISSING)); + } + }); + + it('throws an error if `manifest` is not a yaml.', async () => { + process.env.fileExists = 'true'; + process.env.result = 'manifest-is-not-yaml'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new CliSourceFileError(SOURCE_IS_NOT_YAML)); + } + } + }); + + it('throws an error if `manifest` path is invalid.', async () => { + process.env.fileExists = 'false'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new ParseCliParamsError(MANIFEST_NOT_FOUND)); + } + } + }); + + it('throws an error if parsing failed.', async () => { + process.env.result = 'env-throw-error'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + if (error instanceof Error) { + expect(error).toEqual(new ParseCliParamsError('mock-error')); + } + } + }); + + it('throws error if parsing failed (not instance of error).', async () => { + process.env.result = 'env-throw'; + expect.assertions(1); + + try { + await parseIfCheckArgs(); + } catch (error) { + expect(error).toEqual('mock-error'); + } + }); + }); + process.env = originalEnv; }); diff --git a/src/__tests__/unit/util/fs.test.ts b/src/__tests__/unit/util/fs.test.ts index 82f28bc75..77b3c0370 100644 --- a/src/__tests__/unit/util/fs.test.ts +++ b/src/__tests__/unit/util/fs.test.ts @@ -1,4 +1,12 @@ -import {isFileExists} from '../../../util/fs'; +import * as fs from 'fs/promises'; + +import { + getFileName, + isDirectoryExists, + isFileExists, + getYamlFiles, + removeFileIfExists, +} from '../../../util/fs'; jest.mock('fs/promises', () => require('../../../__mocks__/fs')); @@ -18,4 +26,119 @@ describe('util/fs: ', () => { expect(result).toEqual(false); }); }); + + describe('isDirectoryExists(): ', () => { + it('returns true if directory exists.', async () => { + const result = await isDirectoryExists('true'); + + expect.assertions(1); + expect(result).toEqual(true); + }); + + it('returns false if directory does not exist.', async () => { + const result = await isDirectoryExists('false'); + + expect.assertions(1); + expect(result).toEqual(false); + }); + }); + + describe('getFileName(): ', () => { + it('returns the file name without extension for a file with an extension.', () => { + const filePath = '/path/to/file/example.yaml'; + const result = getFileName(filePath); + + expect.assertions(1); + expect(result).toBe('example'); + }); + + it('returns the file name without extension for a file with multiple dots.', () => { + const filePath = '/path/to/file/example.test.yaml'; + const result = getFileName(filePath); + expect(result).toBe('example.test'); + }); + + it('returns the file name as is if there is no extension.', () => { + const filePath = '/path/to/file/example'; + const result = getFileName(filePath); + expect(result).toBe('example'); + }); + + it('handles file names with special characters.', () => { + const filePath = + '/path/to/file/complex-file.name.with-multiple.parts.yaml'; + const result = getFileName(filePath); + expect(result).toBe('complex-file.name.with-multiple.parts'); + }); + + it('handles file names with no path.', () => { + const filePath = 'example.yaml'; + const result = getFileName(filePath); + expect(result).toBe('example'); + }); + + it('handles empty string as file path.', () => { + const filePath = ''; + const result = getFileName(filePath); + expect(result).toBe(''); + }); + }); + + describe('getYamlFiles(): ', () => { + it('returns an empty array if the directory is empty.', async () => { + const fsReaddirSpy = jest.spyOn(fs, 'readdir'); + const result = await getYamlFiles('/mock-empty-directory'); + + expect(result).toEqual([]); + expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-empty-directory'); + }); + + it('returns YAML files in the directory', async () => { + const fsReaddirSpy = jest.spyOn(fs, 'readdir'); + jest + .spyOn(fs, 'lstat') + .mockResolvedValue({isDirectory: () => false} as any); + + const result = await getYamlFiles('/mock-directory'); + expect.assertions(2); + expect(result).toEqual([ + '/mock-directory/file1.yaml', + '/mock-directory/file2.yml', + ]); + expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-directory'); + }); + + it('recursively finds YAML files in nested directories.', async () => { + const fsReaddirSpy = jest.spyOn(fs, 'readdir'); + jest + .spyOn(fs, 'lstat') + .mockResolvedValue({isDirectory: () => false} as any); + const result = await getYamlFiles('/mock-sub-directory'); + + expect.assertions(2); + expect(result).toEqual([ + '/mock-sub-directory/subdir/file2.yml', + '/mock-sub-directory/file1.yaml', + ]); + expect(fsReaddirSpy).toHaveBeenCalledWith('/mock-directory'); + }); + }); + + describe('removeFileIfExists(): ', () => { + it('successfully delete file if exists.', async () => { + await isFileExists('true'); + const result = await removeFileIfExists('mock-path'); + + expect.assertions(1); + expect(result).toEqual(undefined); + }); + + it('does not throw an error if the file not exists.', async () => { + await isFileExists('false'); + const result = await removeFileIfExists('mock-path'); + + expect.assertions(1); + expect(result).toEqual(undefined); + }); + }); }); diff --git a/src/__tests__/unit/util/helpers.test.ts b/src/__tests__/unit/util/helpers.test.ts index d04ad43d2..7b6bc2eec 100644 --- a/src/__tests__/unit/util/helpers.test.ts +++ b/src/__tests__/unit/util/helpers.test.ts @@ -146,7 +146,7 @@ import { getOptionsFromArgs, addTemplateManifest, initializeAndInstallLibs, - // initializeAndInstallLibs, + logStdoutFailMessage, } from '../../../util/helpers'; import {CONFIG} from '../../../config'; import {Difference} from '../../../types/lib/compare'; @@ -667,4 +667,21 @@ description: mock-description process.exit = originalProcessExit; }); }); + + describe('logStdoutFailMessage(): ', () => { + it('successfully logs the failed message.', () => { + const errorMessage = {stdout: '\n\nmock error message'}; + const mockFilename = 'mock-filename.yaml'; + const logSpy = jest.spyOn(global.console, 'log'); + logStdoutFailMessage(errorMessage, mockFilename); + + expect.assertions(2); + + expect(logSpy).toHaveBeenCalledWith( + `if-check could not verify ${mockFilename}. The re-executed file does not match the original.\n` + ); + + expect(logSpy).toHaveBeenCalledWith('mock error message'); + }); + }); }); diff --git a/src/__tests__/unit/util/npm.test.ts b/src/__tests__/unit/util/npm.test.ts index 403a1138c..b7f028fa4 100644 --- a/src/__tests__/unit/util/npm.test.ts +++ b/src/__tests__/unit/util/npm.test.ts @@ -21,6 +21,7 @@ import { updatePackageJsonDependencies, extractPathsWithVersion, updatePackageJsonProperties, + executeCommands, } from '../../../util/npm'; import {isFileExists} from '../../../util/fs'; @@ -176,7 +177,7 @@ describe('util/npm: ', () => { '@commitlint/cli@18.6.0', '@commitlint/config-conventional@18.6.0', '@grnsft/if-core@0.0.7', - '@grnsft/if-plugins@v0.3.2 extraneous -> file:../../../if-models', + '@grnsft/if-plugins@v0.3.2', '@grnsft/if-unofficial-plugins@v0.3.0 extraneous -> file:../../../if-unofficial-models', '@jest/globals@29.7.0', ]; @@ -246,4 +247,25 @@ describe('util/npm: ', () => { expect(fsReadSpy).toHaveBeenCalledWith(packageJsonPath, 'utf8'); }); }); + + describe('executeCommands(): ', () => { + it('successfully executes with correct commands.', async () => { + const manifest = './src/__mocks__/mock-manifest.yaml'; + const reManifest = 'src/__mocks__/re-mock-manifest.yaml'; + const logSpy = jest.spyOn(global.console, 'log'); + + jest.spyOn(fs, 'unlink').mockResolvedValue(); + + await executeCommands(manifest, false); + + expect.assertions(1); + expect(logSpy).toHaveBeenCalledWith( + 'if-check successfully verified mock-manifest.yaml\n' + ); + + const packageJsonPath = 'src/__mocks__/package.json'; + fsSync.unlink(path.resolve(process.cwd(), reManifest), () => {}); + fsSync.unlink(path.resolve(process.cwd(), packageJsonPath), () => {}); + }, 70000); + }); }); diff --git a/src/check.ts b/src/check.ts new file mode 100644 index 000000000..3d53bc516 --- /dev/null +++ b/src/check.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/* eslint-disable no-process-exit */ +import * as path from 'path'; + +import {logger} from './util/logger'; +import {logStdoutFailMessage} from './util/helpers'; +import {parseIfCheckArgs} from './util/args'; +import {getYamlFiles, removeFileIfExists} from './util/fs'; + +import {STRINGS} from './config'; +import {executeCommands} from './util/npm'; + +const {CHECKING, DIRECTORY_YAML_FILES_NOT_FOUND} = STRINGS; + +const IfCheck = async () => { + const commandArgs = await parseIfCheckArgs(); + + console.log(`${CHECKING}\n`); + + if (commandArgs.manifest) { + const manifest = commandArgs.manifest; + + try { + await executeCommands(manifest, false); + } catch (error: any) { + const fileName = path.basename(manifest); + const executedFile = manifest + .replace(fileName, `re-${fileName}`) + .replace('yml', 'yaml'); + const manifestDirPath = path.dirname(manifest); + + logStdoutFailMessage(error, fileName); + + await removeFileIfExists(`${manifestDirPath}/package.json`); + await removeFileIfExists(executedFile); + } + } else { + const directory = commandArgs.directory; + const files = await getYamlFiles(directory!); + + if (files.length === 0) { + console.log(DIRECTORY_YAML_FILES_NOT_FOUND); + process.exit(1); + } + + for await (const file of files) { + const fileName = path.basename(file); + console.log(fileName); + + try { + await executeCommands(file, true); + } catch (error: any) { + const fileName = path.basename(file); + const executedFile = file + .replace(fileName, `re-${fileName}`) + .replace('yml', 'yaml'); + + logStdoutFailMessage(error, fileName); + + await removeFileIfExists(executedFile); + } + } + } +}; + +IfCheck().catch(error => { + if (error instanceof Error) { + logger.error(error); + process.exit(2); + } +}); diff --git a/src/config/config.ts b/src/config/config.ts index 0f96019f2..b8f676924 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -2,7 +2,12 @@ import {ArgumentConfig, ParseOptions} from 'ts-command-line-args'; import {STRINGS} from './strings'; -import {IFDiffArgs, IEArgs, IFEnvArgs} from '../types/process-args'; +import { + IFDiffArgs, + IEArgs, + IFEnvArgs, + IFCheckArgs, +} from '../types/process-args'; const {DISCLAIMER_MESSAGE} = STRINGS; @@ -120,6 +125,31 @@ export const CONFIG = { 'Faied to create the environment with the template manifest!', FAILURE_MESSAGE_DEPENDENCIES: 'Manifest dependencies are not available!', }, + IF_CHECK: { + ARGS: { + manifest: { + type: String, + optional: true, + alias: 'm', + description: '[path to the manifest file]', + }, + directory: { + type: String, + optional: true, + alias: 'd', + description: '[path to the manifests directory]', + }, + } as ArgumentConfig, + HELP: { + helpArg: 'help', + headerContentSections: [ + {header: 'Impact Framework', content: 'IF-Check Helpful keywords:'}, + ], + footerContentSections: [ + {header: 'Green Software Foundation', content: DISCLAIMER_MESSAGE}, + ], + } as ParseOptions, + }, GITHUB_PATH: 'https://github.com', NATIVE_PLUGIN: 'if-plugins', AGGREGATION_ADDITIONAL_PARAMS: ['timestamp', 'duration'], diff --git a/src/config/strings.ts b/src/config/strings.ts index 2f8cbdfd1..6a8a586f3 100644 --- a/src/config/strings.ts +++ b/src/config/strings.ts @@ -134,6 +134,16 @@ ${error}`, `Exporting to csv file: ${savepath}`, EXPORTING_RAW_CSV_FILE: (savepath: string) => `Exporting raw csv file: ${savepath}`, + CHECKING: 'Checking...', + IF_CHECK_FLAGS_MISSING: + 'Either the `--manifest` or `--directory` command should be provided with a path', + DIRECTORY_NOT_FOUND: 'Directory not found.', + DIRECTORY_YAML_FILES_NOT_FOUND: + 'The directory does not contain any YAML/YML files.\n', + IF_CHECK_FAILED: (filename: string) => + `if-check could not verify ${filename}. The re-executed file does not match the original.\n`, + IF_CHECK_VERIFIED: (filename: string) => + `if-check successfully verified ${filename}\n`, ZERO_DIVISION: (moduleName: string, index: number) => `-- SKIPPING -- DivisionByZero: you are attempting to divide by zero in ${moduleName} plugin : inputs[${index}]\n`, }; diff --git a/src/types/process-args.ts b/src/types/process-args.ts index 40dece54d..0f3799f5a 100644 --- a/src/types/process-args.ts +++ b/src/types/process-args.ts @@ -17,6 +17,11 @@ export interface IFEnvArgs { cwd?: boolean; } +export interface IFCheckArgs { + manifest?: string; + directory?: string; +} + export interface Options { outputPath?: string; stdout?: boolean; diff --git a/src/util/args.ts b/src/util/args.ts index 5d9524390..31da90461 100644 --- a/src/util/args.ts +++ b/src/util/args.ts @@ -5,7 +5,7 @@ import {ERRORS} from '@grnsft/if-core/utils'; import {checkIfFileIsYaml} from './yaml'; -import {isFileExists} from './fs'; +import {isDirectoryExists, isFileExists} from './fs'; import {logger} from './logger'; @@ -16,12 +16,19 @@ import { IEArgs, ProcessArgsOutputs, IFEnvArgs, + IFCheckArgs, } from '../types/process-args'; import {LoadDiffParams} from '../types/util/args'; -const {ParseCliParamsError, CliTargetFileError, CliSourceFileError} = ERRORS; +const { + ParseCliParamsError, + CliTargetFileError, + CliSourceFileError, + InvalidDirectoryError, + MissingCliFlagsError, +} = ERRORS; -const {IE, IF_DIFF, IF_ENV} = CONFIG; +const {IE, IF_DIFF, IF_ENV, IF_CHECK} = CONFIG; const { MANIFEST_IS_MISSING, @@ -30,6 +37,8 @@ const { SOURCE_IS_NOT_YAML, TARGET_IS_NOT_YAML, INVALID_TARGET, + IF_CHECK_FLAGS_MISSING, + DIRECTORY_NOT_FOUND, } = STRINGS; /** @@ -186,3 +195,54 @@ export const parseIfEnvArgs = async () => { return {install, cwd}; }; + +/** -- IF Check -- */ + +/** + * Parses `if-check` process arguments. + */ +const validateAndParseIfCheckArgs = () => { + try { + return parse(IF_CHECK.ARGS, IF_CHECK.HELP); + } catch (error) { + if (error instanceof Error) { + throw new ParseCliParamsError(error.message); + } + + throw error; + } +}; + +/** + * Checks if either `manifest` or `directory` command is provided. + */ +export const parseIfCheckArgs = async () => { + const {manifest, directory} = validateAndParseIfCheckArgs(); + + if (manifest) { + const response = prependFullFilePath(manifest); + const isManifestFileExists = await isFileExists(response); + + if (!isManifestFileExists) { + throw new ParseCliParamsError(MANIFEST_NOT_FOUND); + } + + if (checkIfFileIsYaml(manifest)) { + return {manifest}; + } + + throw new CliSourceFileError(SOURCE_IS_NOT_YAML); + } else if (directory) { + const isDirExists = await isDirectoryExists(directory); + + if (!isDirExists) { + throw new InvalidDirectoryError(DIRECTORY_NOT_FOUND); + } + + const response = prependFullFilePath(directory); + + return {directory: response}; + } + + throw new MissingCliFlagsError(IF_CHECK_FLAGS_MISSING); +}; diff --git a/src/util/fs.ts b/src/util/fs.ts index 70cec75e4..bf10af9ba 100644 --- a/src/util/fs.ts +++ b/src/util/fs.ts @@ -1,4 +1,5 @@ import * as fs from 'fs/promises'; +import * as path from 'path'; /** * Checks if file exists with the given `filePath`. @@ -23,3 +24,45 @@ export const isDirectoryExists = async (directoryPath: string) => { return false; } }; + +/** + * Gets all files that have either .yml or .yaml extension in the given directory. + */ +export const getYamlFiles = async (directory: string) => { + let yamlFiles: string[] = []; + + const files = await fs.readdir(directory); + + for (const file of files) { + const fullPath = path.join(directory, file); + const stat = await fs.lstat(fullPath); + + if (stat.isDirectory()) { + yamlFiles = yamlFiles.concat(await getYamlFiles(fullPath)); + } else { + if (file.endsWith('.yml') || file.endsWith('.yaml')) { + yamlFiles.push(fullPath); + } + } + } + + return yamlFiles; +}; + +/** + * Gets fileName from the given path without an extension. + */ +export const getFileName = (filePath: string) => { + const baseName = path.basename(filePath); + const extension = path.extname(filePath); + return baseName.replace(extension, ''); +}; + +/** + * Removes the given file if exists. + */ +export const removeFileIfExists = async (filePath: string) => { + if (await isFileExists(filePath)) { + await fs.unlink(filePath); + } +}; diff --git a/src/util/helpers.ts b/src/util/helpers.ts index e69b9e0e9..0fcdda963 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -33,7 +33,7 @@ const { FAILURE_MESSAGE_DEPENDENCIES, } = IF_ENV; -const {UNSUPPORTED_ERROR} = STRINGS; +const {UNSUPPORTED_ERROR, IF_CHECK_FAILED} = STRINGS; const {MissingPluginDependenciesError} = ERRORS; /** @@ -268,3 +268,16 @@ export const addTemplateManifest = async (destinationDir: string) => { process.exit(1); } }; + +/** + * Logs the failure message from the stdout of an error. + */ +export const logStdoutFailMessage = (error: any, fileName: string) => { + console.log(IF_CHECK_FAILED(fileName)); + + const stdout = error.stdout; + const logs = stdout.split('\n\n'); + const failMessage = logs[logs.length - 1]; + + console.log(failMessage); +}; diff --git a/src/util/npm.ts b/src/util/npm.ts index 61baf75b8..de27ae4de 100644 --- a/src/util/npm.ts +++ b/src/util/npm.ts @@ -4,7 +4,12 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import {execPromise} from './helpers'; -import {isDirectoryExists, isFileExists} from './fs'; +import { + isDirectoryExists, + getFileName, + isFileExists, + removeFileIfExists, +} from './fs'; import {logger} from './logger'; import {STRINGS} from '../config'; @@ -12,7 +17,8 @@ import {ManifestPlugin, PathWithVersion} from '../types/npm'; const packageJson = require('../../package.json'); -const {INITIALIZING_PACKAGE_JSON, INSTALLING_NPM_PACKAGES} = STRINGS; +const {INITIALIZING_PACKAGE_JSON, INSTALLING_NPM_PACKAGES, IF_CHECK_VERIFIED} = + STRINGS; /** * Checks if the package.json is exists, if not, initializes it. @@ -144,3 +150,44 @@ export const updatePackageJsonProperties = async ( JSON.stringify(newPackageJson, null, 2) ); }; + +/** + * Executes a series of npm commands based on the provided manifest file. + */ +export const executeCommands = async (manifest: string, cwd: boolean) => { + // TODO: After release remove isGlobal and appropriate checks + const isGlobal = !!process.env.npm_config_global; + const manifestDirPath = path.dirname(manifest); + const manifestFileName = getFileName(manifest); + const executedManifest = path.join(manifestDirPath, `re-${manifestFileName}`); + const prefixFlag = + process.env.CURRENT_DIR && process.env.CURRENT_DIR !== process.cwd() + ? `--prefix=${path.relative(process.env.CURRENT_DIR!, process.cwd())}` + : ''; + const ifEnv = `${ + isGlobal ? `if-env ${prefixFlag}` : `npm run if-env ${prefixFlag} --` + } -m ${manifest}`; + const ifEnvCommand = cwd ? `${ifEnv} -c` : ifEnv; + const ifRunCommand = `${ + isGlobal ? `if-run ${prefixFlag}` : `npm run if-run ${prefixFlag} --` + } -m ${manifest} -o ${executedManifest}`; + const ifDiffCommand = `${ + isGlobal ? `if-diff ${prefixFlag}` : `npm run if-diff ${prefixFlag} --` + } -s ${executedManifest}.yaml -t ${manifest}`; + const ttyCommand = " node -p 'Boolean(process.stdout.isTTY)'"; + + await execPromise( + `${ifEnvCommand} && ${ifRunCommand} && ${ttyCommand} | ${ifDiffCommand}`, + { + cwd: process.env.CURRENT_DIR || process.cwd(), + } + ); + + if (!cwd) { + await removeFileIfExists(`${manifestDirPath}/package.json`); + } + + await removeFileIfExists(`${executedManifest}.yaml`); + + console.log(IF_CHECK_VERIFIED(path.basename(manifest))); +};