Pages.Script

An elm-pages Script is a way to execute an elm-pages BackendTask.

Read more about using the elm-pages CLI to run (or bundle) scripts, plus a brief tutorial, at https://elm-pages.com/docs/elm-pages-scripts.

type alias Script =
Script

The type for your run function that can be executed by elm-pages run.

Defining Scripts

Config cliOptions
-> (cliOptions -> BackendTask FatalError ())
-> Script

Same as withoutCliOptions, but allows you to define a CLI Options Parser so the user can pass in additional options for the script.

Uses https://package.elm-lang.org/packages/dillonkearns/elm-cli-options-parser/latest/.

Read more at https://elm-pages.com/docs/elm-pages-scripts/#adding-command-line-options.

withoutCliOptions : BackendTask FatalError () -> Script

Define a simple Script (no CLI Options).

module MyScript exposing (run)

import BackendTask
import Pages.Script as Script

run =
    Script.withoutCliOptions
        (Script.log "Hello!"
            |> BackendTask.allowFatal
        )

Paths

File system APIs in this module use FilePath.

File System Utilities

{ body : String }
-> BackendTask
{ fatal : FatalError
, recoverable : Error
}

Write a file to the file system. Returns the absolute resolved path to the written file for chaining.

File paths are relative to the root of your elm-pages project (next to the elm.json file and src/ directory), or you can pass in absolute paths beginning with a /.

module MyScript exposing (run)

import BackendTask
import FilePath exposing (FilePath)
import Pages.Script as Script

run =
    Script.withoutCliOptions
        (Script.writeFile
            { body = """{ "message": "Hello, World!" }""" }
            (FilePath.fromString "hello.json")
            |> BackendTask.allowFatal
            |> BackendTask.andThen
                (\writtenPath ->
                    Script.log ("Wrote " ++ FilePath.toString writtenPath)
                )
        )
deleteFile : FilePath -> BackendTask FatalError ()

Delete a file. Silently succeeds if the file doesn't exist (like rm -f).

import FilePath exposing (FilePath)

Script.writeFile { body = "..." } (FilePath.fromString "temp.txt")
    |> BackendTask.allowFatal
    |> BackendTask.andThen (\_ -> Script.deleteFile (FilePath.fromString "temp.txt"))
copyFile : { to : FilePath } -> FilePath -> BackendTask FatalError FilePath

Copy a single file. Auto-creates parent directories of the destination (matching writeFile behavior). Returns the absolute resolved destination path for chaining.

import FilePath exposing (FilePath)

FilePath.fromString "src/config.json"
    |> Script.copyFile { to = FilePath.fromString "dist/config.json" }
move : { to : FilePath } -> FilePath -> BackendTask FatalError FilePath

Move (rename) a file or directory. Atomic on the same filesystem. Auto-creates parent directories of the destination. Returns the absolute resolved destination path for chaining.

import FilePath exposing (FilePath)

FilePath.fromString "build/output.js"
    |> Script.move { to = FilePath.fromString "dist/app.js" }
makeDirectory : { recursive : Bool } -> FilePath -> BackendTask FatalError FilePath

Create a directory. Returns the absolute resolved path.

The { recursive : Bool } flag controls whether parent directories are created (like mkdir -p).

import FilePath exposing (FilePath)

-- Create nested directories
Script.makeDirectory { recursive = True } (FilePath.fromString "dist/assets/images")

-- Create a single directory (parent must exist)
Script.makeDirectory { recursive = False } (FilePath.fromString "output")
removeDirectory : { recursive : Bool } -> FilePath -> BackendTask FatalError ()

Remove a directory. Silently succeeds if the directory doesn't exist.

The explicit { recursive : Bool } flag makes dangerous rm -rf behavior opt-in — you must consciously choose recursive removal of non-empty directories.

import FilePath exposing (FilePath)

-- Remove a directory and all its contents
Script.removeDirectory { recursive = True } (FilePath.fromString "build")

-- Remove only if empty
Script.removeDirectory { recursive = False } (FilePath.fromString "empty-dir")
makeTempDirectory : String -> BackendTask FatalError FilePath

Create a temporary directory with a given prefix. Returns the absolute path to the created directory.

Pairs naturally with BackendTask.finally for cleanup:

import FilePath exposing (FilePath)

Script.makeTempDirectory "my-build-"
    |> BackendTask.andThen
        (\tmpDir ->
            doWork tmpDir
                |> BackendTask.finally
                    (Script.removeDirectory { recursive = True } tmpDir)
        )

Shell Commands

command : String -> List String -> BackendTask FatalError String

Run a single command and return stderr and stdout combined as a single String.

If you want to do more advanced things like piping together multiple commands in a pipeline, or piping in a file to a command, etc., see the Stream module.

module MyScript exposing (run)

import BackendTask
import Pages.Script as Script exposing (Script)

run : Script
run =
    Script.withoutCliOptions
        (Script.command "ls" []
            |> BackendTask.andThen
                (\files ->
                    Script.log ("Files: " ++ files)
                )
        )
exec : String -> List String -> BackendTask FatalError ()

Like command, but prints stderr and stdout to the console as the command runs instead of capturing them.

module MyScript exposing (run)

import BackendTask
import Pages.Script as Script exposing (Script)

run : Script
run =
    Script.withoutCliOptions
        (Script.exec "ls" [])

Utilities

log : String -> BackendTask error ()

Log to stdout.

module MyScript exposing (run)

import BackendTask
import Pages.Script as Script

run =
    Script.withoutCliOptions
        (Script.log "Hello!"
            |> BackendTask.allowFatal
        )
sleep : Int -> BackendTask error ()

Sleep for a number of milliseconds.

module MyScript exposing (run)

import BackendTask
import Pages.Script as Script

run =
    Script.withoutCliOptions
        (Script.log "Hello..."
            |> Script.doThen
                (Script.sleep 1000)
            |> Script.doThen
                (Script.log "World!")
        )
BackendTask error value
-> BackendTask error ()
-> BackendTask error value

Run a command with no output, then run another command.

module MyScript exposing (run)

import BackendTask
import Pages.Script as Script

run =
    Script.withoutCliOptions
        (Script.log "Hello!"
            |> Script.doThen
                (Script.log "World!")
        )
which : String -> BackendTask error (Maybe String)

Same as expectWhich, but returns Nothing if the command is not found instead of failing with a FatalError.

expectWhich : String -> BackendTask FatalError String

Check if a command is available on the system. If it is, return the full path to the command, otherwise fail with a FatalError.

module MyScript exposing (run)

import BackendTask
import Pages.Script as Script

run : Script
run =
    Script.withoutCliOptions
        (Script.expectWhich "elm-review"
            |> BackendTask.andThen
                (\path ->
                    Script.log ("The path to `elm-review` is: " ++ path)
                )
        )

If you run it with a command that is not available, you will see an error like this:

Script.expectWhich "hype-script"
-- COMMAND NOT FOUND ---------------
I expected to find `hype-script`, but it was not on your PATH. Make sure it is installed and included in your PATH.
question : String -> BackendTask error String
module QuestionDemo exposing (run)

import BackendTask

run : Script
run =
    Script.withoutCliOptions
        (Script.question "What is your name? "
            |> BackendTask.andThen
                (\name ->
                    Script.log ("Hello, " ++ name ++ "!")
                )
        )
readKey : BackendTask error String

Read a single keypress from stdin without requiring Enter.

This is useful for interactive prompts where you want immediate response to a single key, like confirmation dialogs (y/n) or menu navigation.

module ConfirmDemo exposing (run)

import BackendTask

run : Script
run =
    Script.withoutCliOptions
        (Script.log "Approve this change? [y/n] "
            |> BackendTask.andThen (\_ -> Script.readKey)
            |> BackendTask.andThen
                (\key ->
                    if key == "y" then
                        Script.log "Approved!"

                    else
                        Script.log "Rejected."
                )
        )

Note: Returns the raw key character. Control characters like Ctrl+C will terminate the process.

When not running in an interactive terminal (e.g., piped input or CI), falls back to line-buffered input and returns the first character of the line. This allows scripts to work both interactively and with piped input like echo "y" | elm-pages run MyScript.elm.

readKeyWithDefault : String -> BackendTask error String

Like readKey, but returns a default value when Enter is pressed.

Script.log "Continue? [Y/n] "
    |> BackendTask.andThen (\_ -> Script.readKeyWithDefault "y")
    |> BackendTask.andThen
        (\key ->
            if String.toLower key == "y" then
                continue

            else
                abort
        )

Useful for prompts where pressing Enter should accept a default option.

Errors

type Error
= FileWriteError

The recoverable error type for file writes. You can use BackendTask.allowFatal if you want to allow the program to crash with an error message if a file write is unsuccessful.