You’ve probably heard that front-end components can run in browsers, in mobile apps, and even directly on various devices, but have you ever seen front-end components run directly in command lines, allowing the front-end code to build the GUI interface and interaction logic of the terminal window?

Today, I want to share with you a very interesting open source project: INK. It renders the React component in a terminal window, showing the final command line interface.

This article focuses on actual combat, the first will take you familiar with the basic use, and then will do a practice project based on the actual scene.

Hands-on experience

At the beginning, it is recommended to use official scaffolding to create projects, save time and worry.

npx create-ink-app --typescript
Copy the code

Then run this code:

import React, { useState, useEffect } from 'react'
import { render, Text} from 'ink'

const Counter = () = > {
  const [count, setCount] = useState(0)
  useEffect(() = > {
    const timer = setInterval(() = > {
      setCount(count= > ++count)
    }, 100)
    return () = > {
      clearInterval(timer)
    }
    
  })

  return (
    <Text color="green">
      {count} tests passed
    </Text>
  )
}

render(<Counter />);
Copy the code

The following screen appears:

And the numbers keep increasing! The demo is small but illustrative:

  1. First, the text output is not consoled directly, but rendered by the React component.

  2. The React component’s state management and hooks logic are still in effect on the GUI on the command line.

That said, the front-end capability and extension to the command line window is a very scary capability. The famous document generation tool Gatsby and package management tool Yarn2 both use this ability to complete the construction of terminal GUI.

Command line tool project practice

You may be new to this tool and know what it does, but you are still unfamiliar with how to use it. Let’s use a practical example to get familiar with it. The repository has been uploaded to Git, you can fork the code at this address: github.com/sanyuan0704…

Let’s develop the project from start to finish.

Project background

First of all, let’s talk about the background of the project. In a TS business project, we once encountered a problem: due to the production mode, we adopted TSC first, obtained JS product codes, and then packaged these products with Webpack.

The build failed because TSC couldn’t move any resource files other than TS (x) to the artifacts directory, so when WebPack packaged the artifacts, it found that some of the resource files were not found at all! For example, there was an image with a path like SRC /asset/1.png, but it was not in the production directory dist, so when WebPack packaged the code in dist, it found that the image did not exist and reported an error.

solution

So how do you solve that?

Obviously, it is difficult to extend TSC’s capabilities, so the best way to do this is to write a script to manually copy all the resource files under SRC to the dist directory. This will solve the problem of resources not being found.

Copy file logic

With a solution in mind, we write the following ts code:

import { join, parse } from "path";
import { fdir } from 'fdir';
import fse from 'fs-extra'
const staticFiles = await new fdir() 
  .withFullPaths()   
  // filter out node_modules, ts, TSX
  .filter(
    (p) = >! p.includes('node_modules') &&
      !p.endsWith('.ts') &&
      !p.endsWith('.tsx'))// Search the SRC directory
  .crawl(srcPath)
  .withPromise() as string[]

await Promise.all(staticFiles.map(file= > {
  const targetFilePath = file.replace(srcPath, distPath);
  // Create a directory and copy the files
  return fse.mkdirp(parse(targetFilePath).dir)
    .then(() = > fse.copyFile(file, distPath))
   );
}))
Copy the code

Code using fdir this library to search files, very easy to use a library, writing is also very elegant, recommend everyone to use.

We execute this logic and successfully move the resource file to the production directory.

The problem is solved, but can we encapsulate this logic to make it easier to reuse in other projects, or even make it available to others?

Next, I thought of command line tools.

Second, command line GUI construction

Then we use ink to build the command line GUI using the React component. The root component code is as follows:


// index.tsx introduces code elision
interface AppProps {
 fileConsumer: FileCopyConsumer
}

const ACTIVE_TAB_NAME = {
 STATE: "Execution status".LOG: "Execution Log"
}

const App: FC<AppProps> = ({ fileConsumer }) = > {
 const [activeTab, setActiveTab] = useState<string>(ACTIVE_TAB_NAME.STATE);
 const handleTabChange = (name) = > {
  setActiveTab(name)
 }
 const WELCOME_TEXT = dedent'Welcome to the' ink-copy 'console! Function overview is as follows (press **Tab** to switch): '

 return <>
   <FullScreen>
    <Box>
     <Markdown>{WELCOME_TEXT}</Markdown>
    </Box>
    <Tabs onChange={handleTabChange}>
     <Tab name={ACTIVE_TAB_NAME.STATE}>{ACTIVE_TAB_NAME.STATE}</Tab>
     <Tab name={ACTIVE_TAB_NAME.LOG}>{ACTIVE_TAB_NAME.LOG}</Tab>
    </Tabs>
    <Box>
     <Box display={ activeTab= = =ACTIVE_TAB_NAME.STATE ? 'flex': 'none'} >
      <State />
     </Box>
     <Box display={ activeTab= = =ACTIVE_TAB_NAME.LOG ? 'flex': 'none'} >
      <Log />
     </Box>
    </Box>
   </FullScreen>
 </>
};

export default App;
Copy the code

As you can see, there are two main components: State and Log, which correspond to two Tab columns. Specific code we can refer to the warehouse, the following release effect map:

3. How does the GUI display service status in real time?

Now the problem is that the logic for file manipulation is developed and the GUI interface is set up. Now how do you combine the two, that is, how does the GUI show the status of file operations in real time?

In this regard, we need to introduce a third party to carry out the communication between the two modules. Specifically, we maintain an EventBus object in the logic of the file operation and pass the EventBus through the Context in the React component. Thus complete UI and file operation module communication.

Now let’s develop the EventBus object, which is the FileCopyConsumer:

export interface EventData {
  kind: string;
  payload: any;
}

export class FileCopyConsumer {

  private callbacks: Function[];
  constructor() {
    this.callbacks = []
  }
  // For the React component binding callback
  onEvent(fn: Function) {
    this.callbacks.push(fn);
  }
  // called after the file operation is complete
  onDone(event: EventData) {
    this.callbacks.forEach(callback= > callback(event))
  }
}
Copy the code

And then in the file manipulation module and the UI module, we need to do the adaptation of the response, so let’s look at the file manipulation module first, let’s do the encapsulation.

export class FileOperator {
  fileConsumer: FileCopyConsumer;
  srcPath: string;
  targetPath: string;
  constructor(srcPath ? : string, targetPath ? : string) {
    // Initialize the EventBus object
    this.fileConsumer = new FileCopyConsumer();
    this.srcPath = srcPath ?? join(process.cwd(), 'src');
    this.targetPath = targetPath ?? join(process.cwd(), 'dist');
  }

  async copyFiles() {
    // Store log information
    const stats = [];
    // Search for files in SRC
    const staticFiles = ...
    
    await Promise.all(staticFiles.map(file= > {
        // ...
        / / store the log
        .then(() = > stats.push(`Copied file from [${file}] to [${targetFilePath}] `));
    }))
    / / call onDone
    this.fileConsumer.onDone({
      kind: "finish".payload: stats
    })
  }
}
Copy the code

After initializing the FileOperator, pass fileConsumer to the component via the React Context. Then the component can access fileConsumer and bind the callback function.

// Get the fileConsumer & bind callback in the component
export const State: FC<{}> = () = > {
  const context = useContext(Context);
  const [finish, setFinish] = useState(false); context? .fileConsumer.onEvent((data: EventData) = > {
    // The following logic is executed after the file is copied
    if (data.kind === 'finish') {
      setTimeout(() = > {
        setFinish(true)},2000)}})return 
  / / (JSX code)
}
Copy the code

Thus, we have successfully concatenated UI and file operation logic. Of course, due to space constraints, there is still some code that is not shown, and the complete code is in the Git repository. I hope you can fork down to experience the design of the whole project.

Overall, the React component’s ability to run on the command line is really exciting, freeing up a lot of imagination for the front end. This article is just the tip of the iceberg for this ability, more positions are waiting for you to unlock, so go and play!

This article was first published in the public number “front three students” welcome your attention, original link: wow! He renders the React component inside the command line terminal window!

Bytedance IES front-end architecture team is in urgent need of talents (P5 / P6 / P7 has a lot of HC), welcome to communicate with us on wechat sanyuan0704, and also welcome everyone to do things together.