diff --git a/src/WebCompiler/Config/Config.cs b/src/WebCompiler/Config/Config.cs index 95aecee6..09b61c37 100644 --- a/src/WebCompiler/Config/Config.cs +++ b/src/WebCompiler/Config/Config.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.IO; using System.Linq; + using Newtonsoft.Json; namespace WebCompiler @@ -23,12 +24,25 @@ public class Config [JsonProperty("outputFile")] public string OutputFile { get; set; } + /// + /// The extension to be used for output files - valid when is wildcard extension. + /// + [JsonIgnore] + public string OutputExtension => this.OutputFile.Substring(1); + + /// /// The relative file path to the input file. /// [JsonProperty("inputFile")] public string InputFile { get; set; } + /// + /// The extension to match input files - valid when is a wildcard extension. + /// + [JsonIgnore] + public string InputExtension => this.InputFile.Substring(1); + /// /// Settings for the minification. /// @@ -62,6 +76,19 @@ public class Config internal string Output { get; set; } + + /// + /// Determines if the config is only an extension pattern - not real file to process. + /// + [JsonIgnore] + public bool IsExtensionPattern => this.InputFile?.StartsWith("*") ?? false; + + /// + /// Marks that the config is created from the extension expansion and not defined in the compilerconfig file. + /// + [JsonIgnore] + public bool IsFromExtensionPattern { get; set; } + /// /// Converts the relative input file to an absolute file path. /// diff --git a/src/WebCompiler/Config/ConfigFileProcessor.cs b/src/WebCompiler/Config/ConfigFileProcessor.cs index aedc8dda..612fa051 100644 --- a/src/WebCompiler/Config/ConfigFileProcessor.cs +++ b/src/WebCompiler/Config/ConfigFileProcessor.cs @@ -31,6 +31,11 @@ public IEnumerable Process(string configFile, IEnumerable SourceFileChanged(string configFile, { string folder = Path.GetDirectoryName(configFile); List list = new List(); - var configs = ConfigHandler.GetConfigs(configFile); + var configs = ConfigHandler.GetConfigs(configFile, sourceFile); // Compile if the file if it's referenced directly in compilerconfig.json foreach (Config config in configs) @@ -161,15 +166,16 @@ private IEnumerable SourceFileChanged(string configFile, /// /// Returns a collection of config objects that all contain the specified sourceFile /// - public static IEnumerable IsFileConfigured(string configFile, string sourceFile) + /// Set to true so that extension based config is ignored. + public static IEnumerable IsFileConfigured(string configFile, string sourceFile, bool ignoreExtensionConfig = false) { try { - var configs = ConfigHandler.GetConfigs(configFile); + var configs = ConfigHandler.GetConfigs(configFile, sourceFile); string folder = Path.GetDirectoryName(configFile); List list = new List(); - foreach (Config config in configs) + foreach (Config config in configs.Where(x=> !ignoreExtensionConfig || !x.IsFromExtensionPattern)) { string input = Path.Combine(folder, config.InputFile.Replace("/", "\\")); diff --git a/src/WebCompiler/Config/ConfigHandler.cs b/src/WebCompiler/Config/ConfigHandler.cs index ed5e7192..ec39c1e4 100644 --- a/src/WebCompiler/Config/ConfigHandler.cs +++ b/src/WebCompiler/Config/ConfigHandler.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -11,14 +12,16 @@ namespace WebCompiler /// public class ConfigHandler { + private static ConcurrentDictionary> ExtensionBasedConfigs { get; } = new ConcurrentDictionary>(); + /// /// Adds a config file if no one exist or adds the specified config to an existing config file. /// /// The file path of the configuration file. - /// The compiler config object to add to the configration file. + /// The compiler config object to add to the configuration file. public void AddConfig(string fileName, Config config) { - IEnumerable existing = GetConfigs(fileName); + IEnumerable existing = GetConfigs(fileName, expandExtensions: false); List configs = new List(); configs.AddRange(existing); configs.Add(config); @@ -39,7 +42,7 @@ public void AddConfig(string fileName, Config config) /// public void RemoveConfig(Config configToRemove) { - IEnumerable configs = GetConfigs(configToRemove.FileName); + IEnumerable configs = GetConfigs(configToRemove.FileName, expandExtensions: false); List newConfigs = new List(); if (configs.Contains(configToRemove)) @@ -95,24 +98,85 @@ public void CreateDefaultsFile(string fileName) /// Get all the config objects in the specified file. /// /// A relative or absolute file path to the configuration file. + /// The name of the source file that is being modified/selected + /// The flag that states if wildcard extension config entry should be processed. If true all files that satisfy it would be returned. /// A list of Config objects. - public static IEnumerable GetConfigs(string fileName) + public static IEnumerable GetConfigs(string fileName, string sourceFile = null, bool expandExtensions = true) { FileInfo file = new FileInfo(fileName); if (!file.Exists) return Enumerable.Empty(); - string content = File.ReadAllText(fileName); + var content = File.ReadAllText(fileName); var configs = JsonConvert.DeserializeObject>(content); - string folder = Path.GetDirectoryName(file.FullName); - + var folder = Path.GetDirectoryName(file.FullName); + var extensionConfigs = new List(); foreach (Config config in configs) { + if (config.IsExtensionPattern + && (sourceFile == null || sourceFile.EndsWith(config.InputExtension)) + && expandExtensions) + { + var cacheKey = $"{Path.GetFullPath(fileName)}-{config.InputExtension}"; + + ProcessExtensionPattern(fileName, sourceFile, folder, cacheKey, config); + extensionConfigs.AddRange(ExtensionBasedConfigs[cacheKey].Values.Where(ec => !configs.Any(c => c.InputFile?.Replace("/", "\\") == ec.InputFile))); + } config.FileName = fileName; } - return configs; + return configs.Where(c => !c.IsExtensionPattern || !expandExtensions).Concat(extensionConfigs); + } + + private static void ProcessExtensionPattern(string fileName, string sourceFile, string folder, string cacheKey, Config config) + { + if (!ExtensionBasedConfigs.ContainsKey(cacheKey)) + { + var folderLength = folder.Length + 1; + var files = Directory.GetFiles(folder, $"{config.InputFile}", SearchOption.AllDirectories); + var fileConfigs = files.ToDictionary(f => f, f => + { + var inputFile = f.Substring(folderLength); + return new Config() + { + FileName = fileName, + InputFile = inputFile, + OutputFile = inputFile.Replace(config.InputExtension, config.OutputExtension), + Minify = config.Minify, + Options = config.Options, + SourceMap = config.SourceMap, + UseNodeSass = config.UseNodeSass, + IncludeInProject = config.IncludeInProject, + IsFromExtensionPattern = true + }; + }); + + ExtensionBasedConfigs.TryAdd(cacheKey, new ConcurrentDictionary(fileConfigs)); + } + else if (sourceFile != null && !ExtensionBasedConfigs[cacheKey].ContainsKey(sourceFile)) + { + ExtensionBasedConfigs[cacheKey].TryAdd(sourceFile, new Config() + { + FileName = fileName, + InputFile = sourceFile, + OutputFile = sourceFile.Replace(config.InputExtension, config.OutputExtension), + Minify = config.Minify, + Options = config.Options, + SourceMap = config.SourceMap, + UseNodeSass = config.UseNodeSass, + IncludeInProject = config.IncludeInProject, + IsFromExtensionPattern = true, + }); + } + } + + /// + /// Clears the configs based on input extensions. + /// + public static void ClearExtensionBasedConfigs() + { + ExtensionBasedConfigs.Clear(); } } } diff --git a/src/WebCompiler/Program.cs b/src/WebCompiler/Program.cs index 068d49db..29752483 100644 --- a/src/WebCompiler/Program.cs +++ b/src/WebCompiler/Program.cs @@ -60,7 +60,7 @@ private static IEnumerable GetConfigs(string configPath, string file) if (file != null) { if (file.StartsWith("*")) - configs = configs.Where(c => Path.GetExtension(c.InputFile).Equals(file.Substring(1), StringComparison.OrdinalIgnoreCase)); + configs = configs.Where(c => c.InputFile.EndsWith(file.Substring(1), StringComparison.OrdinalIgnoreCase)); else configs = configs.Where(c => c.InputFile.Equals(file, StringComparison.OrdinalIgnoreCase)); } diff --git a/src/WebCompilerTest/Config/ConfigFileProcessorTest.cs b/src/WebCompilerTest/Config/ConfigFileProcessorTest.cs new file mode 100644 index 00000000..10d9132f --- /dev/null +++ b/src/WebCompilerTest/Config/ConfigFileProcessorTest.cs @@ -0,0 +1,51 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.IO; +using System.Linq; + +using WebCompiler; + +namespace WebCompilerTest.Config +{ + [TestClass] + public class ConfigFileProcessorTest + { + private const string configFileWithExtensions = "../../artifacts/configwithextensions.json"; + + [TestMethod, TestCategory("Config")] + public void IsFileConfigured_WhenSourceFileMatchTheExtension_ShouldReturnConfigForThatFile() + { + var configFile = new FileInfo(configFileWithExtensions); + var configFileFolder = new FileInfo(configFileWithExtensions).DirectoryName; + var test1FilePath = new FileInfo("../../artifacts/scss/test1.razor.scss"); + var test2FilePath = new FileInfo("../../artifacts/scss/test2.razor.scss"); + var expectedTest1InputFile = test1FilePath.FullName.Replace(configFileFolder, "").Substring(1); + var expectedTest2InputFile = test2FilePath.FullName.Replace(configFileFolder, "").Substring(1); + + + var test1Config = ConfigFileProcessor.IsFileConfigured(configFile.FullName, test1FilePath.FullName).FirstOrDefault(x => x.InputFile == expectedTest1InputFile); + var test2Config = ConfigFileProcessor.IsFileConfigured(configFile.FullName, test2FilePath.FullName).FirstOrDefault(x => x.InputFile == expectedTest2InputFile); + + Assert.IsNotNull(test1Config); + Assert.IsNotNull(test2Config); + } + + [TestMethod, TestCategory("Config")] + public void IsFileConfigured_WhenSourceFileMatchTheExtensionAndIgnoreExtensionConfigIsTrue_ShouldNotReturnConfigForThatFile() + { + var configFile = new FileInfo(configFileWithExtensions); + var configFileFolder = new FileInfo(configFileWithExtensions).DirectoryName; + var test1FilePath = new FileInfo("../../artifacts/scss/test1.razor.scss"); + var test2FilePath = new FileInfo("../../artifacts/scss/test2.razor.scss"); + var expectedTest1InputFile = test1FilePath.FullName.Replace(configFileFolder, "").Substring(1); + var expectedTest2InputFile = test2FilePath.FullName.Replace(configFileFolder, "").Substring(1); + + + var test1Config = ConfigFileProcessor.IsFileConfigured(configFile.FullName, test1FilePath.FullName, ignoreExtensionConfig: true).FirstOrDefault(x => x.InputFile == expectedTest1InputFile); + var test2Config = ConfigFileProcessor.IsFileConfigured(configFile.FullName, test2FilePath.FullName, ignoreExtensionConfig: true).FirstOrDefault(x => x.InputFile == expectedTest2InputFile); + + Assert.IsNull(test1Config); + Assert.IsNull(test2Config); + } + } +} diff --git a/src/WebCompilerTest/Config/ConfigHandlerTest.cs b/src/WebCompilerTest/Config/ConfigHandlerTest.cs index eb809266..a087e9fc 100644 --- a/src/WebCompilerTest/Config/ConfigHandlerTest.cs +++ b/src/WebCompilerTest/Config/ConfigHandlerTest.cs @@ -1,6 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; + using System.IO; using System.Linq; + using WebCompiler; namespace WebCompilerTest.Config @@ -13,6 +15,8 @@ public class ConfigHandlerTest private const string originalConfigFile = "../../artifacts/config/originalcoffeeconfig.json"; private const string processingConfigFile = "../../artifacts/config/coffeeconfig.json"; + private const string configFileWithExtensions = "../../artifacts/configwithextensions.json"; + [TestInitialize] public void Setup() { @@ -51,5 +55,68 @@ public void NonExistingConfigFileShouldReturnEmptyList() Assert.AreEqual(expectedResult, result); } + + [TestMethod, TestCategory("Config")] + public void GetConfig_WhenExpandExtensionsIsNotProvided_ReturnsConfigsIncludingFilesMatchingExtensions() + { + var configs = ConfigHandler.GetConfigs(configFileWithExtensions); + var configFileFolder = new FileInfo(configFileWithExtensions).DirectoryName; + var test1FilePath = new FileInfo("../../artifacts/scss/test1.razor.scss"); + var test2FilePath = new FileInfo("../../artifacts/scss/test2.razor.scss"); + var expectedTest1InputFile = test1FilePath.FullName.Replace(configFileFolder, "").Substring(1); + var expectedTest2InputFile = test2FilePath.FullName.Replace(configFileFolder, "").Substring(1); + + var test1Config = configs.SingleOrDefault(x => x.IsFromExtensionPattern && x.InputFile == expectedTest1InputFile); + var test2Config = configs.SingleOrDefault(x => x.IsFromExtensionPattern && x.InputFile == expectedTest2InputFile); + + + Assert.IsNotNull(test1Config); + Assert.IsNotNull(test2Config); + + Assert.IsTrue(test1Config.IsFromExtensionPattern); + Assert.IsTrue(test2Config.IsFromExtensionPattern); + + Assert.AreEqual(test1Config.OutputFile ,expectedTest1InputFile.Replace(".scss",".css")); + Assert.AreEqual(test2Config.OutputFile , expectedTest2InputFile.Replace(".scss",".css")); + + Assert.IsFalse((bool)test1Config.Minify["enabled"]); + Assert.IsFalse((bool)test1Config.Minify["enabled"]); + + Assert.AreEqual(3, configs.Count()); + Assert.AreEqual(0, configs.Where(x => x.IsExtensionPattern).Count()); + } + + [TestMethod, TestCategory("Config")] + public void GetConfig_WhenExpandExtensionsIsFalse_ReturnsConfigsWithoutFilesMatchingExtensions() + { + var configs = ConfigHandler.GetConfigs(configFileWithExtensions, expandExtensions: false); + + Assert.AreEqual(2, configs.Count()); + Assert.AreEqual(1, configs.Where(x => x.IsExtensionPattern).Count()); + } + + [TestMethod, TestCategory("Config")] + public void GetConfig_WhenExpandExtensionsIsFalseAndSourceFileProvided_ReturnsConfigsWithoutFilesMatchingExtensions() + { + var configs = ConfigHandler.GetConfigs(configFileWithExtensions, sourceFile: "newfile.razor.scss", expandExtensions: false); + + Assert.AreEqual(2, configs.Count()); + Assert.AreEqual(1, configs.Where(x => x.IsExtensionPattern).Count()); + } + + [TestMethod, TestCategory("Config")] + public void GetConfig_WhenSourceFileWithValidExtensionIsProvidedAndCacheAlreadyFilled_ReturnsConfigsIncludingFile() + { + var newFile = "newFile.razor.scss"; + + // trigger loading existing files to dictionary cache + var configs = ConfigHandler.GetConfigs(configFileWithExtensions); + + configs = ConfigHandler.GetConfigs(configFileWithExtensions, newFile); + + Assert.AreEqual(4, configs.Count()); + Assert.AreEqual(1, configs.Where(x => x.InputFile.Contains(newFile)).Count()); + } + } } diff --git a/src/WebCompilerTest/Config/ConfigTest.cs b/src/WebCompilerTest/Config/ConfigTest.cs index a291400e..4ef69607 100644 --- a/src/WebCompilerTest/Config/ConfigTest.cs +++ b/src/WebCompilerTest/Config/ConfigTest.cs @@ -16,6 +16,9 @@ public class ConfigTest private const string firstLevelDependencyFile = "../../artifacts/config/dependencies/foo.scss"; private const string secondLevelDependencyFile = "../../artifacts/config/dependencies/sub/bar.scss"; + private const string inputFileWildcardExtension = "*.razor.scss"; + private const string outputFileWildcardExtension = "*.razor.css"; + private readonly FileInfo _inputFileInfo = new FileInfo(inputFile); private readonly FileInfo _outputFileInfo = new FileInfo(outputFile); private readonly FileInfo _firstLevelDependencyFileInfo = new FileInfo(firstLevelDependencyFile); @@ -116,5 +119,59 @@ public void CompilationRequired_SecondLevelDependencyNewerThanOutput_RequiresCom Assert.AreEqual(true, compilationRequired); } + + [TestMethod, TestCategory("Config")] + public void InputFileExtension_WhenInputFileIsWildcardExtension_StripsAsterisk() + { + var config = new WebCompiler.Config() + { + InputFile = "*.razor.scss", + }; + + Assert.AreEqual(".razor.scss", config.InputExtension); + } + + [TestMethod, TestCategory("Config")] + public void OutputFileExtension_WhenOutputFileIsWildcardExtension_StripsAsterisk() + { + var config = new WebCompiler.Config() + { + OutputFile = "*.razor.css", + }; + + Assert.AreEqual(".razor.css", config.OutputExtension); + } + + [TestMethod, TestCategory("Config")] + public void IsExtensionPattern_WhenInputFileIsWildcardExtension_ReturnsTrue() + { + var config = new WebCompiler.Config() + { + InputFile = "*.razor.scss", + }; + + Assert.AreEqual(true, config.IsExtensionPattern); + } + + [TestMethod, TestCategory("Config")] + public void IsExtensionPattern_WhenInputFileIsNotWildcardExtension_ReturnsFalse() + { + var config = new WebCompiler.Config() + { + InputFile = "somefile.razor.scss", + }; + + Assert.AreEqual(false, config.IsExtensionPattern); + } + + [TestMethod, TestCategory("Config")] + public void IsExtensionPattern_WhenInputFileIsNotSet_ReturnsFalse() + { + var config = new WebCompiler.Config() + { + }; + + Assert.AreEqual(false, config.IsExtensionPattern); + } } } diff --git a/src/WebCompilerTest/artifacts/configWithExtensions.json b/src/WebCompilerTest/artifacts/configWithExtensions.json new file mode 100644 index 00000000..4d1c598c --- /dev/null +++ b/src/WebCompilerTest/artifacts/configWithExtensions.json @@ -0,0 +1,16 @@ +[ + { + "outputFile": "*.razor.css", + "inputFile": "*.razor.scss", + "minify": { + "enabled": false + }, + "includeInProject": true + }, + { + "outputFile": "../scss/test.css", + "inputFile": "../scss/test.scss", + "minify": {}, + "includeInProject": true + } +] \ No newline at end of file diff --git a/src/WebCompilerTest/artifacts/scss/test1.razor.scss b/src/WebCompilerTest/artifacts/scss/test1.razor.scss new file mode 100644 index 00000000..46800d16 --- /dev/null +++ b/src/WebCompilerTest/artifacts/scss/test1.razor.scss @@ -0,0 +1,2 @@ +body { +} diff --git a/src/WebCompilerTest/artifacts/scss/test2.razor.scss b/src/WebCompilerTest/artifacts/scss/test2.razor.scss new file mode 100644 index 00000000..46800d16 --- /dev/null +++ b/src/WebCompilerTest/artifacts/scss/test2.razor.scss @@ -0,0 +1,2 @@ +body { +} diff --git a/src/WebCompilerVsix/Commands/RemoveConfig.cs b/src/WebCompilerVsix/Commands/RemoveConfig.cs index 35f93fd8..4177a4ae 100644 --- a/src/WebCompilerVsix/Commands/RemoveConfig.cs +++ b/src/WebCompilerVsix/Commands/RemoveConfig.cs @@ -56,7 +56,7 @@ private void BeforeQueryStatus(object sender, EventArgs e) string configFile = item.ContainingProject.GetConfigFile(); - _configs = ConfigFileProcessor.IsFileConfigured(configFile, sourceFile); + _configs = ConfigFileProcessor.IsFileConfigured(configFile, sourceFile, true); button.Visible = _configs != null && _configs.Any(); } diff --git a/src/WebCompilerVsix/TaskRunner/WebCompilerTaskRunner.cs b/src/WebCompilerVsix/TaskRunner/WebCompilerTaskRunner.cs index 5e86be20..7304a6a4 100644 --- a/src/WebCompilerVsix/TaskRunner/WebCompilerTaskRunner.cs +++ b/src/WebCompilerVsix/TaskRunner/WebCompilerTaskRunner.cs @@ -68,7 +68,7 @@ private ITaskRunnerNode LoadHierarchy(string configPath) private ITaskRunnerNode GetFileType(string configPath, string extension) { - var configs = ConfigHandler.GetConfigs(configPath); + var configs = ConfigHandler.GetConfigs(configPath, expandExtensions: false); var types = configs?.Where(c => Path.GetExtension(c.InputFile).Equals(extension, StringComparison.OrdinalIgnoreCase)); if (types == null || !types.Any())