@sprucelabs/spruce-cli
Version:
Command line interface for building Spruce skills.
185 lines (158 loc) • 6.17 kB
text/typescript
import pathUtil from 'path'
import globby from '@sprucelabs/globby'
import { namesUtil } from '@sprucelabs/spruce-skill-utils'
import { diskUtil } from '@sprucelabs/spruce-skill-utils'
import { SpruceSchemas } from '#spruce/schemas/schemas.types'
import createTestActionSchema from '#spruce/schemas/spruceCli/v2020_07_22/createTestOptions.schema'
import AbstractAction from '../../AbstractAction'
import { FeatureActionResponse } from '../../features.types'
import TestFeature, { ParentClassCandidate } from '../TestFeature'
export default class CreateAction extends AbstractAction<OptionsSchema> {
public optionsSchema = createTestActionSchema
public invocationMessage = 'Creating a test... 🛡'
public async execute(options: Options): Promise<FeatureActionResponse> {
const normalizedOptions = this.validateAndNormalizeOptions(options)
const { testDestinationDir, namePascal, nameCamel, type } =
normalizedOptions
let resolvedDestination = diskUtil.resolvePath(
this.cwd,
testDestinationDir,
type
)
let parentTestClass:
| undefined
| { name: string; importPath: string; isDefaultExport: boolean }
const testFeature = this.parent as TestFeature
const candidates = await testFeature.buildParentClassCandidates()
if (diskUtil.doesDirExist(resolvedDestination)) {
resolvedDestination =
await this.promptForSubDir(resolvedDestination)
}
if (candidates.length > 0) {
parentTestClass =
await this.promptForParentTestClassAndOptionallyInstallDependencies(
candidates,
parentTestClass,
resolvedDestination
)
}
this.ui.startLoading('Generating test file...')
const writer = this.Writer('test')
const isTestFixturesInstalled = !!this.Service('pkg').get(
'devDependencies.@sprucelabs/spruce-test-fixtures'
)
let doesStaticTestExist =
await this.doesStaticTestAlreadyExist(resolvedDestination)
const results = await writer.writeTest(resolvedDestination, {
...normalizedOptions,
type,
nameCamel,
parentTestClass,
isTestFixturesInstalled,
namePascal: namePascal ?? namesUtil.toPascal(nameCamel),
testType: doesStaticTestExist ? 'static' : 'instance',
})
return {
files: results,
hints: ["run `spruce test` in your skill when you're ready!"],
}
}
private async doesStaticTestAlreadyExist(resolvedDestination: string) {
const matches = await globby(resolvedDestination + `/**/*.test.ts`)
let doesStaticTestExist = false
const match = matches[0]
if (match) {
const contents = diskUtil.readFile(matches[0])
doesStaticTestExist = contents.includes('protected static')
}
return doesStaticTestExist
}
private async promptForSubDir(resolvedDestination: string) {
const match = await this.ui.prompt({
type: 'directory',
label: 'Where should I write this test?',
isRequired: true,
defaultValue: {
path: diskUtil.resolvePath(resolvedDestination),
},
})
resolvedDestination = diskUtil.resolvePath(
resolvedDestination,
match.path
)
return resolvedDestination
}
private async promptForParentTestClassAndOptionallyInstallDependencies(
candidates: ParentClassCandidate[],
parentTestClass:
| { name: string; importPath: string; isDefaultExport: boolean }
| undefined,
resolvedDestination: string
) {
const idx = await this.ui.prompt({
type: 'select',
isRequired: true,
label: 'Which abstract test class do you want to extend?',
options: {
choices: [
{ value: '', label: 'AbstractSpruceTest (default)' },
...candidates.map((candidate, idx) => ({
value: `${idx}`,
label: candidate.label,
})),
],
},
})
if (idx !== '' && candidates[+idx]) {
const match = candidates[+idx]
if (match) {
await this.optionallyInstallFeatureBasedOnSelection(match)
parentTestClass = this.buildParentClassFromCandidate(
match,
resolvedDestination
)
}
}
return parentTestClass
}
private async optionallyInstallFeatureBasedOnSelection(
match: ParentClassCandidate
) {
if (match.featureCode) {
const isInstalled = await this.features.isInstalled(
match.featureCode
)
if (!isInstalled) {
this.ui.startLoading(`Installing ${match.name}...`)
await this.features.install({
features: [{ code: match.featureCode as any }],
})
this.ui.stopLoading()
}
}
}
private buildParentClassFromCandidate(
match: ParentClassCandidate,
resolvedDestination: string
): {
name: string
label: string
importPath: string
isDefaultExport: boolean
} {
return {
name: match.name,
label: match.label,
isDefaultExport: match.isDefaultExport,
importPath:
match.import ??
diskUtil.resolveRelativePath(
resolvedDestination,
//@ts-ignore
match.path.replace(pathUtil.extname(match.path), '')
),
}
}
}
type OptionsSchema = SpruceSchemas.SpruceCli.v2020_07_22.CreateTestOptionsSchema
type Options = SpruceSchemas.SpruceCli.v2020_07_22.CreateTestOptions