diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a3cd93..90ae70b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,15 @@ ## Unreleased +> **Note** +> This version contains a number of breaking changes in how script names are resolved in order to bring the behavior more in line with both NPM and `npm-run-all`. + - Adjusted globbing so `:` acts like a path separator ([#131](https://github.com/xt0rted/dotnet-run-script/pull/131)) - `foo:*` will match `foo:bar` but not `foo:bar:baz` - `foo:*:baz` will match `foo:bar:baz` - `foo:**` will match `foo:bar` and `foo:bar:baz` +- Script names are no longer case insensitive which matches NPM's behavior ([#130](https://github.com/xt0rted/dotnet-run-script/pull/130)) +- Scripts are now stored in a `KeyedCollection` instead of a `Dictionary` which should guarantee they're executed in the order they're loaded from the `global.json` ([#130](https://github.com/xt0rted/dotnet-run-script/pull/130)) ## [0.5.0](https://github.com/xt0rted/dotnet-run-script/compare/v0.4.0...v0.5.0) - 2022-10-11 diff --git a/Directory.Build.props b/Directory.Build.props index 9c9e53c..723499a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - netcoreapp3.1;net6.0 + net6.0;netcoreapp3.1 latest enable enable diff --git a/src/CommandGroupRunner.cs b/src/CommandGroupRunner.cs index 669b280..a510088 100644 --- a/src/CommandGroupRunner.cs +++ b/src/CommandGroupRunner.cs @@ -6,7 +6,7 @@ internal class CommandGroupRunner : ICommandGroupRunner { private readonly IConsoleWriter _writer; private readonly IEnvironment _environment; - private readonly IDictionary _scripts; + private readonly ScriptCollection _scripts; private readonly ProcessContext _processContext; private readonly bool _captureOutput; private readonly CancellationToken _cancellationToken; @@ -14,7 +14,7 @@ internal class CommandGroupRunner : ICommandGroupRunner public CommandGroupRunner( IConsoleWriter writer, IEnvironment environment, - IDictionary scripts, + ScriptCollection scripts, ProcessContext processContext, bool captureOutput, CancellationToken cancellationToken) @@ -38,10 +38,10 @@ public async Task RunAsync(string name, string[]? scriptArgs) { var scriptNames = ImmutableArray.Create(new[] { "pre" + name, name, "post" + name }); - foreach (var subScript in scriptNames.Where(scriptName => _scripts.ContainsKey(scriptName) || scriptName == "env")) + foreach (var subScript in scriptNames.Where(scriptName => _scripts.Contains(scriptName) || scriptName == "env")) { // At this point we should have done enough checks to make sure the only not found script is `env` - if (!_scripts.ContainsKey(subScript)) + if (!_scripts.Contains(subScript)) { GlobalCommands.PrintEnvironmentVariables(_writer, _environment); @@ -56,7 +56,7 @@ public async Task RunAsync(string name, string[]? scriptArgs) var result = await command.RunAsync( subScript, - _scripts[subScript]!, + _scripts[subScript].Script, args); if (result != 0) diff --git a/src/GlobalCommands.cs b/src/GlobalCommands.cs index f552c68..8a300de 100644 --- a/src/GlobalCommands.cs +++ b/src/GlobalCommands.cs @@ -7,15 +7,15 @@ internal static class GlobalCommands /// /// The console logger instance to use. /// The project's scripts. - public static void PrintAvailableScripts(IConsoleWriter writer, IDictionary scripts) + public static void PrintAvailableScripts(IConsoleWriter writer, ScriptCollection scripts) { writer.Line("Available via `{0}`:", writer.ColorText(ConsoleColor.Blue, "dotnet r")); writer.BlankLine(); - foreach (var script in scripts.Keys) + foreach (var (name, script) in scripts) { - writer.Line(" {0}", script); - writer.SecondaryLine(" {0}", scripts[script]); + writer.Line(" {0}", name); + writer.SecondaryLine(" {0}", script); writer.BlankLine(); } } diff --git a/src/Project.cs b/src/Project.cs index 041e2cf..e38831a 100644 --- a/src/Project.cs +++ b/src/Project.cs @@ -2,12 +2,11 @@ namespace RunScript; using System.Text.Json.Serialization; -using RunScript.Serialization; - public class Project { + [JsonPropertyName("scriptShell")] public string? ScriptShell { get; set; } - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - public Dictionary? Scripts { get; set; } + [JsonPropertyName("scripts")] + public ScriptCollection? Scripts { get; set; } } diff --git a/src/ProjectLoader.cs b/src/ProjectLoader.cs index 0b421e0..afd4fdc 100644 --- a/src/ProjectLoader.cs +++ b/src/ProjectLoader.cs @@ -57,10 +57,9 @@ internal class ProjectLoader return JsonSerializer.Deserialize( json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - }); + { + ReadCommentHandling = JsonCommentHandling.Skip, + }); } catch { diff --git a/src/RunScriptCommand.cs b/src/RunScriptCommand.cs index 71e1a4f..6746c4d 100644 --- a/src/RunScriptCommand.cs +++ b/src/RunScriptCommand.cs @@ -134,7 +134,7 @@ public async Task InvokeAsync(InvocationContext context) } internal static List FindScripts( - IDictionary projectScripts, + ScriptCollection projectScripts, string[] scripts) { var results = new List(); @@ -142,7 +142,7 @@ internal static List FindScripts( foreach (var script in scripts) { // The `env` script is special so if it's not explicitly declared we act like it was - if (projectScripts.ContainsKey(script) || string.Equals(script, "env", StringComparison.OrdinalIgnoreCase)) + if (projectScripts.Contains(script) || script == "env") { results.Add(new(script, true)); @@ -160,7 +160,7 @@ internal static List FindScripts( } }); - foreach (var projectScript in projectScripts.Keys) + foreach (var (projectScript, _) in projectScripts) { if (matcher.IsMatch(SwapColonAndSlash(projectScript).AsSpan())) { diff --git a/src/ScriptCollection.cs b/src/ScriptCollection.cs new file mode 100644 index 0000000..48ac3ae --- /dev/null +++ b/src/ScriptCollection.cs @@ -0,0 +1,25 @@ +namespace RunScript; + +using System.Collections.ObjectModel; +using System.Text.Json.Serialization; + +using RunScript.Serialization; + +[JsonConverter(typeof(ScriptCollectionConverter))] +public class ScriptCollection : KeyedCollection +{ + public ScriptCollection() + : base(StringComparer.Ordinal) + { + } + + protected override string GetKeyForItem(ScriptMapping item) + { + if (item is null) throw new ArgumentNullException(nameof(item)); + + return item.Name; + } + + public void Add(string name, string script) + => Add(new ScriptMapping(name, script)); +} diff --git a/src/ScriptMapping.cs b/src/ScriptMapping.cs new file mode 100644 index 0000000..e76c7a4 --- /dev/null +++ b/src/ScriptMapping.cs @@ -0,0 +1,3 @@ +namespace RunScript; + +public record ScriptMapping(string Name, string Script); diff --git a/src/Serialization/CaseInsensitiveDictionaryConverter.cs b/src/Serialization/CaseInsensitiveDictionaryConverter.cs deleted file mode 100644 index 26f60af..0000000 --- a/src/Serialization/CaseInsensitiveDictionaryConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace RunScript.Serialization; - -using System.Text.Json; -using System.Text.Json.Serialization; - -internal class CaseInsensitiveDictionaryConverter : JsonConverter> -{ - public override Dictionary Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - { - var dict = (Dictionary)JsonSerializer - .Deserialize(ref reader, typeToConvert, options)!; - -#pragma warning disable CA1308 // Normalize strings to uppercase - return dict.ToDictionary( - i => i.Key.ToLowerInvariant(), - i => i.Value, - StringComparer.OrdinalIgnoreCase); -#pragma warning restore CA1308 // Normalize strings to uppercase - } - - public override void Write( - Utf8JsonWriter writer, - Dictionary value, - JsonSerializerOptions options) - => JsonSerializer.Serialize( - writer, - value, - value.GetType(), - options); -} diff --git a/src/Serialization/ScriptCollectionConverter.cs b/src/Serialization/ScriptCollectionConverter.cs new file mode 100644 index 0000000..a87626a --- /dev/null +++ b/src/Serialization/ScriptCollectionConverter.cs @@ -0,0 +1,42 @@ +namespace RunScript.Serialization; + +using System.Text.Json; +using System.Text.Json.Serialization; + +internal class ScriptCollectionConverter : JsonConverter +{ + public override ScriptCollection Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + var scripts = new ScriptCollection(); + + using (var jsonDoc = JsonDocument.ParseValue(ref reader)) + using (var jsonScripts = jsonDoc.RootElement.EnumerateObject()) + { + foreach (var script in jsonScripts) + { + scripts.Add(script.Name, script.Value.ToString()); + } + } + + return scripts; + } + + public override void Write( + Utf8JsonWriter writer, + ScriptCollection value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var element in value) + { + writer.WritePropertyName(element.Name); + writer.WriteStringValue(element.Script); + } + + writer.WriteEndObject(); + } +} diff --git a/test/CommandBuilderTests.cs b/test/CommandBuilderTests.cs index 11a2857..1ad39e0 100644 --- a/test/CommandBuilderTests.cs +++ b/test/CommandBuilderTests.cs @@ -101,7 +101,7 @@ private static CommandBuilder SetUpTest(bool isWindows, string? comSpec = Defaul var project = new Project { - Scripts = new Dictionary(), + Scripts = new ScriptCollection(), }; var environment = new TestEnvironment( diff --git a/test/CommandGroupRunnerTests.cs b/test/CommandGroupRunnerTests.cs index b803f64..6bcace5 100644 --- a/test/CommandGroupRunnerTests.cs +++ b/test/CommandGroupRunnerTests.cs @@ -240,7 +240,7 @@ public async Task Should_return_first_error_hit(bool isWindows) private static (TestConsole console, CommandGroupRunner groupRunner) SetUpTest( TestCommandRunner[] commandRunners, bool isWindows, - Action>? scriptSetup = null) + Action? scriptSetup = null) { var console = new TestConsole(); var consoleFormatProvider = new ConsoleFormatInfo @@ -249,7 +249,7 @@ private static (TestConsole console, CommandGroupRunner groupRunner) SetUpTest( }; var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: true); - var scripts = new Dictionary(StringComparer.OrdinalIgnoreCase) + var scripts = new ScriptCollection { // clean { "clean", "echo clean" }, diff --git a/test/GlobalCommandsTests.cs b/test/GlobalCommandsTests.cs index 57066e8..068dc20 100644 --- a/test/GlobalCommandsTests.cs +++ b/test/GlobalCommandsTests.cs @@ -20,7 +20,7 @@ public async Task Should_log_all_available_scripts() }; var consoleWriter = new ConsoleWriter(console, consoleFormatProvider, verbose: true); - var scripts = new Dictionary(StringComparer.OrdinalIgnoreCase) + var scripts = new ScriptCollection { { "clean", "echo clean" }, { "prebuild", "echo prebuild" }, diff --git a/test/Integration/CommandBuilderTests.cs b/test/Integration/CommandBuilderTests.cs index 33b37ad..4648d70 100644 --- a/test/Integration/CommandBuilderTests.cs +++ b/test/Integration/CommandBuilderTests.cs @@ -53,7 +53,7 @@ private static async Task Should_execute_single_script_in_shell(bool isWindows, var project = new Project { - Scripts = new Dictionary(StringComparer.OrdinalIgnoreCase) + Scripts = new ScriptCollection { { "test", "echo testing" }, }, diff --git a/test/ModuleInitializer.cs b/test/ModuleInitializer.cs index bb335e0..f41d7fe 100644 --- a/test/ModuleInitializer.cs +++ b/test/ModuleInitializer.cs @@ -8,5 +8,6 @@ public static class ModuleInitializer public static void Init() { VerifierSettings.AddExtraSettings(settings => settings.Converters.Add(new ConsoleConverter())); + VerifierSettings.AddExtraSettings(settings => settings.Converters.Add(new ScriptCollectionConverter())); } } diff --git a/test/ProjectLoaderTests.Should_not_treat_script_names_as_always_lowercase.verified.txt b/test/ProjectLoaderTests.Should_not_treat_script_names_as_always_lowercase.verified.txt new file mode 100644 index 0000000..333896c --- /dev/null +++ b/test/ProjectLoaderTests.Should_not_treat_script_names_as_always_lowercase.verified.txt @@ -0,0 +1,7 @@ +{ + Scripts: { + bUiLD: build, + TEST: test, + pack: pack + } +} \ No newline at end of file diff --git a/test/ProjectLoaderTests.cs b/test/ProjectLoaderTests.cs index 091d192..33030a7 100644 --- a/test/ProjectLoaderTests.cs +++ b/test/ProjectLoaderTests.cs @@ -85,7 +85,7 @@ public async Task Should_look_up_the_tree() } [Fact] - public async Task Should_treat_script_names_as_lowercase() + public async Task Should_not_treat_script_names_as_always_lowercase() { // Given var testPath = TestPath("script-names"); @@ -95,8 +95,6 @@ public async Task Should_treat_script_names_as_lowercase() var (project, _) = await projectLoader.LoadAsync(testPath); // Then - project.Scripts?.Comparer.ShouldBe(StringComparer.OrdinalIgnoreCase); - await Verify(project); } diff --git a/test/RunScriptCommandTests.cs b/test/RunScriptCommandTests.cs index c81653a..0ac3d18 100644 --- a/test/RunScriptCommandTests.cs +++ b/test/RunScriptCommandTests.cs @@ -9,11 +9,11 @@ public static class RunScriptCommandTests [Trait("category", "unit")] public class FindScripts { - private readonly Dictionary _projectScripts; + private readonly ScriptCollection _projectScripts; public FindScripts() { - _projectScripts = new Dictionary(StringComparer.OrdinalIgnoreCase) + _projectScripts = new ScriptCollection { { "clean", "echo clean" }, { "prebuild", "echo prebuild" }, diff --git a/test/ScriptCollectionConverter.cs b/test/ScriptCollectionConverter.cs new file mode 100644 index 0000000..14120e3 --- /dev/null +++ b/test/ScriptCollectionConverter.cs @@ -0,0 +1,20 @@ +namespace RunScript; + +public class ScriptCollectionConverter : WriteOnlyJsonConverter +{ + public override void Write(VerifyJsonWriter writer, ScriptCollection value) + { + if (writer is null) throw new ArgumentNullException(nameof(writer)); + if (value is null) throw new ArgumentNullException(nameof(value)); + + writer.WriteStartObject(); + + foreach (var (name, script) in value) + { + writer.WritePropertyName(name); + writer.WriteValue(script); + } + + writer.WriteEndObject(); + } +}