0.3.6 • Published 4 months ago

soybean v0.3.6

Weekly downloads
-
License
GPL-3.0
Repository
github
Last release
4 months ago

About the project

Ever had to work on a complicated project, using multiple tools, compilers, bundlers, manually restart things or move a file from one place to another each time a tiny thing has changed?

Soybean started as a simple script to ease the annoying process of dealing with these sorts of things and evolved into an automation multi-tool.

As of now Soybean is capable of:

  • Spawning and managing multiple child processes such as compilers and frameworks from within a single terminal window - No more switching between terminal tabs to see what's broken.
  • Running automated tasks, or "routines" based on events that happen as you do your work - Fetch the remote at the start of your work, restart your app on .env file change or run a shell command in an interval.
  • Running custom handlers from the terminal that allow you to easily run quick tasks, input and manipulate data such as local variables, along with keeping your command history and passing through unrecognized commands to a system shell like zsh or powershell - You don't need a separate terminal tab to interact with your OS.

Table of contents

Getting started

Soybean works on a basis that similar to many other tools such as vite or tsc. It is a CLI tool available under the command soybean (or sb for convenience) that's initialized using a JavaScript configuration file.

Installation

Soybean is a CLI tool, meaning it has to be installed globally to be accessible through user shell.

  1. Install globally

    npm install soybean@latest -g
  2. Alternatively, if you only need Soybean for a single project, you can install it locally and run it through an npm script.

    npm install soybean@latest --save-dev

    Inside of package.json:

    {
        "scripts": {
            "soybean": "soybean run ./soybean.config.js"
        }
    }

Configuration

Soybean by design requires a configuration file that stores all of its settings used inside your project.

Create a new configuration file.

soybean init [file]

The above command will create a boilerplate JavaScript config file as seen below.

import { Soybean, handlers } from 'Soybean'

export default Soybean({
    cp: {},
    routines: {
        launch: [],
        watch: []
    },
    terminal: {
        passthroughShell: false,
        keepHistory: 50,
        handlers: {}
    }
})

Child processes configuration

One of Soybean's core features is consolidating all your processes into a single terminal window. Using the cp object in the Soybean config, you can easily set up multiple different instances of compilers, bundlers and different tasks.

Although having all your processes print out to the same terminal might seem messy at first, it saves lots of time switching between terminal windows/tabs looking at possible issues. Instead, everything can be spotted at an instant.

Soybean({
    cp: {
        // Specify a child process' name
        typescript: {
            // Specify the spawn command
            command: ["tsc", ''],
            // And the CWD
            cwd: "./src" 
        },
        // Set up another process...
        static: {
            command: "http-server",
            cwd: "./dist" 
        }
    }
})

Child process configuration options

Property nameTypeDescription
commandstring\|string[]Specifies the command used to spawn the child process. A simple string can be used for a bare command, like tsc or vite and an string[] to specify the command together with command parameters, eg. ["tsc", "-w", "--strict"].
stdout"all"\|"none"Specifies whether or not to pipe the child process' STDOUT, this will effectively mute the child process if used with "none" or display all of it's output if used with "all".
cwdstringThe current working directory of the child process, relative to the Soybean configuration file.
deferNextnumberTime in ms for which to wait with further execution after spawning this process. This allows for tricks like spawning a compiler and waiting a second before spawning a different process that relies on the compiler's output.
onSpawnEventHandlerAn event handler called whenever the process is spawned.
onKillEventHandlerAn event handler called whenever the process is killed by the user, so either through the terminal using the kl command or by another cp.kill event handler.
onCloseEventHandlerAn event handler called whenever the process quits on its own.

The child process' configuration object accepts the above properties, as well as standard options available for child_process.spawn() such as shell or signal, with exception of stdio and detached which were either disabled or altered due to how soybean operates.

Routines

Routines are small pieces of code executed based on events that happen as you work on your project, such us when you modify a file or type a command in the terminal.

Routines use Soybean's event handlers module, a set of predefined methods to build tidy, easy to write procedures, such as moving a file, restarting a child process or calling a custom callback with your own code.

Launch routines

Launch routines are really simple. All they do is run exactly once when soybean is spawned. This is useful when you want to prepare things before starting your work, such as fetching the remote.

Soybean({
    routines: {
        launch: [
            // Run "git fetch" on startup
            handlers.shell.spawn(['git', 'fetch'])
        ]
    }
})

The above code will result in git fetch being called once each time Soybean is started:

Watch routines

Watch routines are executed based on events coming from the file system, such as when you update a file, move it or delete it. They are especially useful when you can't use solutions like Nodemon and you would like to restart a program on file change.

Soybean({
    routines: {
        watch: [
            {
                // Watch directory.txt
                file: './my/file/or/directory.txt',
                // Restart "my-program" when the file changes
                handle: handlers.cp.restart('my-program'),
                // Watch options
                options: { 
                    rateLimiter: 500 
                }
            }
        ]
    }
})

Watch routines configuration options

Property nameTypeDefault valueDescription
filestring-The target file or directory path that is being watched.
handleFunction-The event handler called whenever a watch event is fired.
options.rateLimiternumber500The amount of time in milliseconds to wait after a watch event before the next one can be registered.

The options object also accepts native fs.watch options - encoding, persistent, recursive and signal.

Integrated terminal

Soybean's integrated terminal lets you interact with the program while it's running. Alongside a few built-in commands, you're able to specify your own command handlers.

Soybean({
    terminal: {
        // Keep the history of the last 50 commands and save it across sessions.
        keepHistory: 50,
        // Use a passthrough shell to pass "through" unrecognized commands to.
        passthroughShell: true,
        // Add a "change" to let the used change `myVar` on the fly.
        handlers: {
            change: handlers.handle(e => {
                // Change some variable
                myVar = e.argv[0]
                // Update the settings of an external tool or library...
                someTool.updateState(`new state: ${e.argvRaw}`)
            })
        }
    }
})

Terminal configuration options

Property nameTypeDefault valueDescription
passthroughShellstring\|booleanfalseThe passthrough shell used to execute commands that are unrecognized by Soybean itself, such as mkdir, sudo, etc... Use false to disable, true to use the default system shell, or a string, such as "/bin/zsh" or "cmd.exe" to use a specific shell.
keepHistorynumber0The number specifying how many previous commands to remember across sessions. These commands will be remembered by the global/local (depending on user setup) installation of Soybean even if the program is closed and reopened again.
handlersRecord<string, EventHandler>-An object containing user-specified command handlers.

Modules

Event handlers module

The handlers module is a set of methods that allow you to configure event handlers that react events that occur in Soybean during your work. They happen when you save a file, type a command or as you launch Soybean itself.

import { handlers } from 'soybean'

Event object

Each time an event handler is called in Soybean, a new event object is created containing the information about the event that ocurred.

The different types of events include:

  • SoybeanEvent - The default object whose properties and information are available to every event handler used. All of the event objects below extend the SoybeanEvent class.
    • SoybeanEvent.source - The source of the event, which, depending on where it originated from can be one of - "event" (Default value), "terminal", "launch", "watcher".
    • SoybeanEvent.set() - A method used to store data on the event object, which can then be retrieved and used later by a subsequent event handler inside a handler group.
      event.set(key: string, data: any)
      Alternatively, it is possible to read properties directly (Only available as read-only. It isn't possible to set data this way).
      event.get('key') | event.key |  event["key"]
    • SoybeanEvent.get() - Used to retrieve a piece of data set on the event object with set().
      event.get(key: string)
    • SoybeanEvent.update() - Used to update the value of an item stored on the event object with set(). It accepts a callback function used to process the variable and return it to be saved.
      event.update(key: string, callback: (data: any) => any)
    • SoybeanEvent.updateAsync() - The equivalent of the update() method used for asynchronous information.
      event.updateAsync(key: string, callback: (data: any) => Promise<any>)   
  • WatchEvent - The event emitted by watch routines.

    • WatchEvent.filename (string) - Path to the file/directory where the event originated.
    • WatchEvent.watchEventType ("rename" | "change") - The type of change.
  • TerminalEvent - Event emitted when a user-specified command is entered in the integrated terminal.

    • WatchEven.argv (string[]) - An array of space-separated command parameters passed after the command keyword.
    • WatchEven.argvRaw (string) - The raw string of text passed after the command keyword.
  • ChildProcessEvent - Event emitted by a child process when it's killed, revived, restarted or dies unexpectedly, configurable through child process options.

    • ChildProcessEvent.processName (string) - The name of the process that triggered the event.
    • ChildProcessEvent.exitCode (number | null) - The exit code returned by the child process, if the event originated from the process' death.

Using symbols

Many of the event handlers allow you to replace their regular parameters with symbols which are used to make the values of these parameters dynamic, as opposed to hardcoding them.

Any time a handler supporting symbol parameters is passed a symbol, it will read its description and use it as a key. This key is then used to fetch any data living on the event object and use it in place of the parameter.

Example:

fs.mkdir('./my/directory/')
group([
    // Do some setup and save a "my-dir" variable.
    ...,
    // Use the "my-dir" variable to choose the directory path dynamically.
    fs.mkdir(Symbol('my-dir'))
])

Additionally, for string parameters that support dynamic values with the use of symbols, string templates can be used instead:

group([
    // Set a "my-dir" variable.
    ...,
    // Then u it within a string parameter:
    fs.mkdir('./path/to/{{ my-dir }}/')
])

Miscellaneous handlers

handle()

The handle event handler uses a traditional callback function to allow you to perform any kind of action not possible with Soybean's built-in handlers.

Additionally it has access to the event object which allows it to interact with data shared with other handlers within the same event group.

handle(event: SoybeanEvent, callback: EventHandler)
handle(e => {
    const data = getSomeData()
    e.set('data', data)
})

group()

The group handler allows you to group multiple handlers and share data between them. This lets you create complex routines that perform a set of tasks on each event.

Inside of groups the event object includes an additional method stopPropagation() which lets you stop the group's execution when called.

group(handlers: EventHandler[])
group([
    // Read a file
    fs.readFile('./src/myConfig.json', 'my-config', 'utf-8'),
    // Parse it & save to "my-parsed-config"
    json.parse('my-config', 'my-parsed-config'),
    // Log it to the console
    handle(e => {
        console.log(event.get('my-parsed-config'))
    })
])

wait()

The wait event handler lets you create a time gap inside a handler group.

wait(time: number)
group([
    handle(e => { event.set('start', Date.now()) }),
    wait(1000),
    // Logs out "1000"
    handle(e => { console.log(Date.now() - e.get('start')) })
])

set()

The set handler is used to set a property on the event object inside a handler group. It is effectively a shorthand for event.set inside a handle event handler.

set(key: string, data: any)
group([
    set('config', fs.readFileSync('./config.js')),
    handle(e => console.log(e.get('config')))
])

update()

The update handler is a shorthand for event.update & event.updateAsync. It lets you quickly update a piece of information saved on the event object.

update(key: string, callback: (data: any) => any)
group([
    set('config', fs.readFileSync('./config.yaml')),
    update('config', (config) => {
        return YAML.parse(config)
    })
])

forEach()

Loops over an array or string. Accepts direct reference or a symbol with a description matching the key to read from the event object.

Exposes event.break and event.continue methods to manage the loop's operation. All of them accept an ID to target a specific loop

Inside forEach loops, three items are available through get() on the event object:

  • "array" - The array that the loop is iterating over.
  • "index" - The current array index inside the loop.
  • "value" - The current value from the array the loop is iterating over.

Note: If the loop has an ID set, all the above properties will be prefixed with its ID.
Eg. For a loop labeled as "loop1", the "value" property would be changed to "loop1-value". This lets you nest loops and groups inside each other without variable naming conflicts.

forEach(iterable: symbol | any[], handler: EventHandler)
forEach(iterable: symbol | any[], id: string, handler: EventHandler)
forEach([1, 2, 3, 4, 5], handle(e => {
    console.log(e.get('value'))
}))
group([
    set('my-array', [1, 2, 3, 4, 5]),
    forEach(Symbol("my-array"), handle(e => {
        console.log(e.get('value'))
    }))
])

forOf()

Loops over an iterable object or array. Accepts an iterable object or a symbol with a description matching the key to read from the event object.

Exposes event.break and event.continue methods to manage the loop's operation. All of them accept an ID to target a specific loop

Inside forOf loops, two items are available through get() on the event object:

  • "object" - The object that the loop is iterating over.
  • "value" - The current value read from the iterated object.

Note: If the loop has an ID set, all the above properties will be prefixed with its ID.
Eg. For a loop labeled as "loop1", the "value" property would be changed to "loop1-value". This lets you nest loops and groups inside each other without variable naming conflicts.

forOf(iterable: symbol | Iterable, handler: EventHandler)
forOf(iterable: symbol | Iterable, id: string, handler: EventHandler)
forOf([1, 2, 3, 4, 5], handle(e => {
    console.log(e.get('value'))
}))
group([
    set('values', [1, 2, 3, 4, 5])
    forOf(Symbol('values'), handle(e => {
        console.log(e.get('value'))
    }))
])

forIn()

Loops over enumerable string properties of an object. Accepts an enumerable object or a symbol with a description matching the key to read from the event object.

Exposes event.break and event.continue methods to manage the loop's operation. All of them accept an ID to target a specific loop

Inside forIn loops, three items are available through get() on the event object:

  • "object" - The reference to the object being looped over.
  • "key" - The current object key inside the loop.
  • "value" - The current value read from the object.

Note: If the loop has an ID set, all the above properties will be prefixed with its ID.
Eg. For a loop labeled as "loop1", the "value" property would be changed ro "loop1-value". This lets you nest loops and groups inside each other without variable naming conflicts.

forIn(iterable: symbol | Record<any, any>, handler: EventHandler)
forIn(iterable: Record<any, any>, id: string, handler: EventHandler)
forIn({ a: 1, b: 2, c: 3 }, handle(e => {
    console.log(e.get('value'))
}))
group([
    set('values', { a: 1, b: 2, c: 3 })
    forIn(Symbol('values'), handle(e => {
        console.log(e.get('value'))
    }))
])

FS handlers

fs.mkdir()

Used to create a new directory. Accepts either a string path or a symbol with a description matching a key to read from the event object in place of the text path.

fs.mkdir(path: string | symbol, options: MkdirOptions)
fs.mkdir('./relative/to/soybean-config/')
group([
    set('path', './some/path')
    fs.mkdir(Symbol('some-path'))
])

fs.readdir()

Reads the contents of a directory and saves it on the event object. Accepts a string path or a symbol with a description matching a key to read from the event object in place of the path, a string which is used to save the results on the event object and read options the same as in the native fs.readdir.

fs.readdir(path: string | symbol, saveTo: string, options?: ReaddirOptions)
group([
    fs.readdir('./relative/to/soybean-config/', 'my-dir'),
    handle(e => console.log(e.get('my-dir')))
])

fs.rmdir()

Removes a directory. Accepts a string path or a symbol with a description matching a key to read from the event object and an options object (see native fs.rmdir options).

fs.rmdir(path: string | symbol, options?: RmdirOptions)
fs.rmdir('./path/to/remove', { recursive: true })
group([
    set('path', './my/path/'),
    fs.rmdir(Symbol('path'))
])

fs.rm()

Removes a file or directory. Accepts a string path or a symbol with a description matching the key to read from the event object and an options object same as in native fs.rm.

fs.rm(path: string | symbol, options>: RmOptions)
fs.rm('./my/path.txt', { force: true })

fs.readFile()

Reads the contents of a file and saves it on the event object to be later used inside another handler. Accepts a string path or symbol with a description matching the key to read from the event object and a string key used to save the file data on the event object.

fs.readFile(path: string | symbol, saveTo: string, options?: ReadFileOptions)
group([
    fs.readFile('./my/config.txt', 'my-config', { encoding: 'utf-8' }),
    handle(e => console.log(e.get('my-config')))
])

fs.writeFile()

Writes data to a file. Accepts a string file path or symbol with a description matching the key to read from the event object, content which can be a Stream, ArrayBuffer, string and many others, including a symbol similarly to the path parameter. An optional options object is also accepted (See the native fs.writeFile documentation).

fs.writeFile(path: string | symbol, content: string | ..., options?: WriteFileOptions)
fs.writeFile('./my/file.txt', 'My file content!', 'utf-8')
group([
    set('my-content', 'My file content!'),
    fs.writeFile('./my/file.txt', Symbol('my-content'), { encoding: 'utf-8' })
])

fs.copyFile()

Copies a file from src to dest. src and dest both support regular strings and symbols. The symbols must have descriptions matching keys to read from the event object. Additionally a mode parameter is accepted letting you specify access permissions. See native fs.copyFile for more information.

fs.copyFile(src: string | symbol, dest: string | symbol, mode?: number)
fs.copyFile('./src/file.txt', './dest/file.txt', constants.COPYFILE_EXCL)
group([
    set('src', './src/file.txt'),
    set('dest', './dest/file.txt'),
    fs.copyFile('{{src}}', '{{dest}}')
])

fs.chmod()

Changes access permissions of a file or directory. Accepts a string path or symbol with a description matching the key to read from the event object and mode parameter specifying the item's permissions. See native fs.chmod for more information.

fs.chmod(src: string | symbol, mode: number)
fs.chmod('./path/to/file.js', 0o400)

CP Handlers

Handlers for child process management.
Use them for managing the lifecycle of your child processes, like restarting a child process when a config file it depends on is changed, or when you need to reset their state completely before different actions.

cp.kill()

Kills a child process of a given name if it's alive.
Note: Use the pcs command in the integrated terminal to check the process' status.

cp.kill(process: string | symbol)

cp.revive()

Revives a dead child process of a given name if it's dead.
Note: Use the pcs command in the integrated terminal to check the process' status.

cp.revive(process: string | symbol)

cp.restart()

Restarts a child process of a given name.
Unlike cp.kill and cp.revive, cp.restart works no matter if the child process is alive or dead. If it's dead, it will simply be revived, if it's alive, it will be killed and brought back.

cp.restart(process: string | symbol)

Shell handlers

shell.spawn()

Spawns a temporary child process using the specified command.
Accepts a string command and an options object - An altered version of the options object used by Node's native cp.spawn.

shell.spawn(command: string | string[], options?: SpawnOptions)
// Spawn TSC and compile some code
shell.spawn('tsc')
// Spawn TSC with custom parameters
shell.spawn(['tsc', '--outDir', './dest/'])
// Spawn the process muted
shell.spawn(['tsc', '--outDir', './dest/'], { stdio: 'none' })

The altered options object properties for the shell.spawn handler include:

  • stdio
    • "all" (default) - Pipes stdout and stderr to the terminal.
    • "none" - Mutes the entire process.
    • "takeover" - Temporarily takes over the terminal's output and input, letting you interact with the spawned process until it dies.

JSON handlers

Handlers used primarily for parsing JSON data, whether for manipulating files or parsing fetch request bodies.

json.parse()

Parses a JSON string.
Accepts a string key used to read the data from the event object and optionally a saveTo key, which is used to save the parsed data onto the event object. If saveTo is not provided then the parsed information will be saved back under the same key it was read from.

Additionally, in rare instances where it's needed, a replacer function can be provided for custom parsing directives.

json.parse(key: string, saveTo?: string, replacer?: Function) 
(+3 overloads)
group([
    // Get some data
    set('src', `{ "some": ["data"] }`),
    // Parse the data and save it to `dest`
    json.parse('src', 'dest'),
    // Read the data
    handle(e => console.log(e.get('dest')))
])

json.stringify()

Stringifies an object into a string.
Accepts a string key to read the data from the event object and optionally a saveTo key, which is used to save the resulting string back on the event object. If saveTo is not provided then the string will be saved back under the same key it was read from.

Additionally, in rare instances where it's needed, a replacer function can be provided for custom parsing directives, otherwise an optional space setting is used to specify spaces or tabs used for pretty formatting.

json.stringify(key: string, saveTo?: string, replacer?: Function, space?: number|string) 
(+5 overloads)
group([
    // Get some data
    set('data', { some: ["data"] }),
    // Stringify the object and update the `data` variable
    json.stringify('data'),
    // Read the data
    handle(e => console.log(e.get('data')))
])

Network handlers

Network handlers let you make network requests, ping servers, fetch information, etc.

net.fetch()

Makes a network request using the fetch API.
Accepts string network address and an init object containing all the configuration, such as request type, headers, etc.

Additionally, besides the usual fetch request options, the init object accepts a cb callback function which can be used to access the response and interact with the event object.

net.fetch(input: RequestInfo, init?: RequestInit)
net.fetch('https://github.com', {
    method: "GET",
    headers: { "Content-Type": "text/html" },
    cb: async (res, event) => {
        // Parse the body
        const body = await res.text()
        // Save the body on the event object
        // to further process it in the next event handler.
        event.set('response', body)
    }
})

OS handlers

Operating system handlers that let you interact with the OS.

os.platform()

Runs the callback event handler only if Node's native process.platform's value matches at least one name in specified by the platform parameter.

os.platform(platform: string, handler: EventHandler)
handlers.os.platform('darwin|linux', handlers.handle(event => {
    console.log("We're running on MacOS or Linux!")
}))
0.3.6

4 months ago

0.3.0

6 months ago

0.2.0

6 months ago

0.3.5

6 months ago

0.3.2

6 months ago

0.3.1

6 months ago

0.3.4

6 months ago

0.3.3

6 months ago

0.1.0

9 months ago

0.1.2

9 months ago

0.1.1

9 months ago

0.0.2

9 months ago

0.0.5

9 months ago

0.1.3

8 months ago

0.0.4

9 months ago

0.0.6

9 months ago

0.0.1

1 year ago