Repository: casey/just
Pull request #2237 (2024-07-08 03:42:08)

Allow enabling unstable features with `set unstable`


Patch:
diff --git a/README.md b/README.md
index a992d62f..1f5d6ad0 100644
--- a/README.md
+++ b/README.md
@@ -379,11 +379,11 @@ There will never be a `just` 2.0. Any desirable backwards-incompatible changes
 will be opt-in on a per-`justfile` basis, so users may migrate at their
 leisure.
 
-Features that aren't yet ready for stabilization are gated behind the
-`--unstable` flag. Features enabled by `--unstable` may change in backwards
-incompatible ways at any time. Unstable features can also be enabled by setting
-the environment variable `JUST_UNSTABLE` to any value other than `false`, `0`,
-or the empty string.
+Features that aren't yet ready for stabilization are marked as unstable and may
+be changed or removed at any time. Using unstable features produces an error by
+default, which can be suppressed with by passing the `--unstable` flag,
+`set unstable`, or setting the environment variable `JUST_UNSTABLE`, to any
+value other than `false`, `0`, or the empty string.
 
 Editor Support
 --------------
@@ -820,6 +820,7 @@ foo:
 | `positional-arguments` | boolean | `false` | Pass positional arguments. |
 | `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
 | `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. |
+| `unstable`<sup>master</sup> | boolean | `false` | Enable unstable features. |
 | `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. |
 | `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
 
@@ -3154,8 +3155,8 @@ Missing source files for optional imports do not produce an error.
 ### Modules<sup>1.19.0</sup>
 
 A `justfile` can declare modules using `mod` statements. `mod` statements are
-currently unstable, so you'll need to use the `--unstable` flag, or set the
-`JUST_UNSTABLE` environment variable to use them.
+currently unstable, so you'll need to use the `--unstable` flag,
+`set unstable`, or set the `JUST_UNSTABLE` environment variable to use them.
 
 If you have the following `justfile`:
 
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 928d9fda..b0f447ec 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -37,6 +37,8 @@ impl<'src> Analyzer<'src> {
 
     let mut warnings = Vec::new();
 
+    let mut unstable = BTreeSet::new();
+
     let mut modules: Table<Justfile> = Table::new();
 
     let mut unexports: HashSet<String> = HashSet::new();
@@ -92,6 +94,8 @@ impl<'src> Analyzer<'src> {
             doc,
             ..
           } => {
+            unstable.insert(Unstable::Modules);
+
             if let Some(absolute) = absolute {
               define(*name, "module", false)?;
               modules.insert(Self::analyze(
@@ -194,6 +198,7 @@ impl<'src> Analyzer<'src> {
       settings,
       source: root.into(),
       unexports,
+      unstable,
       warnings,
     })
   }
diff --git a/src/argument_parser.rs b/src/argument_parser.rs
index 85daccce..091ffd89 100644
--- a/src/argument_parser.rs
+++ b/src/argument_parser.rs
@@ -252,7 +252,7 @@ mod tests {
     fs::write(&path, "mod foo").unwrap();
     fs::create_dir(tempdir.path().join("foo")).unwrap();
     fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
-    let compilation = Compiler::compile(true, &loader, &path).unwrap();
+    let compilation = Compiler::compile(&loader, &path).unwrap();
 
     assert_eq!(
       ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "bar"]).unwrap(),
@@ -271,7 +271,7 @@ mod tests {
     fs::write(&path, "mod foo").unwrap();
     fs::create_dir(tempdir.path().join("foo")).unwrap();
     fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
-    let compilation = Compiler::compile(true, &loader, &path).unwrap();
+    let compilation = Compiler::compile(&loader, &path).unwrap();
 
     assert_matches!(
       ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "zzz"]).unwrap_err(),
@@ -289,7 +289,7 @@ mod tests {
     tempdir.write("foo.just", "bar:");
 
     let loader = Loader::new();
-    let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+    let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
 
     assert_matches!(
       ArgumentParser::parse_arguments(&compilation.justfile, &["foo::zzz"]).unwrap_err(),
@@ -307,7 +307,7 @@ mod tests {
     tempdir.write("foo.just", "bar:");
 
     let loader = Loader::new();
-    let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+    let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
 
     assert_matches!(
       ArgumentParser::parse_arguments(&compilation.justfile, &["foo::bar::baz"]).unwrap_err(),
@@ -323,7 +323,7 @@ mod tests {
     tempdir.write("justfile", "");
 
     let loader = Loader::new();
-    let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+    let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
 
     assert_matches!(
       ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
@@ -337,7 +337,7 @@ mod tests {
     tempdir.write("justfile", "foo bar:");
 
     let loader = Loader::new();
-    let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+    let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
 
     assert_matches!(
       ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
@@ -355,7 +355,7 @@ mod tests {
     tempdir.write("foo.just", "bar:");
 
     let loader = Loader::new();
-    let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
+    let compilation = Compiler::compile(&loader, &tempdir.path().join("justfile")).unwrap();
 
     assert_matches!(
       ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
diff --git a/src/compiler.rs b/src/compiler.rs
index 31ad3627..a977be58 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -4,14 +4,13 @@ pub(crate) struct Compiler;
 
 impl Compiler {
   pub(crate) fn compile<'src>(
-    unstable: bool,
     loader: &'src Loader,
     root: &Path,
   ) -> RunResult<'src, Compilation<'src>> {
     let mut asts = HashMap::<PathBuf, Ast>::new();
+    let mut loaded = Vec::new();
     let mut paths = HashMap::<PathBuf, PathBuf>::new();
     let mut srcs = HashMap::<PathBuf, &str>::new();
-    let mut loaded = Vec::new();
 
     let mut stack = Vec::new();
     stack.push(Source::root(root));
@@ -42,12 +41,6 @@ impl Compiler {
             relative,
             ..
           } => {
-            if !unstable {
-              return Err(Error::Unstable {
-                message: "Modules are currently unstable.".into(),
-              });
-            }
-
             let parent = current.path.parent().unwrap();
 
             let import = if let Some(relative) = relative {
@@ -112,9 +105,9 @@ impl Compiler {
 
     Ok(Compilation {
       asts,
-      srcs,
       justfile,
       root: root.into(),
+      srcs,
     })
   }
 
@@ -225,7 +218,7 @@ recipe_b: recipe_c
     let loader = Loader::new();
 
     let justfile_a_path = tmp.path().join("justfile");
-    let compilation = Compiler::compile(false, &loader, &justfile_a_path).unwrap();
+    let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap();
 
     assert_eq!(compilation.root_src(), justfile_a);
   }
@@ -242,7 +235,7 @@ recipe_b: recipe_c
     let loader = Loader::new();
 
     let justfile_a_path = tmp.path().join("justfile");
-    let loader_output = Compiler::compile(false, &loader, &justfile_a_path).unwrap_err();
+    let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err();
 
     assert_matches!(loader_output, Error::CircularImport { current, import }
       if current == tmp.path().join("subdir").join("b").lexiclean() &&
diff --git a/src/error.rs b/src/error.rs
index ca2acb15..d2e8704f 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -460,7 +460,7 @@ impl<'src> ColorDisplay for Error<'src> {
         }
       }
       Unstable { message } => {
-        write!(f, "{message} Invoke `just` with the `--unstable` flag to enable unstable features.")?;
+        write!(f, "{message} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
       }
       WriteJustfile { justfile, io_error } => {
         let justfile = justfile.display();
diff --git a/src/justfile.rs b/src/justfile.rs
index 19503923..c5be95df 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -27,6 +27,8 @@ pub(crate) struct Justfile<'src> {
   pub(crate) source: PathBuf,
   pub(crate) unexports: HashSet<String>,
   pub(crate) warnings: Vec<Warning>,
+  #[serde(skip)]
+  pub(crate) unstable: BTreeSet<Unstable>,
 }
 
 impl<'src> Justfile<'src> {
@@ -225,6 +227,22 @@ impl<'src> Justfile<'src> {
     Ok(())
   }
 
+  pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {
+    if !config.unstable && !self.settings.unstable {
+      if let Some(unstable) = self.unstable.iter().next() {
+        return Err(Error::Unstable {
+          message: unstable.message(),
+        });
+      }
+    }
+
+    for module in self.modules.values() {
+      module.check_unstable(config)?;
+    }
+
+    Ok(())
+  }
+
   pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
     self.aliases.get(name)
   }
diff --git a/src/keyword.rs b/src/keyword.rs
index d57032ae..7f8b5b3f 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -26,6 +26,7 @@ pub(crate) enum Keyword {
   Tempdir,
   True,
   Unexport,
+  Unstable,
   WindowsPowershell,
   WindowsShell,
   X,
diff --git a/src/lib.rs b/src/lib.rs
index 50e54e90..127881e2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,8 +42,8 @@ pub(crate) use {
     shell::Shell, show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind,
     string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
     thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
-    unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
-    verbosity::Verbosity, warning::Warning,
+    unresolved_recipe::UnresolvedRecipe, unstable::Unstable, use_color::UseColor,
+    variables::Variables, verbosity::Verbosity, warning::Warning,
   },
   camino::Utf8Path,
   clap::ValueEnum,
@@ -204,6 +204,7 @@ mod token_kind;
 mod unindent;
 mod unresolved_dependency;
 mod unresolved_recipe;
+mod unstable;
 mod use_color;
 mod variables;
 mod verbosity;
diff --git a/src/node.rs b/src/node.rs
index 2fd73e30..1de9bdcb 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -294,6 +294,7 @@ impl<'src> Node<'src> for Set<'src> {
       | Setting::Fallback(value)
       | Setting::PositionalArguments(value)
       | Setting::Quiet(value)
+      | Setting::Unstable(value)
       | Setting::WindowsPowerShell(value)
       | Setting::IgnoreComments(value) => {
         set.push_mut(value.to_string());
diff --git a/src/parser.rs b/src/parser.rs
index 90389a29..6a18d5f9 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -936,6 +936,7 @@ impl<'run, 'src> Parser<'run, 'src> {
       Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
       Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
       Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
+      Keyword::Unstable => Some(Setting::Unstable(self.parse_set_bool()?)),
       Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
       _ => None,
     };
diff --git a/src/setting.rs b/src/setting.rs
index e6cb5155..f19dab47 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -15,6 +15,7 @@ pub(crate) enum Setting<'src> {
   Quiet(bool),
   Shell(Shell<'src>),
   Tempdir(String),
+  Unstable(bool),
   WindowsPowerShell(bool),
   WindowsShell(Shell<'src>),
 }
@@ -31,6 +32,7 @@ impl<'src> Display for Setting<'src> {
       | Self::IgnoreComments(value)
       | Self::PositionalArguments(value)
       | Self::Quiet(value)
+      | Self::Unstable(value)
       | Self::WindowsPowerShell(value) => write!(f, "{value}"),
       Self::Shell(shell) | Self::WindowsShell(shell) => write!(f, "{shell}"),
       Self::DotenvFilename(value) | Self::DotenvPath(value) | Self::Tempdir(value) => {
diff --git a/src/settings.rs b/src/settings.rs
index ae6c4b1c..338f2cc7 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -20,6 +20,7 @@ pub(crate) struct Settings<'src> {
   pub(crate) quiet: bool,
   pub(crate) shell: Option<Shell<'src>>,
   pub(crate) tempdir: Option<String>,
+  pub(crate) unstable: bool,
   pub(crate) windows_powershell: bool,
   pub(crate) windows_shell: Option<Shell<'src>>,
 }
@@ -66,6 +67,9 @@ impl<'src> Settings<'src> {
         Setting::Shell(shell) => {
           settings.shell = Some(shell);
         }
+        Setting::Unstable(unstable) => {
+          settings.unstable = unstable;
+        }
         Setting::WindowsPowerShell(windows_powershell) => {
           settings.windows_powershell = windows_powershell;
         }
diff --git a/src/subcommand.rs b/src/subcommand.rs
index 65c80a83..6c405b74 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -190,7 +190,9 @@ impl Subcommand {
     loader: &'src Loader,
     search: &Search,
   ) -> RunResult<'src, Compilation<'src>> {
-    let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
+    let compilation = Compiler::compile(loader, &search.justfile)?;
+
+    compilation.justfile.check_unstable(config)?;
 
     if config.verbosity.loud() {
       for warning in &compilation.justfile.warnings {
diff --git a/src/summary.rs b/src/summary.rs
index 65a28ba1..ee3a8d11 100644
--- a/src/summary.rs
+++ b/src/summary.rs
@@ -28,7 +28,7 @@ mod full {
 pub fn summary(path: &Path) -> io::Result<Result<Summary, String>> {
   let loader = Loader::new();
 
-  match Compiler::compile(false, &loader, path) {
+  match Compiler::compile(&loader, path) {
     Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),
     Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {
       compile_error.to_string()
diff --git a/src/unstable.rs b/src/unstable.rs
new file mode 100644
index 00000000..6647fb4a
--- /dev/null
+++ b/src/unstable.rs
@@ -0,0 +1,12 @@
+#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
+pub(crate) enum Unstable {
+  Modules,
+}
+
+impl Unstable {
+  pub(crate) fn message(self) -> String {
+    match self {
+      Self::Modules => "Modules are currently unstable.".into(),
+    }
+  }
+}


Test patch:
diff --git a/tests/fmt.rs b/tests/fmt.rs
index ba050fc3..29a12ba4 100644
--- a/tests/fmt.rs
+++ b/tests/fmt.rs
@@ -4,10 +4,7 @@ test! {
   name: unstable_not_passed,
   justfile: "",
   args: ("--fmt"),
-  stderr: "
-    error: The `--fmt` command is currently unstable. \
-    Invoke `just` with the `--unstable` flag to enable unstable features.
-  ",
+  stderr_regex: "error: The `--fmt` command is currently unstable..*",
   status: EXIT_FAILURE,
 }
 
diff --git a/tests/json.rs b/tests/json.rs
index 9b827ca4..a88bdefd 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -56,7 +56,9 @@ fn alias() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "ignore_comments": false,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -98,6 +100,7 @@ fn assignment() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -153,6 +156,7 @@ fn body() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -220,6 +224,7 @@ fn dependencies() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -325,6 +330,7 @@ fn dependency_argument() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -392,6 +398,7 @@ fn duplicate_recipes() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -437,6 +444,7 @@ fn duplicate_variables() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -485,6 +493,7 @@ fn doc_comment() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -519,6 +528,7 @@ fn empty_justfile() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -674,6 +684,7 @@ fn parameters() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -762,6 +773,7 @@ fn priors() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -810,6 +822,7 @@ fn private() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -858,6 +871,7 @@ fn quiet() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -921,6 +935,7 @@ fn settings() {
           "command": "a",
         },
         "tempdir": null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -972,6 +987,7 @@ fn shebang() {
         "quiet": false,
         "shell": null,
         "tempdir": null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -1020,6 +1036,7 @@ fn simple() {
         "quiet": false,
         "shell": null,
         "tempdir": null,
+        "unstable": false,
         "windows_powershell": false,
         "windows_shell": null,
       },
@@ -1070,6 +1087,7 @@ fn attribute() {
         "quiet": false,
         "shell": null,
         "tempdir" : null,
+        "unstable": false,
         "ignore_comments": false,
         "windows_powershell": false,
         "windows_shell": null,
@@ -1136,6 +1154,7 @@ fn module() {
               "quiet": false,
               "shell": null,
               "tempdir" : null,
+              "unstable": false,
               "ignore_comments": false,
               "windows_powershell": false,
               "windows_shell": null,
@@ -1158,6 +1177,7 @@ fn module() {
           "quiet": false,
           "shell": null,
           "tempdir" : null,
+          "unstable": false,
           "ignore_comments": false,
           "windows_powershell": false,
           "windows_shell": null,
diff --git a/tests/modules.rs b/tests/modules.rs
index 36928585..17839420 100644
--- a/tests/modules.rs
+++ b/tests/modules.rs
@@ -8,10 +8,8 @@ fn modules_are_unstable() {
         mod foo
       ",
     )
-    .stderr(
-      "error: Modules are currently unstable. \
-      Invoke `just` with the `--unstable` flag to enable unstable features.\n",
-    )
+    .write("foo.just", "")
+    .stderr_regex("error: Modules are currently unstable..*")
     .status(EXIT_FAILURE)
     .run();
 }
diff --git a/tests/test.rs b/tests/test.rs
index 29350351..288f6bd3 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -150,7 +150,7 @@ impl Test {
   }
 
   pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef<str>) -> Self {
-    self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap());
+    self.stderr_regex = Some(Regex::new(&format!("^(?s){}$", stderr_regex.as_ref())).unwrap());
     self
   }
 
diff --git a/tests/unstable.rs b/tests/unstable.rs
index 23a5fa6e..ad196cf7 100644
--- a/tests/unstable.rs
+++ b/tests/unstable.rs
@@ -26,12 +26,12 @@ default:
   "#;
   for val in ["0", "", "false"] {
     Test::new()
-    .justfile(justfile)
-    .args(["--fmt"])
-    .env("JUST_UNSTABLE", val)
-    .status(EXIT_FAILURE)
-    .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
-    .run();
+      .justfile(justfile)
+      .args(["--fmt"])
+      .env("JUST_UNSTABLE", val)
+      .status(EXIT_FAILURE)
+      .stderr_regex("error: The `--fmt` command is currently unstable.*")
+      .run();
   }
 }
 
@@ -45,6 +45,40 @@ default:
     .justfile(justfile)
     .args(["--fmt"])
     .status(EXIT_FAILURE)
-    .stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
+    .stderr_regex("error: The `--fmt` command is currently unstable.*")
+    .run();
+}
+
+#[test]
+fn set_unstable_with_setting() {
+  Test::new()
+    .justfile(
+      "
+        set unstable
+
+        mod foo
+      ",
+    )
+    .write("foo.just", "@bar:\n echo BAR")
+    .args(["foo", "bar"])
+    .stdout("BAR\n")
+    .run();
+}
+
+#[test]
+fn unstable_setting_does_not_affect_submodules() {
+  Test::new()
+    .justfile(
+      "
+        set unstable
+
+        mod foo
+      ",
+    )
+    .write("foo.just", "mod bar")
+    .write("bar.just", "baz:\n echo hello")
+    .args(["foo", "bar"])
+    .stderr_regex("error: Modules are currently unstable.*")
+    .status(EXIT_FAILURE)
     .run();
 }