Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[legacy-framework] Add runCommand as option to the recipe builder #3090

Merged
merged 3 commits into from
Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions nextjs/packages/installer/src/executors/run-command-executor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { spawn } from 'cross-spawn'
import { Box, Text } from 'ink'
import Spinner from 'ink-spinner'
import * as React from 'react'
import { Newline } from '../components/newline'
import { RecipeCLIArgs } from '../types'
import { useEnterToContinue } from '../utils/use-enter-to-continue'
import { useUserInput } from '../utils/use-user-input'
import { Executor, ExecutorConfig, getExecutorArgument } from './executor'

export type CliCommand = string | [string, ...string[]]

export interface Config extends ExecutorConfig {
command: CliCommand
}
export interface CommitChildProps {
commandInstalled: boolean
handleChangeCommitted: () => void
command: CliCommand
cliArgs: RecipeCLIArgs
step: Config
}

export const type = 'run-command'

function Command({
command,
loading,
}: {
command: CliCommand
loading: boolean
}) {
return (
<Text>
{` `}
{loading ? <Spinner /> : '✅'}
{` ${typeof command === 'string' ? command : command.join(' ')}`}
</Text>
)
}

const CommandList = ({
lede = 'Hang tight! Running...',
commandLoading = false,
step,
command,
}: {
lede?: string
commandLoading?: boolean
step: Config
command: CliCommand
}) => {
return (
<Box flexDirection="column">
<Text>{lede}</Text>
<Newline />
<Command key={step.stepId} command={command} loading={commandLoading} />
</Box>
)
}

/**
* INFO: Exported for unit testing purposes
*
* This function calls the defined command with their optional arguments if defined
*
* @param {CliCommand} input The Command and arguments
* @return Promise<void>
*
* @example await executeCommand("ls")
* @example await executeCommand(["ls"])
* @example await executeCommand(["ls", ...["-a", "-l"]])
*/
export async function executeCommand(input: CliCommand): Promise<void> {
// from https://stackoverflow.com/a/43766456/9950655
const argsRegex = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|\/[^/\\]*(?:\\[\S\s][^/\\]*)*\/[gimy]*(?=\s|$)|(?:\\\s|\S)+)/g
const command: string[] = Array.isArray(input)
? input
: input.match(argsRegex) || []

if (command.length === 0) {
throw new Error(`The command is too short: \`${JSON.stringify(input)}\``)
}

await new Promise((resolve) => {
const cp = spawn(command[0], command.slice(1), {
stdio: ['inherit', 'pipe', 'pipe'],
})
cp.on('exit', resolve)
})
}

export const Commit: Executor['Commit'] = ({
cliArgs,
cliFlags,
step,
onChangeCommitted,
}) => {
const userInput = useUserInput(cliFlags)
const [commandInstalled, setCommandInstalled] = React.useState(false)
const executorCommand = getExecutorArgument((step as Config).command, cliArgs)

const handleChangeCommitted = React.useCallback(() => {
onChangeCommitted(`Executed command ${executorCommand}`)
}, [executorCommand, onChangeCommitted])

React.useEffect(() => {
async function runCommand() {
await executeCommand(executorCommand)
setCommandInstalled(true)
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
runCommand()
}, [cliArgs, step, executorCommand])

React.useEffect(() => {
if (commandInstalled) {
handleChangeCommitted()
}
}, [commandInstalled, handleChangeCommitted])

const childProps: CommitChildProps = {
commandInstalled,
handleChangeCommitted,
command: executorCommand,
cliArgs,
step: step as Config,
}

if (userInput) return <CommitWithInput {...childProps} />
else return <CommitWithoutInput {...childProps} />
}

const CommitWithInput = ({
commandInstalled,
handleChangeCommitted,
command,
step,
}: CommitChildProps) => {
useEnterToContinue(handleChangeCommitted, commandInstalled)

return (
<CommandList
commandLoading={!commandInstalled}
step={step}
command={command}
/>
)
}

const CommitWithoutInput = ({
commandInstalled,
command,
step,
}: CommitChildProps) => (
<CommandList
commandLoading={!commandInstalled}
step={step}
command={command}
/>
)
12 changes: 12 additions & 0 deletions nextjs/packages/installer/src/recipe-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as AddDependencyExecutor from './executors/add-dependency-executor'
import * as TransformFileExecutor from './executors/file-transform-executor'
import * as NewFileExecutor from './executors/new-file-executor'
import * as PrintMessageExecutor from './executors/print-message-executor'
import * as RunCommandExecutor from './executors/run-command-executor'
import { ExecutorConfigUnion, RecipeExecutor } from './recipe-executor'
import { RecipeMeta } from './types'

Expand All @@ -22,6 +23,10 @@ export interface IRecipeBuilder {
addTransformFilesStep(
step: Omit<TransformFileExecutor.Config, 'stepType'>
): IRecipeBuilder
addRunCommandStep(
step: Omit<RunCommandExecutor.Config, 'stepType'>
): IRecipeBuilder

build(): RecipeExecutor<any>
}

Expand Down Expand Up @@ -78,6 +83,13 @@ export function RecipeBuilder(): IRecipeBuilder {
})
return this
},
addRunCommandStep(step: Omit<RunCommandExecutor.Config, 'stepType'>) {
steps.push({
stepType: RunCommandExecutor.type,
...step,
})
return this
},
build() {
return new RecipeExecutor(meta as RecipeMeta, steps)
},
Expand Down
2 changes: 2 additions & 0 deletions nextjs/packages/installer/src/recipe-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Executor, ExecutorConfig, Frontmatter } from './executors/executor'
import * as FileTransformExecutor from './executors/file-transform-executor'
import * as NewFileExecutor from './executors/new-file-executor'
import * as PrintMessageExecutor from './executors/print-message-executor'
import * as RunCommandExecutor from './executors/run-command-executor'
import { RecipeCLIArgs, RecipeCLIFlags, RecipeMeta } from './types'
import { useEnterToContinue } from './utils/use-enter-to-continue'
import { useUserInput } from './utils/use-user-input'
Expand All @@ -32,6 +33,7 @@ const ExecutorMap: { [key: string]: Executor } = {
[NewFileExecutor.type]: NewFileExecutor,
[PrintMessageExecutor.type]: PrintMessageExecutor,
[FileTransformExecutor.type]: FileTransformExecutor,
[RunCommandExecutor.type]: RunCommandExecutor,
} as const

interface State {
Expand Down