diff --git a/src/commands/build/build.ts b/src/commands/build/build.ts index 6cde3b45..b05508b5 100644 --- a/src/commands/build/build.ts +++ b/src/commands/build/build.ts @@ -26,10 +26,7 @@ import { compressAndSave } from '../../utils/compressAndSave' import { getMacroFolders, getStreamConfig } from '../../utils/config' import { isSasFile } from '../../utils/file' import { compile } from '../compile/compile' -import { - getCompileTree, - loadDependencies -} from '../compile/internal/loadDependencies' +import { getCompileTree, loadDependencies } from '../compile/internal' import { getBuildInit, getBuildTerm } from './internal/config' import { getLaunchPageCode } from './internal/getLaunchPageCode' diff --git a/src/commands/compile/compile.ts b/src/commands/compile/compile.ts index b5432ca2..67eaa9c9 100644 --- a/src/commands/compile/compile.ts +++ b/src/commands/compile/compile.ts @@ -23,21 +23,19 @@ import { import { isSasFile } from '../../utils/file' import { createWebAppServices } from '../web/web' import * as compileModule from './compile' -import { checkCompileStatus } from './internal/checkCompileStatus' -import { compileFile } from './internal/compileFile' -import { getAllFolders, SasFileType } from './internal/getAllFolders' - import { + checkCompileStatus, + compileFile, compileTestFile, compileTestFlow, - copyTestMacroFiles -} from './internal/compileTestFile' -import { copySyncFolder } from './internal/copySyncFolder' -import { + copyTestMacroFiles, + copySyncFolder, + getAllFolders, + SasFileType, getDestinationJobPath, - getDestinationServicePath -} from './internal/getDestinationPath' -import { getCompileTree } from './internal/loadDependencies' + getDestinationServicePath, + getCompileTree +} from './internal' export async function compile(target: Target, forceCompile = false) { const result = await checkCompileStatus(target, ['tests']) diff --git a/src/commands/compile/compileCommand.ts b/src/commands/compile/compileCommand.ts index ab2a04b6..9bda98d1 100644 --- a/src/commands/compile/compileCommand.ts +++ b/src/commands/compile/compileCommand.ts @@ -13,6 +13,7 @@ import { getAbsolutePath } from '@sasjs/utils' enum CompileSubCommand { Job = 'job', Service = 'service', + Test = 'test', Identify = 'identify' } @@ -41,15 +42,18 @@ export class CompileCommand extends TargetCommand { constructor(args: string[]) { let parseOptions = {} const subCommand = args[3] + if ( [ CompileSubCommand.Job, - CompileSubCommand.Identify, - CompileSubCommand.Service + CompileSubCommand.Service, + CompileSubCommand.Test, + CompileSubCommand.Identify ].includes(subCommand as CompileSubCommand) ) { parseOptions = subCommandParseOptions } + super(args, { parseOptions, syntax, usage, description, examples, aliases }) } @@ -96,6 +100,7 @@ export class CompileCommand extends TargetCommand { process.logger?.success( `The project was successfully compiled for ${target.serverType} using target '${target.name}'\nThe compile output is located in the ${buildDestinationFolder} directory.` ) + return ReturnCode.Success }) .catch((err) => { @@ -108,6 +113,7 @@ export class CompileCommand extends TargetCommand { async executeSingleFileCompile() { const { target } = await this.getTargetInfo() const output = await this.output + return await compileSingleFile( target, this.parsed.subCommand as string, @@ -118,10 +124,12 @@ export class CompileCommand extends TargetCommand { process.logger?.success( `Source has been successfully compiled!\nThe compiled output is located at: ${res.destinationPath}` ) + return ReturnCode.Success }) .catch((err) => { displayError(err, 'Error compiling source.') + return ReturnCode.InternalError }) } diff --git a/src/commands/compile/compileSingleFile.ts b/src/commands/compile/compileSingleFile.ts index 9aea8b7f..fb1e993a 100644 --- a/src/commands/compile/compileSingleFile.ts +++ b/src/commands/compile/compileSingleFile.ts @@ -9,9 +9,22 @@ import { getAbsolutePath, SASJsFileType } from '@sasjs/utils' -import { compileFile } from './internal/compileFile' -import { identifySasFile } from './internal/identifySasFile' -import { getCompileTree } from './internal/loadDependencies' +import { prefixMessage } from '@sasjs/utils/error' +import { + compileFile, + identifySasFile, + getCompileTree, + compileTestFile +} from './internal' + +export enum CompileSingleFileSubCommands { + Job = 'job', + Service = 'service', + Test = 'test' +} + +const isCompileSingleFileSubCommands = (command: string) => + (Object.values(CompileSingleFileSubCommands) as string[]).includes(command) export async function compileSingleFile( target: Target, @@ -21,15 +34,13 @@ export async function compileSingleFile( insertProgramVar: boolean = false, currentFolder?: string ) { - const subCommands = { - job: 'job', - service: 'service' - } - - if (!subCommands.hasOwnProperty(subCommand) && subCommand !== 'identify') { + if ( + !isCompileSingleFileSubCommands(subCommand) && + subCommand !== 'identify' + ) { throw new Error( - `Unsupported context command. Supported commands are:\n${Object.keys( - subCommands + `Unsupported context command. Supported commands are:\n${Object.values( + CompileSingleFileSubCommands ).join('\n')}` ) } @@ -49,27 +60,34 @@ export async function compileSingleFile( ) if (!(await validateSourcePath(sourcePath))) { - throw new Error(`Provide a path to source file (eg '${commandExample}')`) + throw new Error( + `Provide a valid path to source file (eg '${commandExample}')` + ) } if (subCommand === 'identify') { - subCommand = await identifySasFile(target, sourcePath) + subCommand = await identifySasFile(target, sourcePath).catch((err) => { + throw prefixMessage(err, 'Single file compilation failed. ') + }) } process.logger?.info(`Compiling source file:\n- ${sourcePath}`) - let outputPathParts = output.split(path.sep) - const leafFolderName = source.split(path.sep).pop() as string + const outputPathParts = output.split(path.sep) outputPathParts.pop(), outputPathParts.pop() + + const leafFolderName = source.split(path.sep).pop() as string const parentOutputFolder = outputPathParts.join(path.sep) const pathExists = await fileExists(parentOutputFolder) + if (pathExists) await deleteFolder(parentOutputFolder) await createFolder(output) const sourceFileName = sourcePath.split(path.sep).pop() as string const destinationPath = path.join(output, sourceFileName) + await copy(sourcePath, destinationPath) const sourceFileNameWithoutExt = sourceFileName.split('.')[0] @@ -86,7 +104,7 @@ export async function compileSingleFile( .join(path.sep) switch (subCommand) { - case subCommands.service: + case CompileSingleFileSubCommands.Service: await compileFile( target, destinationPath, @@ -98,7 +116,7 @@ export async function compileSingleFile( sourceFolder ) break - case subCommands.job: + case CompileSingleFileSubCommands.Job: await compileFile( target, destinationPath, @@ -110,7 +128,16 @@ export async function compileSingleFile( sourceFolder ) break - default: + case CompileSingleFileSubCommands.Test: + await compileTestFile( + target, + sourcePath, + programVar, + undefined, + false, + compileTree, + destinationPath + ) break } diff --git a/src/commands/compile/internal/checkCompileStatus.ts b/src/commands/compile/internal/checkCompileStatus.ts index 17e13687..3b0e763f 100644 --- a/src/commands/compile/internal/checkCompileStatus.ts +++ b/src/commands/compile/internal/checkCompileStatus.ts @@ -1,10 +1,11 @@ import { Target, asyncForEach, folderExists } from '@sasjs/utils' -import { compareFolders } from './compareFolders' -import { getAllFolders, SasFileType } from './getAllFolders' import { + compareFolders, + getAllFolders, + SasFileType, getDestinationJobPath, getDestinationServicePath -} from './getDestinationPath' +} from './' export async function checkCompileStatus( target: Target, diff --git a/src/commands/compile/internal/compileFile.ts b/src/commands/compile/internal/compileFile.ts index 8c9a3253..6c2e94aa 100644 --- a/src/commands/compile/internal/compileFile.ts +++ b/src/commands/compile/internal/compileFile.ts @@ -1,8 +1,6 @@ -import { createFile, readFile, isTestFile, CompileTree } from '@sasjs/utils' -import { Target, ServerType, SASJsFileType } from '@sasjs/utils/types' -import { ServerTypeError } from '@sasjs/utils/error' -import { loadDependencies } from './loadDependencies' -import { getServerType } from './getServerType' +import { createFile, isTestFile, CompileTree } from '@sasjs/utils' +import { Target, SASJsFileType } from '@sasjs/utils/types' +import { loadDependencies } from './' import path from 'path' /** diff --git a/src/commands/compile/internal/compileTestFile.ts b/src/commands/compile/internal/compileTestFile.ts index e7d2d5ce..86416f5b 100644 --- a/src/commands/compile/internal/compileTestFile.ts +++ b/src/commands/compile/internal/compileTestFile.ts @@ -20,7 +20,7 @@ import path from 'path' import { Coverage, CoverageState, CoverageType, TestFlow } from '../../../types' import { getMacroFolders, getProgramFolders } from '../../../utils/config' import { sasFileRegExp } from '../../../utils/file' -import { loadDependencies } from './loadDependencies' +import { loadDependencies } from './' const getFileName = (filePath: string) => path.parse(filePath).base @@ -30,7 +30,8 @@ export async function compileTestFile( testVar: string = '', saveToRoot: boolean = true, removeOriginalFile = true, - compileTree: CompileTree + compileTree: CompileTree, + destinationPath?: string ) { let dependencies = await loadDependencies( target, @@ -50,23 +51,25 @@ export async function compileTestFile( .split(path.sep) .pop() - const destinationPath = path.join( - buildDestinationTestFolder, - saveToRoot - ? filePath.split(path.sep).pop() || '' - : filePath - .split(path.sep) - .reduce( - (acc: any, item: any, i: any, arr: any) => - acc.length - ? [...acc, item] - : arr[i - 1] === buildDestinationFolderName - ? [...acc, item] - : acc, - [] - ) - .join(path.sep) - ) + destinationPath = + destinationPath || + path.join( + buildDestinationTestFolder, + saveToRoot + ? filePath.split(path.sep).pop() || '' + : filePath + .split(path.sep) + .reduce( + (acc: any, item: any, i: any, arr: any) => + acc.length + ? [...acc, item] + : arr[i - 1] === buildDestinationFolderName + ? [...acc, item] + : acc, + [] + ) + .join(path.sep) + ) await createFile(destinationPath, dependencies) diff --git a/src/commands/compile/internal/getAllFolders.ts b/src/commands/compile/internal/getAllFolders.ts index c6ca263c..dc5dda2b 100644 --- a/src/commands/compile/internal/getAllFolders.ts +++ b/src/commands/compile/internal/getAllFolders.ts @@ -8,12 +8,13 @@ import { export enum SasFileType { Service = 'service', - Job = 'job' + Job = 'job', + Test = 'test' } export const getAllFolders = async ( target: Target, - type: SasFileType, + type: SasFileType.Service | SasFileType.Job, rootConfig?: Configuration ): Promise => { const configuration = rootConfig || process.sasjsConfig diff --git a/src/commands/compile/internal/getServerType.ts b/src/commands/compile/internal/getServerType.ts deleted file mode 100644 index 2063ee8f..00000000 --- a/src/commands/compile/internal/getServerType.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Target } from '@sasjs/utils' -import { ServerType } from '@sasjs/utils/types' - -/** - * Returns server type for 'compile' step. - * If target is not present, it looks for serverType at root level - * Default value for serverType is 'SASVIYA' - * @param {Target} target- the target to check server type. - */ -export async function getServerType(target: Target): Promise { - if (target?.serverType) return target.serverType - - const configuration = process.sasjsConfig - - return configuration?.serverType - ? configuration.serverType - : ServerType.SasViya -} diff --git a/src/commands/compile/internal/identifySasFile.ts b/src/commands/compile/internal/identifySasFile.ts index 79e25e8d..6a566c1e 100644 --- a/src/commands/compile/internal/identifySasFile.ts +++ b/src/commands/compile/internal/identifySasFile.ts @@ -1,20 +1,26 @@ +import { Target, isTestFile } from '@sasjs/utils' +import { getAllFolders, SasFileType } from './' import path from 'path' -import { Target } from '@sasjs/utils' -import { getAllFolders, SasFileType } from './getAllFolders' export const identifySasFile = async ( target: Target, sourcePath: string -): Promise<'job' | 'service'> => { +): Promise => { + if (isTestFile(sourcePath.split(path.sep).pop() as string)) { + return SasFileType.Test + } + const serviceFolders = await getAllFolders(target, SasFileType.Service) - const isService = serviceFolders.find((folder) => sourcePath.includes(folder)) - if (isService) return 'service' + if (serviceFolders.find((folder) => sourcePath.includes(folder))) { + return SasFileType.Service + } const jobFolders = await getAllFolders(target, SasFileType.Job) - const isJob = jobFolders.find((folder) => sourcePath.includes(folder)) - if (isJob) return 'job' + if (jobFolders.find((folder) => sourcePath.includes(folder))) { + return SasFileType.Job + } - throw 'Unable to identify file as Service or Job' + throw `Unable to identify file as ${SasFileType.Service}, ${SasFileType.Job} or ${SasFileType.Test}` } diff --git a/src/commands/compile/internal/index.ts b/src/commands/compile/internal/index.ts new file mode 100644 index 00000000..88de32f6 --- /dev/null +++ b/src/commands/compile/internal/index.ts @@ -0,0 +1,9 @@ +export * from './checkCompileStatus' +export * from './compareFolders' +export * from './compileFile' +export * from './compileTestFile' +export * from './copySyncFolder' +export * from './getAllFolders' +export * from './getDestinationPath' +export * from './identifySasFile' +export * from './loadDependencies' diff --git a/src/commands/compile/internal/spec/compileTestFile.spec.ts b/src/commands/compile/internal/spec/compileTestFile.spec.ts index c71f7641..e9dc19ee 100644 --- a/src/commands/compile/internal/spec/compileTestFile.spec.ts +++ b/src/commands/compile/internal/spec/compileTestFile.spec.ts @@ -1,4 +1,4 @@ -import { compileTestFlow } from '../compileTestFile' +import { compileTestFlow } from '../' import { Logger, LogLevel, diff --git a/src/commands/compile/internal/spec/getAllFolders.spec.ts b/src/commands/compile/internal/spec/getAllFolders.spec.ts index f792b4c9..2779e985 100644 --- a/src/commands/compile/internal/spec/getAllFolders.spec.ts +++ b/src/commands/compile/internal/spec/getAllFolders.spec.ts @@ -1,4 +1,4 @@ -import { SasFileType, getAllFolders } from '../getAllFolders' +import { SasFileType, getAllFolders } from '../' import { Target, readFile, ServerType, Configuration } from '@sasjs/utils' import { generateTestTarget } from '../../../../utils/test' import * as configUtils from '../../../../utils/config' diff --git a/src/commands/compile/spec/compareFolders.spec.ts b/src/commands/compile/spec/compareFolders.spec.ts index 2c471d6e..a755866c 100644 --- a/src/commands/compile/spec/compareFolders.spec.ts +++ b/src/commands/compile/spec/compareFolders.spec.ts @@ -5,7 +5,7 @@ import { deleteFolder, generateTimestamp } from '@sasjs/utils' -import { compareFolders } from '../internal/compareFolders' +import { compareFolders } from '../internal' describe('compareFolders', () => { let sourceFolderPath: string diff --git a/src/commands/compile/spec/compileFile.spec.ts b/src/commands/compile/spec/compileFile.spec.ts index 0563cfa4..2bceadc2 100644 --- a/src/commands/compile/spec/compileFile.spec.ts +++ b/src/commands/compile/spec/compileFile.spec.ts @@ -15,9 +15,8 @@ import { verifyCompiledService, verifyCompiledJob } from '../../../utils/test' -import { compileFile } from '../internal/compileFile' +import { compileFile, getCompileTree } from '../internal' import { copy, fileExists, createFolder, readFile } from '@sasjs/utils' -import { getCompileTree } from '../internal/loadDependencies' const fakeJobInit = `/** @file diff --git a/src/commands/compile/spec/compileSingleFile.spec.ts b/src/commands/compile/spec/compileSingleFile.spec.ts new file mode 100644 index 00000000..e50c50bc --- /dev/null +++ b/src/commands/compile/spec/compileSingleFile.spec.ts @@ -0,0 +1,115 @@ +import { + compileSingleFile, + CompileSingleFileSubCommands +} from '../compileSingleFile' +import * as identifySasFileModule from '../internal/identifySasFile' +import * as fileModule from '@sasjs/utils/file' +import * as configModule from '../../../utils/config' +import * as loadDependenciesModule from '../internal/loadDependencies' +import * as compileTestFileModule from '../internal/compileTestFile' +import { SasFileType } from '../internal' + +describe('compileSingleFile', () => { + it('should throw an error provided not supported subcommand', async () => { + const expectedError = new Error( + `Unsupported context command. Supported commands are:\n${Object.values( + CompileSingleFileSubCommands + ).join('\n')}` + ) + + await expect( + compileSingleFile({} as any, 'WRONG', 'testSource', 'testOutput') + ).rejects.toEqual(expectedError) + }) + + it('should throw an error source was not provided', async () => { + const expectedError = new Error( + `'--source' flag is missing (eg 'sasjs compile --source myjob.sas --target targetName -output /some/folder')` + ) + + await expect( + compileSingleFile( + {} as any, + CompileSingleFileSubCommands.Test, + '', + 'testOutput' + ) + ).rejects.toEqual(expectedError) + }) + + it('should throw an error provided source path is not valid', async () => { + const expectedError = new Error( + `Provide a valid path to source file (eg 'sasjs compile --source myjob.sas --target targetName -output /some/folder')` + ) + + await expect( + compileSingleFile( + {} as any, + CompileSingleFileSubCommands.Test, + 'not valid', + 'testOutput', + false, + 'testCurrentDir' + ) + ).rejects.toEqual(expectedError) + }) + + it(`should identify sas file if a sub command is 'identify'`, async () => { + const testFile = 'testSource.test.sas' + + jest + .spyOn(fileModule, 'fileExists') + .mockImplementation((filePath: string) => + Promise.resolve(filePath.includes(testFile)) + ) + jest.spyOn(fileModule, 'copy').mockImplementation(() => Promise.resolve()) + jest + .spyOn(configModule, 'getMacroFolders') + .mockImplementation(() => Promise.resolve([])) + jest + .spyOn(configModule, 'getProgramFolders') + .mockImplementation(() => Promise.resolve([])) + jest + .spyOn(loadDependenciesModule, 'getCompileTree') + .mockImplementation(() => ({ saveTree: () => Promise.resolve() } as any)) + jest + .spyOn(compileTestFileModule, 'compileTestFile') + .mockImplementation(() => Promise.resolve()) + jest.spyOn(identifySasFileModule, 'identifySasFile') + + await compileSingleFile( + {} as any, + 'identify', + testFile, + 'testOutput', + false, + 'testCurrentDir' + ) + + expect(identifySasFileModule.identifySasFile).toHaveBeenCalledTimes(1) + }) + + it(`should throw an error if a sas file was not identified`, async () => { + const identificationError = `Unable to identify file as ${SasFileType.Service}, ${SasFileType.Job} or ${SasFileType.Test}` + + jest + .spyOn(fileModule, 'fileExists') + .mockImplementation(() => Promise.resolve(true)) + jest + .spyOn(identifySasFileModule, 'identifySasFile') + .mockImplementation(() => Promise.reject(identificationError)) + + const expectedError = `Single file compilation failed. ${identificationError}` + + await expect( + compileSingleFile( + {} as any, + 'identify', + 'testSource.sas', + 'testOutput', + false, + 'testCurrentDir' + ) + ).rejects.toEqual(expectedError) + }) +}) diff --git a/src/commands/compile/spec/loadDependencies.spec.ts b/src/commands/compile/spec/loadDependencies.spec.ts index c347f1f3..45bffecf 100644 --- a/src/commands/compile/spec/loadDependencies.spec.ts +++ b/src/commands/compile/spec/loadDependencies.spec.ts @@ -19,8 +19,8 @@ import { updateConfig } from '../../../utils/test' import { getLocalConfig, setConstants } from '../../../utils' +import { SasFileType } from '../internal' import { loadDependencies, getCompileTree } from '../internal/loadDependencies' -import { SasFileType } from '../internal/getAllFolders' const fakeInit = `/** @file serviceinit.sas diff --git a/src/commands/run/run.ts b/src/commands/run/run.ts index 253c85e2..5a19752e 100644 --- a/src/commands/run/run.ts +++ b/src/commands/run/run.ts @@ -22,7 +22,7 @@ import { isSasJsServerInServerMode } from '../../utils/' import axios from 'axios' -import { getDestinationServicePath } from '../compile/internal/getDestinationPath' +import { getDestinationServicePath } from '../compile/internal' import { saveLog } from '../../utils/saveLog' import { parseSourceFile } from '../../utils/parseSourceFile' diff --git a/src/types/command/commandBase.ts b/src/types/command/commandBase.ts index 695a9937..1dd90e12 100644 --- a/src/types/command/commandBase.ts +++ b/src/types/command/commandBase.ts @@ -4,7 +4,7 @@ import { ReturnCode } from './returnCode' import { unalias } from './unalias' const subCommandMap = new Map([ - ['compile', ['job', 'service', 'identify']], + ['compile', ['job', 'service', 'test', 'identify']], ['context', ['create', 'delete', 'edit', 'export', 'list']], ['folder', ['create', 'delete', 'list']], ['doc', ['init', 'lineage']], diff --git a/src/types/command/targetCommand.ts b/src/types/command/targetCommand.ts index d13382f8..0cfea97a 100644 --- a/src/types/command/targetCommand.ts +++ b/src/types/command/targetCommand.ts @@ -22,6 +22,7 @@ export class TargetCommand extends CommandBase { description: 'The target to execute this command against.' } } + super(args, { ...options, parseOptions }) }