diff --git a/packages/platform/README.md b/packages/platform/README.md index 8113be2368..6f8e8c7523 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -1,64 +1,253 @@ -# Effect +# Introduction -Welcome to Effect, a powerful TypeScript framework that provides a fully-fledged functional effect system with a rich standard library. +Welcome to the documentation for `@effect/platform`, a library designed for creating platform-independent abstractions (Node.js, Bun, browsers). -# Requirements +With `@effect/platform`, you can incorporate abstract services like `Terminal` or `FileSystem` into your program. Later, during the assembly of the final application, you can provide specific layers for the target platform using the corresponding packages: `platform-node`, `platform-bun`, and `platform-browser`. -- TypeScript 5.0 or newer -- The `strict` flag enabled in your `tsconfig.json` file +This package empowers you to perform various operations, such as: -``` -{ - // ... - "compilerOptions": { - // ... - "strict": true, - } -} -``` +| **Operation** | **Description** | +| -------------- | ------------------------------------------------------------------------------------------------ | +| Terminal | Reading and writing from/to standard input/output | +| Command | Creating and running a command with the specified process name and an optional list of arguments | +| FileSystem | Reading and writing from/to the file system | +| HTTP Client | Sending HTTP requests and receiving responses | +| HTTP Server | Creating HTTP servers to handle incoming requests | +| HTTP Router | Routing HTTP requests to specific handlers | +| KeyValueStore | Storing and retrieving key-value pairs | +| PlatformLogger | Creating a logger that writes to a specified file from another string logger | -## Documentation +By utilizing `@effect/platform`, you can write code that remains platform-agnostic, ensuring compatibility across different environments. -For detailed information and usage examples, please visit the [Effect website](https://www.effect.website/). +# Terminal -## Introduction to Effect +The `@effect/platform/Terminal` module exports a single `Terminal` tag, which serves as the entry point to reading from and writing to standard input and standard output. -To get started with Effect, watch our introductory video on YouTube. This video provides an overview of Effect and its key features, making it a great starting point for newcomers: +## Writing to standard output -[![Introduction to Effect](https://img.youtube.com/vi/SloZE4i4Zfk/maxresdefault.jpg)](https://youtu.be/SloZE4i4Zfk) +```ts +import { Terminal } from "@effect/platform"; +import { NodeRuntime, NodeTerminal } from "@effect/platform-node"; +import { Effect } from "effect"; -## Connect with Our Community +// const program: Effect.Effect +const program = Effect.gen(function* (_) { + const terminal = yield* _(Terminal.Terminal); + yield* _(terminal.display("a message\n")); +}); -Join our vibrant community on Discord to interact with fellow developers, ask questions, and share your experiences. Here's the invite link to our Discord server: [Join Effect's Discord Community](https://discord.gg/hdt7t7jpvn). +NodeRuntime.runMain(program.pipe(Effect.provide(NodeTerminal.layer))); +// Output: "a message" +``` -## API Reference +## Reading from standard input -For detailed information on the Effect API, please refer to our [API Reference](https://effect-ts.github.io/effect/). +```ts +import { Terminal } from "@effect/platform"; +import { NodeRuntime, NodeTerminal } from "@effect/platform-node"; +import { Console, Effect } from "effect"; -## Pull Requests +// const program: Effect.Effect +const program = Effect.gen(function* (_) { + const terminal = yield* _(Terminal.Terminal); + const input = yield* _(terminal.readLine); + yield* _(Console.log(`input: ${input}`)); +}); -We welcome contributions via pull requests! Here are some guidelines to help you get started: +NodeRuntime.runMain(program.pipe(Effect.provide(NodeTerminal.layer))); +// Input: "hello" +// Output: "input: hello" +``` -1. Fork the repository and clone it to your local machine. -2. Create a new branch for your changes: `git checkout -b my-new-feature`. -3. Ensure you have the required dependencies installed by running: `pnpm install` (assuming pnpm version `8.x`). -4. Make your desired changes and, if applicable, include tests to validate your modifications. -5. Run the following commands to ensure the integrity of your changes: - - `pnpm check`: Verify that the code compiles. - - `pnpm test`: Execute the tests. - - `pnpm circular`: Confirm there are no circular imports. - - `pnpm lint`: Check for code style adherence (if you happen to encounter any errors during this process, you can use `pnpm lint-fix` to automatically fix some of these style issues). - - `pnpm dtslint`: Run type-level tests. - - `pnpm docgen`: Update the automatically generated documentation. -6. Create a changeset for your changes: before committing your changes, create a changeset to document the modifications. This helps in tracking and communicating the changes effectively. To create a changeset, run the following command: `pnpm changeset`. Always choose the `patch` option when prompted (please note that we are currently in pre-release mode). -7. Commit your changes: after creating the changeset, commit your changes with a descriptive commit message: `git commit -am 'Add some feature'`. -8. Push your changes to your fork: `git push origin my-new-feature`. -9. Open a pull request against our `main` branch. +These simple examples illustrate how to utilize the `Terminal` module for handling standard input and output in your programs. Let's use this knowledge to build a number guessing game: + +```ts +import { Terminal } from "@effect/platform"; +import type { PlatformError } from "@effect/platform/Error"; +import { Effect, Option, Random } from "effect"; + +export const secret = Random.nextIntBetween(1, 100); + +const parseGuess = (input: string) => { + const n = parseInt(input, 10); + return isNaN(n) || n < 1 || n > 100 ? Option.none() : Option.some(n); +}; + +const display = (message: string) => + Effect.gen(function* (_) { + const terminal = yield* _(Terminal.Terminal); + yield* _(terminal.display(`${message}\n`)); + }); + +const prompt = Effect.gen(function* (_) { + const terminal = yield* _(Terminal.Terminal); + yield* _(terminal.display("Enter a guess: ")); + return yield* _(terminal.readLine); +}); + +const answer: Effect.Effect< + number, + Terminal.QuitException | PlatformError, + Terminal.Terminal +> = Effect.gen(function* (_) { + const input = yield* _(prompt); + const guess = parseGuess(input); + if (Option.isNone(guess)) { + yield* _(display("You must enter an integer from 1 to 100")); + return yield* _(answer); + } + return guess.value; +}); + +const check = ( + secret: number, + guess: number, + ok: Effect.Effect, + ko: Effect.Effect +): Effect.Effect => + Effect.gen(function* (_) { + if (guess > secret) { + yield* _(display("Too high")); + return yield* _(ko); + } else if (guess < secret) { + yield* _(display("Too low")); + return yield* _(ko); + } else { + return yield* _(ok); + } + }); + +const end = display("You guessed it!"); + +const loop = ( + secret: number +): Effect.Effect< + void, + Terminal.QuitException | PlatformError, + Terminal.Terminal +> => + Effect.gen(function* (_) { + const guess = yield* _(answer); + return yield* _(check(secret, guess, end, loop(secret))); + }); + +export const game = Effect.gen(function* (_) { + yield* _( + display( + "We have selected a random number between 1 and 100. See if you can guess it in 10 turns or fewer. We'll tell you if your guess was too high or too low." + ) + ); + yield* _(loop(yield* _(secret))); +}); +``` -### Pull Request Guidelines +Let's run the game in Node.js: -- Please make sure your changes are consistent with the project's existing style and conventions. -- Please write clear commit messages and include a summary of your changes in the pull request description. -- Please make sure all tests pass and add new tests as necessary. -- If your change requires documentation, please update the relevant documentation. -- Please be patient! We will do our best to review your pull request as soon as possible. +```ts +import { NodeRuntime, NodeTerminal } from "@effect/platform-node"; +import * as Effect from "effect/Effect"; +import { game } from "./game.js"; + +NodeRuntime.runMain(game.pipe(Effect.provide(NodeTerminal.layer))); +``` + +Let's run the game in Bun: + +```ts +import { BunRuntime, BunTerminal } from "@effect/platform-bun"; +import * as Effect from "effect/Effect"; +import { game } from "./game.js"; + +BunRuntime.runMain(game.pipe(Effect.provide(BunTerminal.layer))); +``` + +# Command + +As an example of using the `@effect/platform/Command` module, let's see how to run the TypeScript compiler `tsc`: + +```ts +import { Command, CommandExecutor } from "@effect/platform"; +import { + NodeCommandExecutor, + NodeFileSystem, + NodeRuntime, +} from "@effect/platform-node"; +import { Effect } from "effect"; + +// const program: Effect.Effect +const program = Effect.gen(function* (_) { + const executor = yield* _(CommandExecutor.CommandExecutor); + + // Creating a command to run the TypeScript compiler + const command = Command.make("tsc", "--noEmit"); + console.log("Running tsc..."); + + // Executing the command and capturing the output + const output = yield* _(executor.string(command)); + console.log(output); + return output; +}); + +// Running the program with the necessary runtime and executor layers +NodeRuntime.runMain( + program.pipe( + Effect.provide(NodeCommandExecutor.layer), + Effect.provide(NodeFileSystem.layer) + ) +); +``` + +# FileSystem + +The `@effect/platform/FileSystem` module provides a single `FileSystem` tag, which acts as the gateway for interacting with the filesystem. + +Here's a list of operations that can be performed using the `FileSystem` tag: + +| **Name** | **Arguments** | **Return** | **Description** | +| --------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **access** | `path: string`, `options?: AccessFileOptions` | `Effect` | Check if a file can be accessed. You can optionally specify the level of access to check for. | +| **copy** | `fromPath: string`, `toPath: string`, `options?: CopyOptions` | `Effect` | Copy a file or directory from `fromPath` to `toPath`. Equivalent to `cp -r`. | +| **copyFile** | `fromPath: string`, `toPath: string` | `Effect` | Copy a file from `fromPath` to `toPath`. | +| **chmod** | `path: string`, `mode: number` | `Effect` | Change the permissions of a file. | +| **chown** | `path: string`, `uid: number`, `gid: number` | `Effect` | Change the owner and group of a file. | +| **exists** | `path: string` | `Effect` | Check if a path exists. | +| **link** | `fromPath: string`, `toPath: string` | `Effect` | Create a hard link from `fromPath` to `toPath`. | +| **makeDirectory** | `path: string`, `options?: MakeDirectoryOptions` | `Effect` | Create a directory at `path`. You can optionally specify the mode and whether to recursively create nested directories. | +| **makeTempDirectory** | `options?: MakeTempDirectoryOptions` | `Effect` | Create a temporary directory. By default, the directory will be created inside the system's default temporary directory. | +| **makeTempDirectoryScoped** | `options?: MakeTempDirectoryOptions` | `Effect` | Create a temporary directory inside a scope. Functionally equivalent to `makeTempDirectory`, but the directory will be automatically deleted when the scope is closed. | +| **makeTempFile** | `options?: MakeTempFileOptions` | `Effect` | Create a temporary file. The directory creation is functionally equivalent to `makeTempDirectory`. The file name will be a randomly generated string. | +| **makeTempFileScoped** | `options?: MakeTempFileOptions` | `Effect` | Create a temporary file inside a scope. Functionally equivalent to `makeTempFile`, but the file will be automatically deleted when the scope is closed. | +| **open** | `path: string`, `options?: OpenFileOptions` | `Effect` | Open a file at `path` with the specified `options`. The file handle will be automatically closed when the scope is closed. | +| **readDirectory** | `path: string`, `options?: ReadDirectoryOptions` | `Effect, PlatformError>` | List the contents of a directory. You can recursively list the contents of nested directories by setting the `recursive` option. | +| **readFile** | `path: string` | `Effect` | Read the contents of a file. | +| **readFileString** | `path: string`, `encoding?: string` | `Effect` | Read the contents of a file as a string. | +| **readLink** | `path: string` | `Effect` | Read the destination of a symbolic link. | +| **realPath** | `path: string` | `Effect` | Resolve a path to its canonicalized absolute pathname. | +| **remove** | `path: string`, `options?: RemoveOptions` | `Effect` | Remove a file or directory. By setting the `recursive` option to `true`, you can recursively remove nested directories. | +| **rename** | `oldPath: string`, `newPath: string` | `Effect` | Rename a file or directory. | +| **sink** | `path: string`, `options?: SinkOptions` | `Sink` | Create a writable `Sink` for the specified `path`. | +| **stat** | `path: string` | `Effect` | Get information about a file at `path`. | +| **stream** | `path: string`, `options?: StreamOptions` | `Stream` | Create a readable `Stream` for the specified `path`. | +| **symlink** | `fromPath: string`, `toPath: string` | `Effect` | Create a symbolic link from `fromPath` to `toPath`. | +| **truncate** | `path: string`, `length?: SizeInput` | `Effect` | Truncate a file to a specified length. If the `length` is not specified, the file will be truncated to length `0`. | +| **utimes** | `path: string`, `atime: Date \| number`, `mtime: Date \| number` | `Effect` | Change the file system timestamps of the file at `path`. | +| **watch** | `path: string` | `Stream` | Watch a directory or file for changes. | + +Let's explore a simple example using `readFileString`: + +```ts +import { FileSystem } from "@effect/platform"; +import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"; +import { Effect } from "effect"; + +// const program: Effect.Effect +const program = Effect.gen(function* (_) { + const fs = yield* _(FileSystem.FileSystem); + + // Reading the content of the same file where this code is written + const content = yield* _(fs.readFileString("./index.ts", "utf8")); + console.log(content); +}); + +NodeRuntime.runMain(program.pipe(Effect.provide(NodeFileSystem.layer))); +```