Skip to content

Commit

Permalink
Test tokens.
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj committed Jun 28, 2024
1 parent 737ce23 commit da28bea
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 81 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/config/src/config_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use moon_common::path::hash_component;
use rustc_hash::FxHashMap;
use schematic::{Cacher, ConfigError};
use std::fs;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::time::{Duration, SystemTime};

pub struct ConfigCache {
Expand Down
1 change: 1 addition & 0 deletions crates/project-expander/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository = "https://github.com/moonrepo/moon"
publish = false

[dependencies]
moon_args = { path = "../args" }
moon_common = { path = "../common" }
moon_config = { path = "../config" }
moon_project = { path = "../project" }
Expand Down
41 changes: 23 additions & 18 deletions crates/project-expander/src/token_expander.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::expander_context::{substitute_env_var, ExpanderContext};
use crate::token_expander_error::TokenExpanderError;
use moon_args::join_args;
use moon_common::path::{self, WorkspaceRelativePathBuf};
use moon_config::{patterns, InputPath, OutputPath};
use moon_project::FileGroup;
Expand All @@ -17,6 +18,7 @@ pub struct ExpandedResult {
pub env: Vec<String>,
pub files: Vec<WorkspaceRelativePathBuf>,
pub globs: Vec<WorkspaceRelativePathBuf>,
pub token: Option<String>,
}

#[derive(PartialEq)]
Expand Down Expand Up @@ -66,6 +68,10 @@ impl<'graph, 'query> TokenExpander<'graph, 'query> {
if patterns::TOKEN_FUNC_DISTINCT.is_match(value) {
return true;
} else if patterns::TOKEN_FUNC.is_match(value) {
if self.scope == TokenScope::Script {
return true;
}

warn!(
"Found a token function in `{}` with other content. Token functions *must* be used literally as the only value.",
value
Expand Down Expand Up @@ -96,14 +102,21 @@ impl<'graph, 'query> TokenExpander<'graph, 'query> {
pub fn expand_script(&mut self, task: &Task) -> miette::Result<String> {
self.scope = TokenScope::Script;

let script = task.script.as_ref().expect("Script not defined!");
let mut value = Cow::Borrowed(task.script.as_ref().expect("Script not defined!"));

if self.has_token_function(script) {
// Trigger the scope error
self.replace_function(task, script)?;
while self.has_token_function(&value) {
let result = self.replace_function(task, &value)?;

if let Some(token) = result.token {
let mut paths = vec![];
paths.extend(result.files);
paths.extend(result.globs);

value = Cow::Owned(value.replace(&token, &join_args(&paths)));
}
}

self.replace_variables(task, script)
self.replace_variables(task, &value)
}

#[instrument(skip_all)]
Expand Down Expand Up @@ -323,13 +336,16 @@ impl<'graph, 'query> TokenExpander<'graph, 'query> {
let token = matches.get(0).unwrap().as_str(); // @name(arg)
let func = matches.get(1).unwrap().as_str(); // name
let arg = matches.get(2).unwrap().as_str(); // arg

let mut result = ExpandedResult::default();
result.token = Some(token.to_owned());

let loose_check = matches!(self.scope, TokenScope::Outputs);
let file_group = || -> miette::Result<&FileGroup> {
self.check_scope(
token,
&[
TokenScope::Script,
TokenScope::Args,
TokenScope::Env,
TokenScope::Inputs,
Expand Down Expand Up @@ -384,7 +400,7 @@ impl<'graph, 'query> TokenExpander<'graph, 'query> {
}
// Inputs, outputs
"in" => {
self.check_scope(token, &[TokenScope::Args])?;
self.check_scope(token, &[TokenScope::Script, TokenScope::Args])?;

let index = self.parse_index(token, arg)?;
let input =
Expand Down Expand Up @@ -415,7 +431,7 @@ impl<'graph, 'query> TokenExpander<'graph, 'query> {
};
}
"out" => {
self.check_scope(token, &[TokenScope::Args])?;
self.check_scope(token, &[TokenScope::Script, TokenScope::Args])?;

let index = self.parse_index(token, arg)?;
let output =
Expand Down Expand Up @@ -484,17 +500,6 @@ impl<'graph, 'query> TokenExpander<'graph, 'query> {
let variable = matches.get(1).unwrap().as_str(); // var
let project = self.context.project;

self.check_scope(
token,
&[
TokenScope::Command,
TokenScope::Args,
TokenScope::Env,
TokenScope::Inputs,
TokenScope::Outputs,
],
)?;

let replaced_value = match variable {
"workspaceRoot" => Cow::Owned(path::to_string(self.context.workspace_root)?),
// Project
Expand Down
91 changes: 91 additions & 0 deletions crates/project-expander/tests/token_expander_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,4 +1030,95 @@ mod token_expander {
);
}
}

mod script {
use super::*;

#[test]
fn passes_through() {
let sandbox = create_empty_sandbox();
let project = create_project(sandbox.path());
let mut task = create_task();

task.script = Some("bin --foo -az bar".into());

let context = create_context(&project, sandbox.path());
let mut expander = TokenExpander::new(&context);

assert_eq!(expander.expand_script(&task).unwrap(), "bin --foo -az bar");
}

#[test]
fn replaces_one_var() {
let sandbox = create_empty_sandbox();
let project = create_project(sandbox.path());
let mut task = create_task();

task.script = Some("$project/bin --foo -az bar".into());

let context = create_context(&project, sandbox.path());
let mut expander = TokenExpander::new(&context);

assert_eq!(
expander.expand_script(&task).unwrap(),
"project/bin --foo -az bar"
);
}

#[test]
fn replaces_two_vars() {
let sandbox = create_empty_sandbox();
let project = create_project(sandbox.path());
let mut task = create_task();

task.script = Some("$project/bin/$task --foo -az bar".into());

let context = create_context(&project, sandbox.path());
let mut expander = TokenExpander::new(&context);

assert_eq!(
expander.expand_script(&task).unwrap(),
"project/bin/task --foo -az bar"
);
}

#[test]
fn supports_outputs() {
let sandbox = create_sandbox("file-group");
let project = create_project(sandbox.path());
let mut task = create_task();

task.script = Some("bin --foo -az @out(0)".into());
task.outputs = vec![OutputPath::ProjectGlob("**/*.json".into())];

let context = create_context(&project, sandbox.path());
let mut expander = TokenExpander::new(&context);

assert_eq!(
expander.expand_script(&task).unwrap(),
"bin --foo -az project/source/**/*.json"
);
}

#[test]
fn supports_inputs() {
let sandbox = create_sandbox("file-group");
let project = create_project(sandbox.path());
let mut task = create_task();

task.script = Some("bin --foo -az @in(0) @in(1)".into());
task.inputs = vec![
InputPath::ProjectFile("docs.md".into()),
InputPath::ProjectFile("other/file.json".into()),
];

let context = create_context(&project, sandbox.path());
let mut expander = TokenExpander::new(&context);

assert_eq!(
expander.expand_script(&task).unwrap(),
"bin --foo -az project/source/docs.md project/source/other/file.json"
);
}
}
}
2 changes: 1 addition & 1 deletion packages/types/src/project-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

/* eslint-disable */

import type { UnresolvedVersionSpec } from './toolchain-config';
import type { PartialTaskConfig, PlatformType, TaskConfig } from './tasks-config';
import type { UnresolvedVersionSpec } from './toolchain-config';

/** The scope and or relationship of the dependency. */
export type DependencyScope = 'build' | 'development' | 'peer' | 'production' | 'root';
Expand Down
12 changes: 12 additions & 0 deletions packages/types/src/tasks-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ export interface TaskConfig {
* @default 'unknown'
*/
platform: PlatformType;
/**
* A script to run within a shell. A script is anything from a single command,
* to multiple commands (&&, etc), or shell specific syntax. Does not support
* arguments, merging, or inheritance.
*/
script: string | null;
/**
* The type of task, primarily used for categorical reasons. When not provided,
* will be automatically determined.
Expand Down Expand Up @@ -415,6 +421,12 @@ export interface PartialTaskConfig {
* @default 'unknown'
*/
platform?: PlatformType | null;
/**
* A script to run within a shell. A script is anything from a single command,
* to multiple commands (&&, etc), or shell specific syntax. Does not support
* arguments, merging, or inheritance.
*/
script?: string | null;
/**
* The type of task, primarily used for categorical reasons. When not provided,
* will be automatically determined.
Expand Down
23 changes: 23 additions & 0 deletions website/docs/concepts/task.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,29 @@ tasks:
Tasks can be configured per project through [`moon.yml`](../config/project), or for many projects
through [`.moon/tasks.yml`](../config/tasks).

### Commands vs Scripts

A task is either a command or script, but not both. So what's the difference exactly? In the context
of a moon task, a command is a single binary execution with optional arguments, configured with the
[`command`](../config/project#command) and [`args`](../config/project#args) settings (which both
support a string or array). While a script is one or many binary executions, with support for pipes
and redirects, and configured with the [`script`](../config/project#script) setting (which is only a
string).

A command also supports merging during task inheritance, while a script does not and will always
replace values. Refer to the table below for more differences between the 2.

| | Command | Script |
| :--------------------------------------- | :------------------------ | :----------------- |
| Inheritance merging | ✅ via `mergeArgs` option | ⚠️ always replaces |
| Additional args | ✅ via `args` setting | ❌ |
| Passthrough args (from CLI) | ✅ | ❌ |
| Multiple commands (with `&&` or `;`) | ❌ | ✅ |
| Pipes, redirects, etc | ❌ | ✅ |
| Always ran in a shell | ❌ | ✅ |
| Custom platform/toolchain | ✅ | ❌ always `system` |
| [Token](./token) functions and variables | ✅ | ✅ |

### Inheritance

View the official documentation on [task inheritance](./task-inheritance).
59 changes: 44 additions & 15 deletions website/docs/config/project.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -451,13 +451,16 @@ tasks:
command: 'vite build'
```

### `command`<RequiredLabel />
### `command`

<HeadingApiLink to="/api/types/interface/TaskConfig#command" />

The `command` field is the command line to run for the task, including the command name (must be
first) and any optional [arguments](#args). This field is required when _not_ inheriting a global
task of the same name.
The `command` field is a _single_ command to execute for the task, including the command binary/name
(must be first) and any optional [arguments](#args). This field supports task inheritance and
merging of arguments.

This setting can be defined using a string, or an array of strings. We suggest using arrays when
dealing with many args, or the args string cannot be parsed easily.

```yaml title="moon.yml" {4,6-9}
tasks:
Expand All @@ -471,16 +474,12 @@ tasks:
- '.'
```

By default a task assumes the command name is an npm binary, and if you'd like to reference a system
command, you'll also need to set the [`platform`](#platform) to "system". We do our best to
automatically detect this, but it's not accurate in all scenarios.
:::info

```yaml title="moon.yml"
tasks:
clean:
command: 'rm -rf ./dist'
platform: 'system'
```
If you need to support pipes, redirects, or multiple commands, use [`script`](#script) instead.
Learn more about [commands vs scripts](../concepts/task#commands-vs-scripts).

:::

#### Special commands

Expand All @@ -501,8 +500,8 @@ For interoperability reasons, the following command names have special handling.

<HeadingApiLink to="/api/types/interface/TaskConfig#args" />

The `args` field is a collection of _additional_ arguments to pass to the command line when
executing the task. This field exists purely to provide arguments for
The `args` field is a collection of _additional_ arguments to append to the [`command`](#command)
when executing the task. This field exists purely to provide arguments for
[inherited tasks](./tasks#tasks).

This setting can be defined using a string, or an array of strings. We suggest using arrays when
Expand Down Expand Up @@ -757,6 +756,36 @@ tasks:
> This field exists because of our [toolchain](../concepts/toolchain), and moon ensuring the correct
> command is ran.

### `script`<VersionLabel version="1.27.0" />

<HeadingApiLink to="/api/types/interface/TaskConfig#script" />

The `script` field is _one or many_ commands to execute for the task, with support for pipes,
redirects, and more. This field does _not_ support task inheritance merging, and can only be defined
with a string.

If defined, will supersede [`command`](#command) and [`args`](#args).

```yaml title="moon.yml" {4,6,8,10}
tasks:
exec:
# Single command
script: 'cp ./in ./out'
# Multiple commands
script: 'rm -rf ./out && cp ./in ./out'
# Pipes
script: 'ps aux | grep 3000'
# Redirects
script: './gen.sh > out.json'
```

:::info

If you need to support merging during task inheritance, use [`command`](#command) instead. Learn
more about [commands vs scripts](../concepts/task#commands-vs-scripts).

:::

### `options`

<HeadingApiLink to="/api/types/interface/TaskConfig#options" />
Expand Down
Loading

0 comments on commit da28bea

Please sign in to comment.