| /** | |
| * Describes child processes before they are started. | |
| * | |
| * A `Command` stores the executable, arguments, environment, standard streams, | |
| * working directory, and other process options. Commands can also be piped | |
| * together. A command is an `Effect`; running it asks the | |
| * `ChildProcessSpawner` service to start the process and returns a | |
| * `ChildProcessHandle`. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Context from "../../Context.js"; | |
| import * as Effectable from "../../Effectable.js"; | |
| import { dual } from "../../Function.js"; | |
| import * as Predicate from "../../Predicate.js"; | |
| import { ChildProcessSpawner } from "./ChildProcessSpawner.js"; | |
| const TypeId = "~effect/unstable/process/ChildProcess"; | |
| // ============================================================================= | |
| // Constructors | |
| // ============================================================================= | |
| const Proto = { | |
| ... /*#__PURE__*/Effectable.Prototype({ | |
| label: "Command", | |
| evaluate(fiber) { | |
| return Context.getUnsafe(fiber.context, ChildProcessSpawner).spawn(this); | |
| } | |
| }), | |
| [TypeId]: TypeId | |
| }; | |
| /** | |
| * Checks whether a value is a `Command`. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isCommand = u => Predicate.hasProperty(u, TypeId); | |
| /** | |
| * Checks whether a command is a `StandardCommand`. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isStandardCommand = command => command._tag === "StandardCommand"; | |
| /** | |
| * Checks whether a command is a `PipedCommand`. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isPipedCommand = command => command._tag === "PipedCommand"; | |
| const makeStandardCommand = (command, args, options) => Object.assign(Object.create(Proto), { | |
| _tag: "StandardCommand", | |
| command, | |
| args, | |
| options | |
| }); | |
| const makePipedCommand = (left, right, options = {}) => Object.assign(Object.create(Proto), { | |
| _tag: "PipedCommand", | |
| left, | |
| right, | |
| options | |
| }); | |
| /** | |
| * Create a command from a template literal, options + template, or array form. | |
| * | |
| * **Details** | |
| * | |
| * This function supports three calling conventions: | |
| * 1. Template literal: `make\`npm run build\`` | |
| * 2. Options + template literal: `make({ cwd: "/app" })\`npm run build\`` | |
| * 3. Array form: `make("npm", ["run", "build"], options?)` | |
| * | |
| * Template literals are not parsed until execution time, allowing parsing | |
| * errors to flow through Effect's error channel. | |
| * | |
| * **Example** (Creating commands) | |
| * | |
| * ```ts | |
| * import { ChildProcess } from "effect/unstable/process" | |
| * | |
| * // Template literal form | |
| * const cmd1 = ChildProcess.make`echo "hello"` | |
| * | |
| * // With options | |
| * const cmd2 = ChildProcess.make({ cwd: "/tmp" })`ls -la` | |
| * | |
| * // Array form | |
| * const cmd3 = ChildProcess.make("git", ["status"]) | |
| * ``` | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const make = function make(...args) { | |
| // Template literal form: make`command` | |
| if (isTemplateString(args[0])) { | |
| const [templates, ...expressions] = args; | |
| const tokens = parseTemplates(templates, expressions); | |
| return makeStandardCommand(tokens[0] ?? "", tokens.slice(1), {}); | |
| } | |
| // Options form: make({ cwd: "/tmp" })`command` | |
| if (typeof args[0] === "object" && !Array.isArray(args[0]) && !isTemplateString(args[0])) { | |
| const options = args[0]; | |
| return function (templates, ...expressions) { | |
| const tokens = parseTemplates(templates, expressions); | |
| return makeStandardCommand(tokens[0] ?? "", tokens.slice(1), options); | |
| }; | |
| } | |
| // Standard form without arguments: make("command", options?) | |
| if (typeof args[0] === "string" && !Array.isArray(args[1])) { | |
| const [command, options = {}] = args; | |
| return makeStandardCommand(command, [], options); | |
| } | |
| // Standard form with arguments: make("command", ["arg1", "arg2"], options?) | |
| const [command, cmdArgs = [], options = {}] = args; | |
| return makeStandardCommand(command, cmdArgs, options); | |
| }; | |
| /** | |
| * Pipes the output of one command to the input of another. | |
| * | |
| * **Details** | |
| * | |
| * By default, pipes `stdout` from the source to `stdin` of the destination. | |
| * Use the `options` parameter to customize which streams are connected. | |
| * | |
| * **Example** (Piping command output) | |
| * | |
| * ```ts | |
| * import { ChildProcess } from "effect/unstable/process" | |
| * | |
| * // Pipe stdout (default) | |
| * const pipeline1 = ChildProcess.make`cat file.txt`.pipe( | |
| * ChildProcess.pipeTo(ChildProcess.make`grep pattern`) | |
| * ) | |
| * | |
| * // Pipe stderr instead of stdout | |
| * const pipeline2 = ChildProcess.make`my-program`.pipe( | |
| * ChildProcess.pipeTo(ChildProcess.make`grep error`, { from: "stderr" }) | |
| * ) | |
| * | |
| * // Pipe combined stdout and stderr | |
| * const pipeline3 = ChildProcess.make`my-program`.pipe( | |
| * ChildProcess.pipeTo(ChildProcess.make`tee output.log`, { from: "all" }) | |
| * ) | |
| * ``` | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const pipeTo = /*#__PURE__*/dual(args => isCommand(args[0]) && isCommand(args[1]), (self, that, options) => makePipedCommand(self, that, options ?? {})); | |
| /** | |
| * Prepends another command to a command. | |
| * | |
| * **Details** | |
| * | |
| * For pipelines, only the leftmost command is prefixed. | |
| * | |
| * **Example** (Prefixing commands) | |
| * | |
| * ```ts | |
| * import { ChildProcess } from "effect/unstable/process" | |
| * | |
| * const command = ChildProcess.make`echo "foo"` | |
| * | |
| * const prefixed = command.pipe( | |
| * ChildProcess.prefix`time` | |
| * ) | |
| * | |
| * // now prefixed will execute `time echo "foo"` | |
| * ``` | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const prefix = function prefix(...args) { | |
| if (isCommand(args[0]) && args.length > 1) { | |
| const [self, ...rest] = args; | |
| const prefixSpec = parsePrefixArgs(rest); | |
| return applyPrefix(self, prefixSpec); | |
| } | |
| const prefixSpec = parsePrefixArgs(args); | |
| return self => applyPrefix(self, prefixSpec); | |
| }; | |
| const parsePrefixArgs = args => { | |
| if (isTemplateString(args[0])) { | |
| const [templates, ...expressions] = args; | |
| const tokens = parseTemplates(templates, expressions); | |
| return { | |
| command: tokens[0] ?? "", | |
| args: tokens.slice(1) | |
| }; | |
| } | |
| const [command, cmdArgs = []] = args; | |
| return { | |
| command, | |
| args: cmdArgs | |
| }; | |
| }; | |
| const applyPrefix = (self, prefixSpec) => { | |
| switch (self._tag) { | |
| case "StandardCommand": | |
| { | |
| return makeStandardCommand(prefixSpec.command, [...prefixSpec.args, self.command, ...self.args], self.options); | |
| } | |
| case "PipedCommand": | |
| { | |
| return makePipedCommand(applyPrefix(self.left, prefixSpec), self.right, self.options); | |
| } | |
| } | |
| }; | |
| /** | |
| * Sets the current working directory for a command. | |
| * | |
| * **Details** | |
| * | |
| * For pipelines, applies to each command in the pipeline. | |
| * | |
| * **Example** (Setting command working directories) | |
| * | |
| * ```ts | |
| * import { ChildProcess } from "effect/unstable/process" | |
| * | |
| * const cmd = ChildProcess.make`ls -la`.pipe( | |
| * ChildProcess.setCwd("/tmp") | |
| * ) | |
| * ``` | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const setCwd = /*#__PURE__*/dual(2, (self, cwd) => { | |
| switch (self._tag) { | |
| case "StandardCommand": | |
| { | |
| return makeStandardCommand(self.command, self.args, { | |
| ...self.options, | |
| cwd | |
| }); | |
| } | |
| case "PipedCommand": | |
| { | |
| return makePipedCommand(setCwd(self.left, cwd), setCwd(self.right, cwd), self.options); | |
| } | |
| } | |
| }); | |
| /** | |
| * Adds environment variables to a command, merging them with any existing | |
| * command environment and overriding duplicate keys. | |
| * | |
| * **Details** | |
| * | |
| * For pipelines, applies to each command in the pipeline. | |
| * | |
| * **Example** (Setting command environment variables) | |
| * | |
| * ```ts | |
| * import { ChildProcess } from "effect/unstable/process" | |
| * | |
| * const cmd = ChildProcess.make`node script.js`.pipe( | |
| * ChildProcess.setEnv({ NODE_ENV: "test" }) | |
| * ) | |
| * ``` | |
| * | |
| * @category combinators | |
| * @since 4.0.0 | |
| */ | |
| export const setEnv = /*#__PURE__*/dual(2, (self, env) => { | |
| switch (self._tag) { | |
| case "StandardCommand": | |
| { | |
| const nextEnv = self.options.env === undefined ? env : { | |
| ...self.options.env, | |
| ...env | |
| }; | |
| return makeStandardCommand(self.command, self.args, { | |
| ...self.options, | |
| env: nextEnv | |
| }); | |
| } | |
| case "PipedCommand": | |
| { | |
| return makePipedCommand(setEnv(self.left, env), setEnv(self.right, env), self.options); | |
| } | |
| } | |
| }); | |
| const isTemplateString = u => Array.isArray(u) && "raw" in u && Array.isArray(u.raw); | |
| // ============================================================================= | |
| // Utilities | |
| // ============================================================================= | |
| /** | |
| * Parses an fd name like "fd3" to its numeric index. | |
| * Returns undefined if the name is invalid. | |
| * | |
| * @category converting | |
| * @since 4.0.0 | |
| */ | |
| export const parseFdName = name => { | |
| const match = /^fd(\d+)$/.exec(name); | |
| if (match === null) return undefined; | |
| const fd = parseInt(match[1], 10); | |
| return fd >= 3 ? fd : undefined; | |
| }; | |
| /** | |
| * Create an fd name from its numeric index. | |
| * | |
| * @category converting | |
| * @since 4.0.0 | |
| */ | |
| export const fdName = fd => `fd${fd}`; | |
| // ============================================================================= | |
| // Template Parsing | |
| // ============================================================================= | |
| const parseTemplates = (templates, expressions) => { | |
| let tokens = []; | |
| for (const [index, template] of templates.entries()) { | |
| tokens = parseTemplate(templates, expressions, tokens, template, index); | |
| } | |
| return tokens; | |
| }; | |
| const parseTemplate = (templates, expressions, prevTokens, template, index) => { | |
| const rawTemplate = templates.raw[index]; | |
| if (rawTemplate === undefined) { | |
| throw new Error(`Invalid backslash sequence: ${templates.raw[index]}`); | |
| } | |
| const { | |
| hasLeadingWhitespace, | |
| hasTrailingWhitespace, | |
| tokens | |
| } = splitByWhitespaces(template, rawTemplate); | |
| const nextTokens = concatTokens(prevTokens, tokens, hasLeadingWhitespace); | |
| if (index === expressions.length) { | |
| return nextTokens; | |
| } | |
| const expression = expressions[index]; | |
| const expressionTokens = Array.isArray(expression) ? expression.map(expression => parseExpression(expression)) : [parseExpression(expression)]; | |
| return concatTokens(nextTokens, expressionTokens, hasTrailingWhitespace); | |
| }; | |
| /** | |
| * Convert valid expressions defined in a template string command (i.e. using | |
| * `${expression}` into strings. | |
| */ | |
| const parseExpression = expression => { | |
| const type = typeof expression; | |
| if (type === "string") { | |
| return expression; // Return strings directly | |
| } | |
| return String(expression); // Convert numbers to strings | |
| }; | |
| const DELIMITERS = /*#__PURE__*/new Set([" ", "\t", "\r", "\n"]); | |
| /** | |
| * Number of characters in backslash escape sequences: \0 \xXX or \uXXXX | |
| * \cX is allowed in RegExps but not in strings | |
| * Octal sequences are not allowed in strict mode | |
| */ | |
| const ESCAPE_LENGTH = { | |
| x: 3, | |
| u: 5 | |
| }; | |
| /** | |
| * Splits a template string by whitespace while also properly handling escape | |
| * sequences. | |
| * | |
| * As an example, let's review the following valid commands: | |
| * | |
| * ```ts | |
| * ChildProcess.exec`echo foo\n bar` | |
| * // We should run `["echo", "foo\n", "bar"]` | |
| * | |
| * ChildProcess.exec`echo foo | |
| * bar` | |
| * // We should run `["echo", "foo", "bar]` | |
| * ``` | |
| * | |
| * The problem is that when we evaluate the template string for both of the above | |
| * commands, we will end up with the same string "echo foo\n bar". | |
| * | |
| * What we really want is to include the escaped character in the arguments for | |
| * the first command, since it was written explicitly by the user. | |
| * | |
| * This is why also having access to the raw template string is useful - in a | |
| * template string, there are two representations of the same string: | |
| * 1. `template` - The processed string (escape sequences are evaluated). | |
| * 2. `template.raw` - The raw string (escape sequences are literal). | |
| */ | |
| const splitByWhitespaces = (template, rawTemplate) => { | |
| if (rawTemplate.length === 0) { | |
| return { | |
| tokens: [], | |
| hasLeadingWhitespace: false, | |
| hasTrailingWhitespace: false | |
| }; | |
| } | |
| const hasLeadingWhitespace = DELIMITERS.has(rawTemplate[0]); | |
| const tokens = []; | |
| // Given that escape sequences will have different lengths in the template | |
| // versus the raw template, we must maintain two indices: | |
| // - One for the index into the template string | |
| // - One for the index into the raw template string | |
| // We also maintain the current cursor position for where we are in the template | |
| let templateCursor = 0; | |
| for (let templateIndex = 0, rawIndex = 0; templateIndex < template.length; templateIndex += 1, rawIndex += 1) { | |
| // Use the raw template character to check for actual whitespace | |
| const rawCharacter = rawTemplate[rawIndex]; | |
| if (DELIMITERS.has(rawCharacter)) { | |
| // Whitespace found, extract token from template if necessary | |
| if (templateCursor !== templateIndex) { | |
| tokens.push(template.slice(templateCursor, templateIndex)); | |
| } | |
| // Advance the template start index to the current position | |
| templateCursor = templateIndex + 1; | |
| } else if (rawCharacter === "\\") { | |
| // Escape sequence detected, check the next raw character | |
| const nextRawCharacter = rawTemplate[rawIndex + 1]; | |
| if (nextRawCharacter === "\n") { | |
| // Handle `\` character followed by a newline (i.e. a line continuation) by: | |
| // - Reversing the template index (backslash-newline is erased in template) | |
| // - Advancing the raw template index past the line continuation | |
| templateIndex -= 1; | |
| rawIndex += 1; | |
| } else if (nextRawCharacter === "u" && rawTemplate[rawIndex + 2] === "{") { | |
| // Handle variable-length unicode escape sequences (i.e. `\u{1F600}`) by: | |
| // - Advancing the raw template index past the unicode escape sequence | |
| rawIndex = rawTemplate.indexOf("}", rawIndex + 3); | |
| } else { | |
| // Advance raw template index past fixed-length escape sequences: | |
| // - \n → 2 chars (backslash + n) | |
| // - \t → 2 chars (backslash + t) | |
| // - \xHH → 4 chars (backslash + x + H + H) | |
| // - \uHHHH → 6 chars (backslash + u + H + H + H + H) | |
| rawIndex += ESCAPE_LENGTH[nextRawCharacter] ?? 1; | |
| } | |
| } | |
| } | |
| // Trailing whitespace only exists if the template cursor is equivalent to the | |
| // length of the template | |
| const hasTrailingWhitespace = templateCursor === template.length; | |
| // If we did not end with trailing whitespace, ensure the final token is added | |
| if (!hasTrailingWhitespace) { | |
| tokens.push(template.slice(templateCursor)); | |
| } | |
| return { | |
| tokens, | |
| hasLeadingWhitespace, | |
| hasTrailingWhitespace | |
| }; | |
| }; | |
| /** | |
| * Concatenates two separate sets of string tokens together. | |
| * | |
| * If either set is empty or `isSeparated=false`, the last element of `prevTokens` | |
| * and the first element of `nextTokens` will be joined into a single token. | |
| */ | |
| const concatTokens = (prevTokens, nextTokens, isSeparated) => isSeparated || prevTokens.length === 0 || nextTokens.length === 0 | |
| // Keep the previous and next tokens separate from one another | |
| ? [...prevTokens, ...nextTokens] | |
| // Join the last token from the previous set and the first token from the next set | |
| : [...prevTokens.slice(0, -1), `${prevTokens.at(-1)}${nextTokens.at(0)}`, ...nextTokens.slice(1)]; | |
| //# sourceMappingURL=ChildProcess.js.map |
Xet Storage Details
- Size:
- 15.2 kB
- Xet hash:
- 524d76ce70d24119f4824c9ed9c5ed3c3f21a1c17f7ccf1b50c1a493b69dcbab
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.