├── .editorconfig ├── .gitattributes ├── .github ├── CONTRIBUTING.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── LessCompiler.sln ├── README.md ├── appveyor.yml ├── art ├── context-menu-solution.png ├── solution-explorer.png ├── watermark-ignored.png ├── watermark-off.png └── watermark-on.png ├── src ├── Adornments │ ├── AdornmentLayer.cs │ ├── CssAdornment.cs │ └── LessAdornment.cs ├── Commands │ ├── ReCompileAll.cs │ └── SaveHandler.cs ├── Compiler │ ├── CompilerOptions.cs │ ├── CompilerResult.cs │ ├── CompilerService.cs │ ├── LessCatalog.cs │ ├── NodeProcess.cs │ └── ProjectMap.cs ├── Helpers │ ├── AsyncLock.cs │ ├── Logger.cs │ └── VsHelpers.cs ├── LessCompiler.csproj ├── LessCompilerPackage.cs ├── Properties │ └── AssemblyInfo.cs ├── Resources │ └── Icon.png ├── Settings │ ├── Settings.cs │ └── SettingsChangedEventArgs.cs ├── VSCommandTable.cs ├── VSCommandTable.vsct ├── packages.config ├── source.extension.cs ├── source.extension.vsixmanifest └── website.pkgdef └── test └── LessCompiler.Test ├── CompilerOptionsTest.cs ├── LessCompiler.Test.csproj ├── NodeProcessTest.cs ├── Properties └── AssemblyInfo.cs ├── artifacts ├── _underscore.less ├── autoprefix.less ├── defaults │ ├── customdefaults.less │ └── less.defaults ├── sourcemap.less └── undefined-variable.less └── packages.config /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | end_of_line = crlf 10 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 11 | 12 | # Code files 13 | [*.{cs,csx,vb,vbx}] 14 | indent_size = 4 15 | 16 | # Xml project files 17 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 18 | indent_size = 2 19 | 20 | # Xml config files 21 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 22 | indent_size = 2 23 | 24 | # JSON files 25 | [*.json] 26 | indent_size = 2 27 | 28 | # Dotnet code style settings: 29 | [*.{cs,vb}] 30 | # Sort using and Import directives with System.* appearing first 31 | dotnet_sort_system_directives_first = true 32 | # Avoid "this." and "Me." if not necessary 33 | dotnet_style_qualification_for_field = false:suggestion 34 | dotnet_style_qualification_for_property = false:suggestion 35 | dotnet_style_qualification_for_method = false:suggestion 36 | dotnet_style_qualification_for_event = false:suggestion 37 | 38 | # Use language keywords instead of framework type names for type references 39 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 40 | dotnet_style_predefined_type_for_member_access = true:suggestion 41 | 42 | # Suggest more modern language features when available 43 | dotnet_style_object_initializer = true:suggestion 44 | dotnet_style_collection_initializer = true:suggestion 45 | dotnet_style_coalesce_expression = true:suggestion 46 | dotnet_style_null_propagation = true:suggestion 47 | dotnet_style_explicit_tuple_names = true:suggestion 48 | 49 | # CSharp code style settings: 50 | [*.cs] 51 | # Prefer "var" everywhere 52 | csharp_style_var_for_built_in_types = false:suggestion 53 | csharp_style_var_when_type_is_apparent = true:suggestion 54 | csharp_style_var_elsewhere = false:suggestion 55 | 56 | # Prefer method-like constructs to have a block body 57 | csharp_style_expression_bodied_methods = false:none 58 | csharp_style_expression_bodied_constructors = false:none 59 | csharp_style_expression_bodied_operators = false:none 60 | 61 | # Prefer property-like constructs to have an expression-body 62 | csharp_style_expression_bodied_properties = true:none 63 | csharp_style_expression_bodied_indexers = true:none 64 | csharp_style_expression_bodied_accessors = true:none 65 | 66 | # Suggest more modern language features when available 67 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 68 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 69 | csharp_style_inlined_variable_declaration = true:suggestion 70 | csharp_style_throw_expression = true:suggestion 71 | csharp_style_conditional_delegate_call = true:suggestion 72 | 73 | # Newline settings 74 | csharp_new_line_before_open_brace = all 75 | csharp_new_line_before_else = true 76 | csharp_new_line_before_catch = true 77 | csharp_new_line_before_finally = true 78 | csharp_new_line_before_members_in_object_initializers = true 79 | csharp_new_line_before_members_in_anonymous_types = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Looking to contribute something? **Here's how you can help.** 4 | 5 | Please take a moment to review this document in order to make the contribution 6 | process easy and effective for everyone involved. 7 | 8 | Following these guidelines helps to communicate that you respect the time of 9 | the developers managing and developing this open source project. In return, 10 | they should reciprocate that respect in addressing your issue or assessing 11 | patches and features. 12 | 13 | 14 | ## Using the issue tracker 15 | 16 | The issue tracker is the preferred channel for [bug reports](#bug-reports), 17 | [features requests](#feature-requests) and 18 | [submitting pull requests](#pull-requests), but please respect the 19 | following restrictions: 20 | 21 | * Please **do not** use the issue tracker for personal support requests. Stack 22 | Overflow is a better place to get help. 23 | 24 | * Please **do not** derail or troll issues. Keep the discussion on topic and 25 | respect the opinions of others. 26 | 27 | * Please **do not** open issues or pull requests which *belongs to* third party 28 | components. 29 | 30 | 31 | ## Bug reports 32 | 33 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 34 | Good bug reports are extremely helpful, so thanks! 35 | 36 | Guidelines for bug reports: 37 | 38 | 1. **Use the GitHub issue search** — check if the issue has already been 39 | reported. 40 | 41 | 2. **Check if the issue has been fixed** — try to reproduce it using the 42 | latest `master` or development branch in the repository. 43 | 44 | 3. **Isolate the problem** — ideally create an 45 | [SSCCE](http://www.sscce.org/) and a live example. 46 | Uploading the project on cloud storage (OneDrive, DropBox, et el.) 47 | or creating a sample GitHub repository is also helpful. 48 | 49 | 50 | A good bug report shouldn't leave others needing to chase you up for more 51 | information. Please try to be as detailed as possible in your report. What is 52 | your environment? What steps will reproduce the issue? What browser(s) and OS 53 | experience the problem? Do other browsers show the bug differently? What 54 | would you expect to be the outcome? All these details will help people to fix 55 | any potential bugs. 56 | 57 | Example: 58 | 59 | > Short and descriptive example bug report title 60 | > 61 | > A summary of the issue and the Visual Studio, browser, OS environments 62 | > in which it occurs. If suitable, include the steps required to reproduce the bug. 63 | > 64 | > 1. This is the first step 65 | > 2. This is the second step 66 | > 3. Further steps, etc. 67 | > 68 | > `` - a link to the project/file uploaded on cloud storage or other publicly accessible medium. 69 | > 70 | > Any other information you want to share that is relevant to the issue being 71 | > reported. This might include the lines of code that you have identified as 72 | > causing the bug, and potential solutions (and your opinions on their 73 | > merits). 74 | 75 | 76 | ## Feature requests 77 | 78 | Feature requests are welcome. But take a moment to find out whether your idea 79 | fits with the scope and aims of the project. It's up to *you* to make a strong 80 | case to convince the project's developers of the merits of this feature. Please 81 | provide as much detail and context as possible. 82 | 83 | 84 | ## Pull requests 85 | 86 | Good pull requests, patches, improvements and new features are a fantastic 87 | help. They should remain focused in scope and avoid containing unrelated 88 | commits. 89 | 90 | **Please ask first** before embarking on any significant pull request (e.g. 91 | implementing features, refactoring code, porting to a different language), 92 | otherwise you risk spending a lot of time working on something that the 93 | project's developers might not want to merge into the project. 94 | 95 | Please adhere to the [coding guidelines](#code-guidelines) used throughout the 96 | project (indentation, accurate comments, etc.) and any other requirements 97 | (such as test coverage). 98 | 99 | Adhering to the following process is the best way to get your work 100 | included in the project: 101 | 102 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 103 | and configure the remotes: 104 | 105 | ```bash 106 | # Clone your fork of the repo into the current directory 107 | git clone https://github.com//.git 108 | # Navigate to the newly cloned directory 109 | cd 110 | # Assign the original repo to a remote called "upstream" 111 | git remote add upstream https://github.com/madskristensen/.git 112 | ``` 113 | 114 | 2. If you cloned a while ago, get the latest changes from upstream: 115 | 116 | ```bash 117 | git checkout master 118 | git pull upstream master 119 | ``` 120 | 121 | 3. Create a new topic branch (off the main project development branch) to 122 | contain your feature, change, or fix: 123 | 124 | ```bash 125 | git checkout -b 126 | ``` 127 | 128 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 129 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 130 | or your code is unlikely be merged into the main project. Use Git's 131 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 132 | feature to tidy up your commits before making them public. Also, prepend name of the feature 133 | to the commit message. For instance: "SCSS: Fixes compiler results for IFileListener.\nFixes `#123`" 134 | 135 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 136 | 137 | ```bash 138 | git pull [--rebase] upstream master 139 | ``` 140 | 141 | 6. Push your topic branch up to your fork: 142 | 143 | ```bash 144 | git push origin 145 | ``` 146 | 147 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 148 | with a clear title and description against the `master` branch. 149 | 150 | 151 | ## Code guidelines 152 | 153 | - Always use proper indentation. 154 | - In Visual Studio under `Tools > Options > Text Editor > C# > Advanced`, make sure 155 | `Place 'System' directives first when sorting usings` option is enabled (checked). 156 | - Before committing, organize usings for each updated C# source file. Either you can 157 | right-click editor and select `Organize Usings > Remove and sort` OR use extension 158 | like [BatchFormat](http://visualstudiogallery.msdn.microsoft.com/a7f75c34-82b4-4357-9c66-c18e32b9393e). 159 | - Before committing, run Code Analysis in `Debug` configuration and follow the guidelines 160 | to fix CA issues. Code Analysis commits can be made separately. 161 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Installed product versions 2 | - Visual Studio: [example 2015 Professional] 3 | - This extension: [example 1.1.21] 4 | 5 | ### Description 6 | Replace this text with a short description 7 | 8 | ### Steps to recreate 9 | 1. Replace this 10 | 2. text with 11 | 3. the steps 12 | 4. to recreate 13 | 14 | ### Current behavior 15 | Explain what it's doing and why it's wrong 16 | 17 | ### Expected behavior 18 | Explain what it should be doing after it's fixed. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | 3 | # User files 4 | *.suo 5 | *.user 6 | *.sln.docstates 7 | .vs/ 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Rr]elease/ 12 | x64/ 13 | [Bb]in/ 14 | [Oo]bj/ 15 | 16 | # MSTest test Results 17 | [Tt]est[Rr]esult*/ 18 | [Bb]uild[Ll]og.* 19 | 20 | # NCrunch 21 | *.ncrunchsolution 22 | *.ncrunchproject 23 | _NCrunch_WebCompiler -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Road map 2 | 3 | - [ ] Support linked .less files 4 | - [ ] Move all strings to .resx file 5 | 6 | Features that have a checkmark are complete and available for 7 | download in the 8 | [CI build](http://vsixgallery.com/extension/7df8a985-0e26-4aab-95fc-f48ee61b086a/). 9 | 10 | # Change log 11 | 12 | These are the changes to each version that has been released 13 | on the official Visual Studio extension gallery. 14 | 15 | ## 0.9 16 | 17 | - [x] Get .less files from project system instead of file system 18 | - [x] Output window log time to build LESS catalog 19 | - [x] Made .less file parsing async 20 | - [x] File nesting support for Website Projects 21 | - [x] Made CSSComb opt-in only 22 | - [x] Changed autoprefix value to `> 1%` 23 | - [x] Show more information on adornment 24 | - [x] Dark theme support for watermarks 25 | 26 | ## 0.8 27 | 28 | - [x] Install npm modules when VS starts 29 | - [x] Alert when the npm modules are installed when .less file is saved 30 | - [x] Compile parent .less files when imported file changes 31 | - [x] Context menu command to enable compiler for project 32 | - [x] Adornment to toggle compilation per solution 33 | - [x] Command to compile less files in all enabled projects in solution 34 | - [x] Make project/solution compiler defaults configurable 35 | - [x] Adapt to renames, additions and deletions from project 36 | - [x] Make indication that .css file generated 37 | 38 | ## 0.7 39 | 40 | - [x] Configurable options per file 41 | - [x] Add unit tests 42 | - [x] Support minification 43 | - [x] Support source maps 44 | - [x] Allow spaces in output file names 45 | 46 | ## 0.6 47 | 48 | - [x] Initial release 49 | - [x] Install npm modules 50 | - [x] Compiles on save 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Mads Kristensen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /LessCompiler.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26212.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LessCompiler", "src\LessCompiler.csproj", "{D1F752E5-6CAB-4513-980A-51B44E104CDF}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{50552A90-8343-4135-A563-AE454C22F3D6}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | appveyor.yml = appveyor.yml 12 | CHANGELOG.md = CHANGELOG.md 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LessCompiler.Test", "test\LessCompiler.Test\LessCompiler.Test.csproj", "{44F8565C-6AED-433C-A2BE-221D7A03EFF8}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {D1F752E5-6CAB-4513-980A-51B44E104CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {D1F752E5-6CAB-4513-980A-51B44E104CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {D1F752E5-6CAB-4513-980A-51B44E104CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {D1F752E5-6CAB-4513-980A-51B44E104CDF}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {44F8565C-6AED-433C-A2BE-221D7A03EFF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {44F8565C-6AED-433C-A2BE-221D7A03EFF8}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {44F8565C-6AED-433C-A2BE-221D7A03EFF8}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {44F8565C-6AED-433C-A2BE-221D7A03EFF8}.Release|Any CPU.Build.0 = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(SolutionProperties) = preSolution 34 | HideSolutionNode = FALSE 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LESS Compiler 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/uh1b5p1wx3ld64r9?svg=true)](https://ci.appveyor.com/project/madskristensen/lesscompiler) 4 | 5 | Download this extension from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.LESSCompiler) 6 | or get the [CI build](http://vsixgallery.com/extension/d32c5250-fa82-4da6-9732-5518fabebfef/). 7 | 8 | --------------------------------------- 9 | 10 | An alternative LESS compiler with no setup. Uses the official node.js based LESS compiler under the hood with AutoPrefixer and CSSComb built in. 11 | 12 | See the [change log](CHANGELOG.md) for changes and road map. 13 | 14 | ## Features 15 | 16 | - Compiles .less files on save 17 | - Uses the [official LESS](https://www.npmjs.com/package/less) node module 18 | - Automatially runs [autoprefix](https://www.npmjs.com/package/less-plugin-autoprefix) 19 | - Support for [CSSComb](https://www.npmjs.com/package/less-plugin-csscomb) 20 | - All compiler options configurable 21 | - Minification support 22 | 23 | ### Compile on save 24 | All .less files will automatically be compiled into a .css file nested under it in Solution Explorer after being enabled on the project. 25 | 26 | By default, compilation is off and that's indicated by a watermark at the bottom right corner of any LESS file. 27 | 28 | ![Watermark Off](art/watermark-off.png) 29 | 30 | To enable LESS compilation, simply click the watermark and it changes to indicate LESS compilation is "on". 31 | 32 | ![Watermark On](art/watermark-on.png) 33 | 34 | For files that are being ignored, the watermark looks like this: 35 | 36 | ![Watermark Ignored](art/watermark-ignored.png) 37 | 38 | All `.less` files in the following folders are ignored from compilation: 39 | 40 | 1. node_modules 41 | 2. bower_components 42 | 3. jspm_packages 43 | 4. lib 44 | 5. vendor 45 | 46 | You can stil reference these files from your own `.less` files, but they will not be compiled themselves. 47 | 48 | Saving the LESS file will then compile into CSS. 49 | 50 | ![Solution Explorer](art/solution-explorer.png) 51 | 52 | The automatic compilation doesn't happen if: 53 | 54 | 1. The project hasn't been enabled for LESS compilation 55 | 2. The .less file name starts with an underscore 56 | 3. The .less file isn't part of any project 57 | 4. The .less file is in a ignored folder 58 | 5. A comment in the .less file with the word `no-compile` is found 59 | 60 | The Output Window shows details about what is being executed to make it easy to troubleshoot any issues. 61 | 62 | **Note** that the the solution (.sln) file is updated to reflect that LESS compilation is enabled. Remember to save the solution when prompted to persits the information. 63 | 64 | ### Compile all .less files 65 | The solution node in Solution Explorer has a command to compile all `.less` files in the enabled projects. 66 | 67 | ![Compile all .less files](art/context-menu-solution.png) 68 | 69 | ### Compiler options 70 | You can set any compiler options as defined on the [LESS compiler website](http://lesscss.org/usage/#command-line-usage) inside a comment in the `.less` file. The comment is prefixed with `lessc` followed by the compiler options. 71 | 72 | ```less 73 | // lessc --strict-math=on 74 | ``` 75 | 76 | The default less compiler arguments are: 77 | 78 | ```bash 79 | lessc --relative-urls --autoprefix="> 0%" "" 80 | ``` 81 | 82 | Here are some examples of the code comments to use in the `.less` files: 83 | 84 | #### Source map 85 | ```less 86 | // lessc --source-map 87 | ``` 88 | 89 | This will produce a `.map` file next to the generated `.css` file. Be aware that if you specify a file name for the source map like `--source-map=file.map`, the file may not be included in the project. Just use the flag without the file name like this `--source-map`. 90 | 91 | #### Output to other directory 92 | ```less 93 | // lessc "../wwwroot/css/file.css" 94 | ``` 95 | 96 | #### Autoprefix 97 | ```less 98 | // lessc --autoprefix="last 2 versions, > 5%" 99 | ``` 100 | 101 | See [Browserlist](https://github.com/ai/browserslist) for description on how to construct the value. 102 | 103 | #### CSSComb 104 | ```less 105 | // lessc --csscomb=zen 106 | ``` 107 | 108 | Available values are `zen`, `yandex` and `csscomb`. Remember to specify `--csscomb` after `--autoprefix` if both are specified. 109 | 110 | #### Minification 111 | By default a `.min.css` file is generated, but that can be turned off by a comment containing `no-minify` in it. You can combine it with the compiler configuration like so: 112 | 113 | ```less 114 | // no-minify lessc --relative-urls" 115 | ``` 116 | 117 | #### Combine it all 118 | ```less 119 | // no-minify lessc --relative-urls --source-map "../wwwroot/css.file.css" 120 | ``` 121 | 122 | This example doesn't minify the output, enables both relative urls and source maps and redirects the output file to a different directory. 123 | 124 | ### Compiler default options 125 | You can specify the compiler options for the solution, the project or for individual folders by placing a file called `less.defaults` in any folder next to or above the .less files. 126 | 127 | The default file cannot contain information about the output file, but all other options can be set. 128 | 129 | **Example:** 130 | 131 | ``` 132 | --source-map --relative-urls --strict-math 133 | ``` 134 | 135 | Note that it isn't prefixed with `//` 136 | 137 | Even though minification isn't technically an option you set on the compiler, you can still opt out of minification like so: 138 | 139 | ``` 140 | no-minify --source-map --relative-urls --strict-math 141 | ``` 142 | 143 | ## Contribute 144 | Check out the [contribution guidelines](.github/CONTRIBUTING.md) 145 | if you want to contribute to this project. 146 | 147 | For cloning and building this project yourself, make sure 148 | to install the 149 | [Extensibility Tools 2015](https://visualstudiogallery.msdn.microsoft.com/ab39a092-1343-46e2-b0f1-6a3f91155aa6) 150 | extension for Visual Studio which enables some features 151 | used by this project. 152 | 153 | ## License 154 | [Apache 2.0](LICENSE) -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2017 2 | 3 | install: 4 | - ps: (new-object Net.WebClient).DownloadString("https://raw.github.com/madskristensen/ExtensionScripts/master/AppVeyor/vsix.ps1") | iex 5 | - npm install npm -g 6 | - set PATH=%APPDATA%\npm;%PATH% 7 | - npm --version 8 | 9 | 10 | before_build: 11 | - ps: Vsix-IncrementVsixVersion | Vsix-UpdateBuildVersion 12 | - ps: Vsix-TokenReplacement src\source.extension.cs 'Version = "([0-9\\.]+)"' 'Version = "{version}"' 13 | 14 | build_script: 15 | - nuget restore -Verbosity quiet 16 | - msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m 17 | 18 | after_test: 19 | - ps: Vsix-PushArtifacts | Vsix-PublishToGallery 20 | -------------------------------------------------------------------------------- /art/context-menu-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/LessCompiler/c0ea8fb39f54ab7d2f00ef76d1dbd048c63cd3e7/art/context-menu-solution.png -------------------------------------------------------------------------------- /art/solution-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/LessCompiler/c0ea8fb39f54ab7d2f00ef76d1dbd048c63cd3e7/art/solution-explorer.png -------------------------------------------------------------------------------- /art/watermark-ignored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/LessCompiler/c0ea8fb39f54ab7d2f00ef76d1dbd048c63cd3e7/art/watermark-ignored.png -------------------------------------------------------------------------------- /art/watermark-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/LessCompiler/c0ea8fb39f54ab7d2f00ef76d1dbd048c63cd3e7/art/watermark-off.png -------------------------------------------------------------------------------- /art/watermark-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/LessCompiler/c0ea8fb39f54ab7d2f00ef76d1dbd048c63cd3e7/art/watermark-on.png -------------------------------------------------------------------------------- /src/Adornments/AdornmentLayer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using Microsoft.VisualStudio.Text.Editor; 3 | using Microsoft.VisualStudio.Utilities; 4 | 5 | namespace LessCompiler 6 | { 7 | class AdornmentLayer 8 | { 9 | public const string LayerName = Vsix.Name; 10 | 11 | [Export(typeof(AdornmentLayerDefinition))] 12 | [Name(LayerName)] 13 | [Order(Before = PredefinedAdornmentLayers.Caret)] 14 | public AdornmentLayerDefinition editorAdornmentLayer = null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Adornments/CssAdornment.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Shell; 2 | using Microsoft.VisualStudio.Text; 3 | using Microsoft.VisualStudio.Text.Editor; 4 | using Microsoft.VisualStudio.Utilities; 5 | using System; 6 | using System.ComponentModel.Composition; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using System.Windows; 11 | using System.Windows.Controls; 12 | using System.Windows.Media; 13 | using System.Windows.Threading; 14 | 15 | namespace LessCompiler 16 | { 17 | [Export(typeof(IWpfTextViewCreationListener))] 18 | [ContentType("CSS")] 19 | [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] 20 | internal sealed class CssAdornmentProvider : IWpfTextViewCreationListener 21 | { 22 | [Import] 23 | private ITextDocumentFactoryService DocumentService { get; set; } 24 | 25 | public void TextViewCreated(IWpfTextView textView) 26 | { 27 | if (!DocumentService.TryGetTextDocument(textView.TextBuffer, out ITextDocument doc)) 28 | return; 29 | 30 | if (!Path.GetExtension(doc.FilePath).Equals(".css", StringComparison.OrdinalIgnoreCase)) 31 | return; 32 | 33 | ThreadHelper.Generic.BeginInvoke(DispatcherPriority.ApplicationIdle, async () => 34 | { 35 | bool isOutput = await ThreadHelper.JoinableTaskFactory.RunAsync(() => IsOutput(doc.FilePath)); 36 | 37 | if (isOutput) 38 | textView.Properties.GetOrCreateSingletonProperty(() => new CssAdornment(textView)); 39 | }); 40 | } 41 | 42 | private async Task IsOutput(string fileName) 43 | { 44 | EnvDTE.Project project = VsHelpers.DTE.Solution.FindProjectItem(fileName)?.ContainingProject; 45 | 46 | if (!project.SupportsCompilation() || !project.IsLessCompilationEnabled() || !await LessCatalog.EnsureCatalog(project)) 47 | return false; 48 | 49 | ProjectMap map = LessCatalog.Catalog[project.UniqueName]; 50 | 51 | return map.LessFiles.Keys.Any(l => 52 | l.OutputFilePath.Equals(fileName, StringComparison.OrdinalIgnoreCase) || 53 | (l.Minify && l.OutputFilePath.Equals(fileName.Replace(".min.css", ".css"), StringComparison.OrdinalIgnoreCase))); 54 | } 55 | } 56 | 57 | class CssAdornment : TextBlock 58 | { 59 | private ITextView _view; 60 | 61 | public CssAdornment(IWpfTextView view) 62 | { 63 | _view = view; 64 | Visibility = Visibility.Hidden; 65 | 66 | IAdornmentLayer adornmentLayer = view.GetAdornmentLayer(AdornmentLayer.LayerName); 67 | 68 | if (adornmentLayer.IsEmpty) 69 | adornmentLayer.AddAdornment(AdornmentPositioningBehavior.ViewportRelative, null, null, this, null); 70 | } 71 | 72 | protected override void OnInitialized(EventArgs e) 73 | { 74 | Text = "Generated"; 75 | FontSize = 70; 76 | FontWeight = FontWeights.SemiBold; 77 | Opacity = 0.4; 78 | ToolTip = "This file was generated by the LESS Compiler extension"; 79 | SetResourceReference(Control.ForegroundProperty, VsBrushes.CaptionTextKey); 80 | SetValue(TextOptions.TextRenderingModeProperty, TextRenderingMode.Aliased); 81 | SetValue(TextOptions.TextFormattingModeProperty, TextFormattingMode.Ideal); 82 | 83 | Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => 84 | { 85 | SetAdornmentLocation(_view, EventArgs.Empty); 86 | 87 | _view.ViewportHeightChanged += SetAdornmentLocation; 88 | _view.ViewportWidthChanged += SetAdornmentLocation; 89 | })); 90 | } 91 | 92 | private void SetAdornmentLocation(object sender, EventArgs e) 93 | { 94 | var view = (IWpfTextView)sender; 95 | Canvas.SetLeft(this, view.ViewportRight - ActualWidth - 20); 96 | Canvas.SetTop(this, view.ViewportBottom - ActualHeight - 20); 97 | Visibility = Visibility.Visible; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Adornments/LessAdornment.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using Microsoft.VisualStudio.Shell; 3 | using Microsoft.VisualStudio.Text.Editor; 4 | using System; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Input; 8 | using System.Windows.Media; 9 | using System.Windows.Threading; 10 | 11 | namespace LessCompiler 12 | { 13 | class LessAdornment : StackPanel 14 | { 15 | private Project _project; 16 | private CompilerOptions _options; 17 | private TextBlock _text; 18 | private ITextView _view; 19 | 20 | public LessAdornment(IWpfTextView view, Project project) 21 | { 22 | _view = view; 23 | _project = project; 24 | 25 | Visibility = Visibility.Hidden; 26 | 27 | view.Closed += ViewClosed; 28 | Settings.Changed += SettingsChanged; 29 | 30 | IAdornmentLayer adornmentLayer = view.GetAdornmentLayer(AdornmentLayer.LayerName); 31 | 32 | if (adornmentLayer.IsEmpty) 33 | adornmentLayer.AddAdornment(AdornmentPositioningBehavior.ViewportRelative, null, null, this, null); 34 | } 35 | 36 | protected override void OnInitialized(EventArgs e) 37 | { 38 | bool enabled = _project.IsLessCompilationEnabled(); 39 | 40 | Opacity = 0.5; 41 | Cursor = Cursors.Hand; 42 | MouseLeftButtonUp += OnClick; 43 | 44 | var header = new TextBlock() 45 | { 46 | FontSize = 20, 47 | Text = "LESS Compiler" 48 | }; 49 | 50 | _text = new TextBlock 51 | { 52 | FontSize = 16 53 | }; 54 | 55 | ThemeControl(header); 56 | ThemeControl(_text); 57 | 58 | SetText(enabled); 59 | 60 | Children.Add(header); 61 | Children.Add(_text); 62 | 63 | Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => 64 | { 65 | SetAdornmentLocation(_view, EventArgs.Empty); 66 | 67 | _view.ViewportHeightChanged += SetAdornmentLocation; 68 | _view.ViewportWidthChanged += SetAdornmentLocation; 69 | })); 70 | } 71 | 72 | private void ThemeControl(TextBlock text) 73 | { 74 | text.SetResourceReference(Control.ForegroundProperty, VsBrushes.CaptionTextKey); 75 | text.SetValue(TextOptions.TextRenderingModeProperty, TextRenderingMode.Aliased); 76 | text.SetValue(TextOptions.TextFormattingModeProperty, TextFormattingMode.Ideal); 77 | } 78 | 79 | public async System.Threading.Tasks.Task Update(CompilerOptions options) 80 | { 81 | _options = options; 82 | 83 | await Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() => 84 | { 85 | bool enabled = _project.IsLessCompilationEnabled(); 86 | SetText(enabled); 87 | 88 | SetAdornmentLocation(_view, EventArgs.Empty); 89 | })); 90 | } 91 | 92 | private void SetText(bool projectEnabled) 93 | { 94 | string projectOnOff = projectEnabled ? "On" : "Off"; 95 | string fileOnOff = _options == null ? "Ignored" : (_options.Compile ? "On" : "Off"); 96 | 97 | if (!projectEnabled && fileOnOff == "On") 98 | fileOnOff = "Off"; 99 | 100 | _text.Text = $" Project: {projectOnOff}\r\n" + 101 | $" File: {fileOnOff}"; 102 | 103 | if (projectEnabled) 104 | ToolTip = $"The LESS Compiler is enabled for project \"{_project.Name}\".\r\nClick to disable it."; 105 | else 106 | ToolTip = $"The LESS Compiler is disabled for project \"{_project.Name}\".\r\nClick to enable it."; 107 | } 108 | 109 | private void SetAdornmentLocation(object sender, EventArgs e) 110 | { 111 | var view = (IWpfTextView)sender; 112 | Canvas.SetLeft(this, view.ViewportRight - ActualWidth - 20); 113 | Canvas.SetTop(this, view.ViewportBottom - ActualHeight - 20); 114 | Visibility = Visibility.Visible; 115 | } 116 | 117 | private void OnClick(object sender, MouseButtonEventArgs e) 118 | { 119 | bool enabled = _project.IsLessCompilationEnabled(); 120 | _project.EnableLessCompilation(!enabled); 121 | 122 | e.Handled = true; 123 | } 124 | 125 | private void SettingsChanged(object sender, SettingsChangedEventArgs e) 126 | { 127 | var project = (Project)sender; 128 | 129 | if (project.UniqueName == _project.UniqueName) 130 | SetText(e.Enabled); 131 | } 132 | 133 | private void ViewClosed(object sender, EventArgs e) 134 | { 135 | var view = (IWpfTextView)sender; 136 | view.Closed -= ViewClosed; 137 | view.ViewportHeightChanged -= SetAdornmentLocation; 138 | view.ViewportWidthChanged -= SetAdornmentLocation; 139 | Settings.Changed -= SettingsChanged; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Commands/ReCompileAll.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using Microsoft.VisualStudio.Shell; 3 | using System; 4 | using System.ComponentModel.Design; 5 | using Tasks = System.Threading.Tasks; 6 | using Microsoft.VisualStudio.Shell.Interop; 7 | using Microsoft.VisualStudio; 8 | using System.Collections.Generic; 9 | 10 | namespace LessCompiler 11 | { 12 | internal sealed class ReCompileAll 13 | { 14 | private readonly Package _package; 15 | 16 | private ReCompileAll(Package package, OleMenuCommandService commandService) 17 | { 18 | _package = package; 19 | 20 | var cmdId = new CommandID(PackageGuids.guidPackageCmdSet, PackageIds.ReCompileAll); 21 | var cmd = new OleMenuCommand(async (s, e) => { await Execute(); }, cmdId); 22 | commandService.AddCommand(cmd); 23 | } 24 | 25 | public static ReCompileAll Instance 26 | { 27 | get; private set; 28 | } 29 | 30 | private IServiceProvider ServiceProvider 31 | { 32 | get { return _package; } 33 | } 34 | 35 | public static void Initialize(Package package, OleMenuCommandService commandService) 36 | { 37 | Instance = new ReCompileAll(package, commandService); 38 | } 39 | 40 | private async Tasks.Task Execute() 41 | { 42 | if (!NodeProcess.IsReadyToExecute()) 43 | return; 44 | 45 | var solution = (IVsSolution)ServiceProvider.GetService(typeof(SVsSolution)); 46 | IEnumerable hierarchies = GetProjectsInSolution(solution, __VSENUMPROJFLAGS.EPF_LOADEDINSOLUTION); 47 | 48 | foreach (IVsHierarchy hierarchy in hierarchies) 49 | { 50 | Project project = GetDTEProject(hierarchy); 51 | 52 | if (project.SupportsCompilation() && project.IsLessCompilationEnabled()) 53 | { 54 | if (await LessCatalog.EnsureCatalog(project)) 55 | await CompilerService.CompileProjectAsync(project); 56 | } 57 | } 58 | } 59 | 60 | // From http://stackoverflow.com/questions/22705089/how-to-get-list-of-projects-in-current-visual-studio-solution 61 | public static IEnumerable GetProjectsInSolution(IVsSolution solution, __VSENUMPROJFLAGS flags) 62 | { 63 | if (solution == null) 64 | yield break; 65 | 66 | Guid guid = Guid.Empty; 67 | solution.GetProjectEnum((uint)flags, ref guid, out IEnumHierarchies enumHierarchies); 68 | if (enumHierarchies == null) 69 | yield break; 70 | 71 | IVsHierarchy[] hierarchy = new IVsHierarchy[1]; 72 | while (enumHierarchies.Next(1, hierarchy, out uint fetched) == VSConstants.S_OK && fetched == 1) 73 | { 74 | if (hierarchy.Length > 0 && hierarchy[0] != null) 75 | yield return hierarchy[0]; 76 | } 77 | } 78 | 79 | public static Project GetDTEProject(IVsHierarchy hierarchy) 80 | { 81 | hierarchy.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_ExtObject, out object obj); 82 | return obj as Project; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Commands/SaveHandler.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using Microsoft.VisualStudio.Editor; 3 | using Microsoft.VisualStudio.Text; 4 | using Microsoft.VisualStudio.Text.Editor; 5 | using Microsoft.VisualStudio.TextManager.Interop; 6 | using Microsoft.VisualStudio.Utilities; 7 | using System.ComponentModel.Composition; 8 | using System.Linq; 9 | using System.Windows.Threading; 10 | 11 | namespace LessCompiler 12 | { 13 | [Export(typeof(IVsTextViewCreationListener))] 14 | [ContentType("LESS")] 15 | [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] 16 | internal sealed class CommandRegistration : IVsTextViewCreationListener 17 | { 18 | private IWpfTextView _view; 19 | private Project _project; 20 | 21 | [Import] 22 | private IVsEditorAdaptersFactoryService AdaptersFactory { get; set; } 23 | 24 | [Import] 25 | private ITextDocumentFactoryService DocumentService { get; set; } 26 | 27 | public void VsTextViewCreated(IVsTextView textViewAdapter) 28 | { 29 | IWpfTextView view = AdaptersFactory.GetWpfTextView(textViewAdapter); 30 | _view = view; 31 | 32 | if (!DocumentService.TryGetTextDocument(view.TextBuffer, out ITextDocument doc)) 33 | return; 34 | 35 | _project = VsHelpers.DTE.Solution.FindProjectItem(doc.FilePath)?.ContainingProject; 36 | 37 | if (!_project.SupportsCompilation()) 38 | return; 39 | 40 | Settings.Changed += OnSettingsChanged; 41 | view.Closed += OnViewClosed; 42 | 43 | Microsoft.VisualStudio.Shell.ThreadHelper.Generic.BeginInvoke(DispatcherPriority.ApplicationIdle, async () => 44 | { 45 | bool isEnabled = _project.IsLessCompilationEnabled(); 46 | 47 | LessAdornment adornment = view.Properties.GetOrCreateSingletonProperty(() => new LessAdornment(view, _project)); 48 | 49 | if (isEnabled && await LessCatalog.EnsureCatalog(_project)) 50 | { 51 | CompilerOptions options = LessCatalog.Catalog[_project.UniqueName].LessFiles.Keys.FirstOrDefault(l => l.InputFilePath == doc.FilePath); 52 | 53 | if (options != null) 54 | { 55 | await adornment.Update(options); 56 | } 57 | } 58 | }); 59 | 60 | doc.FileActionOccurred += DocumentSaved; 61 | } 62 | 63 | private async void OnSettingsChanged(object sender, SettingsChangedEventArgs e) 64 | { 65 | if (!e.Enabled) 66 | return; 67 | 68 | if (_view.Properties.TryGetProperty(typeof(LessAdornment), out LessAdornment adornment)) 69 | { 70 | if (!DocumentService.TryGetTextDocument(_view.TextBuffer, out ITextDocument doc)) 71 | return; 72 | 73 | CompilerOptions options = await CompilerOptions.Parse(doc.FilePath); 74 | 75 | await adornment.Update(options); 76 | } 77 | } 78 | 79 | private async void DocumentSaved(object sender, TextDocumentFileActionEventArgs e) 80 | { 81 | if (e.FileActionType != FileActionTypes.ContentSavedToDisk || !_project.IsLessCompilationEnabled()) 82 | return; 83 | 84 | if (NodeProcess.IsInstalling) 85 | { 86 | VsHelpers.WriteStatus("The LESS compiler is being installed. Please try again in a few seconds..."); 87 | } 88 | else if (NodeProcess.IsReadyToExecute()) 89 | { 90 | CompilerOptions options = await CompilerOptions.Parse(e.FilePath, _view.TextBuffer.CurrentSnapshot.GetText()); 91 | 92 | if (_view.Properties.TryGetProperty(typeof(LessAdornment), out LessAdornment adornment)) 93 | { 94 | await adornment.Update(options); 95 | } 96 | 97 | if (options == null || !_project.SupportsCompilation() || !_project.IsLessCompilationEnabled()) 98 | return; 99 | 100 | await LessCatalog.UpdateFile(_project, options); 101 | 102 | if (await LessCatalog.EnsureCatalog(_project)) 103 | await CompilerService.CompileAsync(options, _project); 104 | } 105 | } 106 | 107 | private void OnViewClosed(object sender, System.EventArgs e) 108 | { 109 | Settings.Changed -= OnSettingsChanged; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Compiler/CompilerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | 6 | namespace LessCompiler 7 | { 8 | public class CompilerOptions 9 | { 10 | private static Regex _regex = new Regex(@"\slessc(?\s.+)(\*/)?", RegexOptions.IgnoreCase | RegexOptions.Multiline); 11 | private static Regex _outFile = new Regex(@"\s((?:""|')(?.+\.css)(?:""|')|(?[^\s=]+\.css))", RegexOptions.Compiled); 12 | 13 | private CompilerOptions(string lessFilePath) 14 | { 15 | InputFilePath = lessFilePath; 16 | OutputFilePath = Path.ChangeExtension(lessFilePath, ".css"); 17 | 18 | string inFile = Path.GetFileName(InputFilePath); 19 | string outFile = Path.GetFileName(OutputFilePath); 20 | string defaults = GetCompilerDefaults(lessFilePath, out bool minify); 21 | 22 | Minify = minify; 23 | Arguments = $"\"{inFile}\" {defaults} \"{outFile}\""; 24 | } 25 | 26 | public string InputFilePath { get; set; } 27 | public string OutputFilePath { get; set; } 28 | public string Arguments { get; set; } 29 | public bool Minify { get; set; } = true; 30 | public bool Compile { get; set; } = true; 31 | public bool SourceMap { get; set; } 32 | 33 | public static async Task Parse(string lessFilePath, string lessContent = null) 34 | { 35 | if (!File.Exists(lessFilePath) || ProjectMap.ShouldIgnore(lessFilePath)) 36 | return null; 37 | 38 | lessContent = lessContent ?? await VsHelpers.ReadFileAsync(lessFilePath); 39 | var options = new CompilerOptions(lessFilePath); 40 | 41 | // Compile 42 | if (Path.GetFileName(lessFilePath).StartsWith("_", StringComparison.Ordinal) 43 | || lessContent.IndexOf("no-compile", StringComparison.OrdinalIgnoreCase) > -1) 44 | options.Compile = false; 45 | 46 | // Minify 47 | if (lessContent.IndexOf("no-minify", StringComparison.OrdinalIgnoreCase) > -1) 48 | options.Minify = false; 49 | 50 | // Arguments 51 | Match argsMatch = _regex.Match(lessContent, 0, Math.Min(500, lessContent.Length)); 52 | if (argsMatch.Success) 53 | { 54 | string inFile = Path.GetFileName(options.InputFilePath); 55 | options.Arguments = $"\"{inFile}\" {argsMatch.Groups["args"].Value.TrimEnd('*', '/').Trim()}"; 56 | } 57 | 58 | // Source map 59 | options.SourceMap = options.Arguments.IndexOf("--source-map", StringComparison.OrdinalIgnoreCase) > -1; 60 | 61 | // OutputFileName 62 | Match outMatch = _outFile.Match(options.Arguments); 63 | if (argsMatch.Success && outMatch.Success) 64 | { 65 | string relative = outMatch.Groups["out"].Value.Replace("/", "\\"); 66 | options.OutputFilePath = Path.Combine(Path.GetDirectoryName(lessFilePath), relative); 67 | } 68 | else 69 | { 70 | options.OutputFilePath = Path.ChangeExtension(lessFilePath, ".css"); 71 | 72 | if (argsMatch.Success) 73 | { 74 | options.Arguments += $" \"{Path.GetFileName(options.OutputFilePath)}\""; 75 | } 76 | } 77 | 78 | // Trim the argument list 79 | options.Arguments = options.Arguments.Trim(); 80 | 81 | return options; 82 | } 83 | 84 | private static string GetCompilerDefaults(string lessFilePath, out bool minify) 85 | { 86 | minify = true; 87 | DirectoryInfo parent = new FileInfo(lessFilePath).Directory; 88 | 89 | while (parent != null) 90 | { 91 | string defaultFile = Path.Combine(parent.FullName, "less.defaults"); 92 | 93 | if (File.Exists(defaultFile)) 94 | { 95 | string content = File.ReadAllText(defaultFile); 96 | 97 | if (content.IndexOf("no-minify") > -1) 98 | minify = false; 99 | 100 | return content.Replace("no-minify", "").Trim(); 101 | } 102 | 103 | parent = parent.Parent; 104 | } 105 | 106 | return "--relative-urls --autoprefix=\"> 1%\""; 107 | } 108 | 109 | public override bool Equals(object obj) 110 | { 111 | if (!(obj is CompilerOptions other)) 112 | return false; 113 | 114 | return Equals(other); 115 | } 116 | 117 | public bool Equals(CompilerOptions other) 118 | { 119 | if (other == null) 120 | return false; 121 | 122 | return InputFilePath.Equals(other.InputFilePath, StringComparison.OrdinalIgnoreCase); 123 | } 124 | 125 | public override int GetHashCode() 126 | { 127 | return InputFilePath.GetHashCode(); 128 | } 129 | 130 | public static bool operator ==(CompilerOptions a, CompilerOptions b) 131 | { 132 | if (ReferenceEquals(a, b)) 133 | return true; 134 | 135 | if (((object)a == null) || ((object)b == null)) 136 | return false; 137 | 138 | return a.Equals(b); 139 | } 140 | 141 | public static bool operator !=(CompilerOptions a, CompilerOptions b) 142 | { 143 | return !(a == b); 144 | } 145 | 146 | public override string ToString() 147 | { 148 | return Path.GetFileName(InputFilePath); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Compiler/CompilerResult.cs: -------------------------------------------------------------------------------- 1 | namespace LessCompiler 2 | { 3 | public class CompilerResult 4 | { 5 | public CompilerResult(string outputFile, string error, string arguments) 6 | { 7 | OutputFile = outputFile; 8 | Error = error; 9 | Arguments = "lessc " + arguments; 10 | } 11 | 12 | public string OutputFile { get; set; } 13 | public string Error { get; } 14 | public string Arguments { get; } 15 | 16 | public bool HasError 17 | { 18 | get { return !string.IsNullOrEmpty(Error); } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Compiler/CompilerService.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using NUglify; 3 | using NUglify.Css; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using Tasks = System.Threading.Tasks; 11 | using EnvDTE80; 12 | 13 | namespace LessCompiler 14 | { 15 | internal static class CompilerService 16 | { 17 | public static async Tasks.Task CompileProjectAsync(Project project) 18 | { 19 | if (project == null || !LessCatalog.Catalog.TryGetValue(project.UniqueName, out ProjectMap map)) 20 | return; 21 | 22 | var compileTasks = new List(); 23 | 24 | foreach (CompilerOptions option in map.LessFiles.Keys) 25 | { 26 | if (option.Compile) 27 | compileTasks.Add(CompileSingleFile(option)); 28 | } 29 | 30 | await Tasks.Task.WhenAll(compileTasks); 31 | 32 | VsHelpers.WriteStatus($"LESS files in solution compiled"); 33 | } 34 | 35 | public static async Tasks.Task CompileAsync(CompilerOptions options, Project project) 36 | { 37 | if (options == null || project == null || !LessCatalog.Catalog.TryGetValue(project.UniqueName, out ProjectMap map)) 38 | return; 39 | 40 | IEnumerable parents = map.LessFiles 41 | .Where(l => l.Value.Exists(c => c == options)) 42 | .Select(l => l.Key) 43 | .Union(new[] { options }) 44 | .Distinct(); 45 | 46 | var sw = new Stopwatch(); 47 | sw.Start(); 48 | 49 | var compilerTaks = new List(); 50 | 51 | foreach (CompilerOptions parentOptions in parents.Where(p => p.Compile)) 52 | { 53 | compilerTaks.Add(CompileSingleFile(parentOptions)); 54 | } 55 | 56 | await Tasks.Task.WhenAll(compilerTaks); 57 | 58 | sw.Stop(); 59 | 60 | VsHelpers.WriteStatus($"LESS file compiled in {Math.Round(sw.Elapsed.TotalSeconds, 2)} seconds"); 61 | } 62 | 63 | private static async Tasks.Task CompileSingleFile(CompilerOptions options) 64 | { 65 | try 66 | { 67 | VsHelpers.CheckFileOutOfSourceControl(options.OutputFilePath); 68 | 69 | if (options.SourceMap) 70 | VsHelpers.CheckFileOutOfSourceControl(options.OutputFilePath + ".map"); 71 | 72 | CompilerResult result = await NodeProcess.ExecuteProcess(options); 73 | 74 | Logger.Log($"{result.Arguments}"); 75 | 76 | if (result.HasError) 77 | { 78 | Logger.Log(result.Error); 79 | VsHelpers.WriteStatus($"Error compiling LESS file. See Output Window for details"); 80 | } 81 | else 82 | { 83 | AddFilesToProject(options); 84 | Minify(options); 85 | } 86 | 87 | return true; 88 | } 89 | catch (Exception ex) 90 | { 91 | Logger.Log(ex); 92 | VsHelpers.WriteStatus($"Error compiling LESS file. See Output Window for details"); 93 | return false; 94 | } 95 | } 96 | 97 | public static bool SupportsCompilation(this Project project) 98 | { 99 | if (project?.Properties == null || project.IsKind(ProjectKinds.vsProjectKindSolutionFolder)) 100 | return false; 101 | 102 | return true; 103 | } 104 | 105 | private static void AddFilesToProject(CompilerOptions options) 106 | { 107 | ProjectItem item = VsHelpers.DTE.Solution.FindProjectItem(options.InputFilePath); 108 | 109 | if (item?.ContainingProject != null) 110 | { 111 | if (options.OutputFilePath == Path.ChangeExtension(options.InputFilePath, ".css")) 112 | { 113 | VsHelpers.AddNestedFile(options.InputFilePath, options.OutputFilePath); 114 | } 115 | else 116 | { 117 | VsHelpers.AddFileToProject(item.ContainingProject, options.OutputFilePath); 118 | } 119 | 120 | string mapFilePath = Path.ChangeExtension(options.OutputFilePath, ".css.map"); 121 | 122 | if (File.Exists(mapFilePath)) 123 | { 124 | VsHelpers.AddNestedFile(options.OutputFilePath, mapFilePath); 125 | } 126 | } 127 | } 128 | 129 | public static void Minify(CompilerOptions options) 130 | { 131 | if (!options.Minify || !File.Exists(options.OutputFilePath)) 132 | return; 133 | 134 | string cssContent = File.ReadAllText(options.OutputFilePath); 135 | 136 | var settings = new CssSettings 137 | { 138 | ColorNames = CssColor.Strict, 139 | CommentMode = CssComment.Important 140 | }; 141 | 142 | UgliflyResult result = Uglify.Css(cssContent, settings); 143 | 144 | if (result.HasErrors) 145 | return; 146 | 147 | string minFilePath = Path.ChangeExtension(options.OutputFilePath, ".min.css"); 148 | VsHelpers.CheckFileOutOfSourceControl(minFilePath); 149 | File.WriteAllText(minFilePath, result.Code, new UTF8Encoding(true)); 150 | VsHelpers.AddNestedFile(options.OutputFilePath, minFilePath); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Compiler/LessCatalog.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace LessCompiler 7 | { 8 | public static class LessCatalog 9 | { 10 | private static SolutionEvents _events; 11 | private static AsyncLock _lock = new AsyncLock(); 12 | 13 | static LessCatalog() 14 | { 15 | Catalog = new Dictionary(); 16 | _events = VsHelpers.DTE.Events.SolutionEvents; 17 | _events.AfterClosing += OnSolutionClosed; 18 | } 19 | 20 | public static Dictionary Catalog 21 | { 22 | get; 23 | } 24 | 25 | public static async Task EnsureCatalog(Project project) 26 | { 27 | if (project == null) 28 | return false; 29 | 30 | if (Catalog.ContainsKey(project.UniqueName)) 31 | return true; 32 | 33 | using (await _lock.LockAsync()) 34 | { 35 | try 36 | { 37 | var map = new ProjectMap(); 38 | await map.BuildMap(project); 39 | 40 | Catalog[project.UniqueName] = map; 41 | } 42 | catch (Exception ex) 43 | { 44 | Logger.Log(ex); 45 | return false; 46 | } 47 | } 48 | 49 | return true; 50 | } 51 | 52 | public static async Task UpdateFile(Project project, CompilerOptions options) 53 | { 54 | if (project == null || options == null || !Catalog.TryGetValue(project.UniqueName, out ProjectMap map)) 55 | return; 56 | 57 | await map.UpdateFile(options); 58 | } 59 | 60 | private static void OnSolutionClosed() 61 | { 62 | foreach (ProjectMap project in Catalog.Values) 63 | { 64 | project.Dispose(); 65 | } 66 | 67 | Catalog.Clear(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Compiler/NodeProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace LessCompiler 8 | { 9 | internal static class NodeProcess 10 | { 11 | public const string Packages = "less@2.7.2 less-plugin-autoprefix less-plugin-csscomb"; 12 | 13 | private static string _installDir = Path.Combine(Path.GetTempPath(), Vsix.Name.Replace(" ", ""), Packages.GetHashCode().ToString()); 14 | private static string _executable = Path.Combine(_installDir, "node_modules\\.bin\\lessc.cmd"); 15 | 16 | public static bool IsInstalling 17 | { 18 | get; 19 | private set; 20 | } 21 | 22 | public static bool IsReadyToExecute() 23 | { 24 | return !IsInstalling && File.Exists(_executable); 25 | } 26 | 27 | public static async Task EnsurePackageInstalled() 28 | { 29 | if (IsInstalling) 30 | return false; 31 | 32 | if (IsReadyToExecute()) 33 | return true; 34 | 35 | IsInstalling = true; 36 | 37 | bool success = await Task.Run(async () => 38 | { 39 | try 40 | { 41 | // Clean up any failed installation attempts 42 | if (Directory.Exists(_installDir)) 43 | Directory.Delete(_installDir, true); 44 | 45 | if (!Directory.Exists(_installDir)) 46 | Directory.CreateDirectory(_installDir); 47 | 48 | string args = $"npm install {Packages} --no-optional"; 49 | Logger.Log(args); 50 | 51 | var start = new ProcessStartInfo("cmd", $"/c {args}") 52 | { 53 | WorkingDirectory = _installDir, 54 | UseShellExecute = false, 55 | RedirectStandardOutput = true, 56 | RedirectStandardError = true, 57 | StandardOutputEncoding = Encoding.UTF8, 58 | StandardErrorEncoding = Encoding.UTF8, 59 | CreateNoWindow = true, 60 | }; 61 | 62 | ModifyPathVariable(start); 63 | 64 | using (var proc = Process.Start(start)) 65 | { 66 | string output = await proc.StandardOutput.ReadToEndAsync(); 67 | string error = await proc.StandardError.ReadToEndAsync(); 68 | 69 | proc.WaitForExit(); 70 | 71 | if (!string.IsNullOrEmpty(output)) 72 | Logger.Log(output); 73 | 74 | if (!string.IsNullOrEmpty(error)) 75 | Logger.Log(error); 76 | 77 | return proc.ExitCode == 0; 78 | } 79 | } 80 | catch (Exception ex) 81 | { 82 | Logger.Log(ex); 83 | return false; 84 | } 85 | finally 86 | { 87 | IsInstalling = false; 88 | } 89 | }); 90 | 91 | return success; 92 | } 93 | 94 | public static async Task ExecuteProcess(CompilerOptions options) 95 | { 96 | if (!await EnsurePackageInstalled()) 97 | return null; 98 | 99 | string fileName = Path.GetFileName(options.InputFilePath); 100 | string arguments = $"--no-color {options.Arguments}"; 101 | 102 | Directory.CreateDirectory(Path.GetDirectoryName(options.OutputFilePath)); 103 | 104 | var start = new ProcessStartInfo("cmd", $"/c \"\"{_executable}\" {arguments}\"") 105 | { 106 | WorkingDirectory = Path.GetDirectoryName(options.InputFilePath), 107 | UseShellExecute = false, 108 | CreateNoWindow = true, 109 | RedirectStandardError = true, 110 | StandardErrorEncoding = Encoding.UTF8, 111 | }; 112 | 113 | ModifyPathVariable(start); 114 | 115 | try 116 | { 117 | using (var proc = Process.Start(start)) 118 | { 119 | string error = await proc.StandardError.ReadToEndAsync(); 120 | 121 | proc.WaitForExit(); 122 | 123 | return new CompilerResult(options.OutputFilePath, error.Trim(), arguments); 124 | } 125 | } 126 | catch (Exception ex) 127 | { 128 | Logger.Log(ex); 129 | return new CompilerResult(options.OutputFilePath, ex.Message, arguments); 130 | } 131 | } 132 | 133 | private static void ModifyPathVariable(ProcessStartInfo start) 134 | { 135 | string path = start.EnvironmentVariables["PATH"]; 136 | 137 | var process = Process.GetCurrentProcess(); 138 | string ideDir = Path.GetDirectoryName(process.MainModule.FileName); 139 | 140 | if (Directory.Exists(ideDir)) 141 | { 142 | string parent = Directory.GetParent(ideDir).Parent.FullName; 143 | 144 | string rc2Preview1Path = new DirectoryInfo(Path.Combine(parent, @"Web\External")).FullName; 145 | 146 | if (Directory.Exists(rc2Preview1Path)) 147 | { 148 | path += ";" + rc2Preview1Path; 149 | path += ";" + rc2Preview1Path + "\\git"; 150 | } 151 | else 152 | { 153 | path += ";" + Path.Combine(ideDir, @"Extensions\Microsoft\Web Tools\External"); 154 | path += ";" + Path.Combine(ideDir, @"Extensions\Microsoft\Web Tools\External\git"); 155 | } 156 | } 157 | 158 | start.EnvironmentVariables["PATH"] = path; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Compiler/ProjectMap.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using EnvDTE80; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text.RegularExpressions; 9 | using System.Threading.Tasks; 10 | using System.Windows.Threading; 11 | using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; 12 | 13 | namespace LessCompiler 14 | { 15 | public class ProjectMap : IDisposable 16 | { 17 | private static string[] _ignore = { "\\node_modules\\", "\\bower_components\\", "\\jspm_packages\\", "\\lib\\", "\\vendor\\" }; 18 | private static Regex _import = new Regex(@"@import(?:[^""']+)?(([""'])(?[^""']+)\2|url\(([""']?)(?[^""')]+)\3\))", RegexOptions.IgnoreCase | RegexOptions.Multiline); 19 | private ProjectItemsEvents _events; 20 | 21 | public ProjectMap() 22 | { 23 | _events = ((Events2)VsHelpers.DTE.Events).ProjectItemsEvents; 24 | _events.ItemAdded += OnProjectItemAdded; 25 | _events.ItemRemoved += OnProjectItemRemoved; 26 | _events.ItemRenamed += OnProjectItemRenamed; 27 | } 28 | 29 | public Dictionary> LessFiles { get; } = new Dictionary>(); 30 | 31 | public async Task BuildMap(Project project) 32 | { 33 | if (!project.SupportsCompilation() || !project.IsLessCompilationEnabled()) 34 | return; 35 | 36 | var sw = new Stopwatch(); 37 | sw.Start(); 38 | 39 | IEnumerable lessFiles = FindLessFiles(project.ProjectItems); 40 | 41 | foreach (string file in lessFiles) 42 | { 43 | await AddFile(file); 44 | } 45 | 46 | sw.Stop(); 47 | Logger.Log($"LESS file catalog for {project.Name} built in {Math.Round(sw.Elapsed.TotalSeconds, 2)} seconds"); 48 | } 49 | 50 | public async Task UpdateFile(CompilerOptions options) 51 | { 52 | CompilerOptions existing = LessFiles.Keys.FirstOrDefault(c => c == options); 53 | 54 | if (existing != null) 55 | LessFiles.Remove(existing); 56 | 57 | if (!ShouldIgnore(options.InputFilePath)) 58 | await AddFile(options.InputFilePath); 59 | } 60 | 61 | public void RemoveFile(CompilerOptions options) 62 | { 63 | if (options == null) 64 | return; 65 | 66 | if (LessFiles.ContainsKey(options)) 67 | LessFiles.Remove(options); 68 | 69 | foreach (CompilerOptions file in LessFiles.Keys) 70 | { 71 | if (LessFiles[file].Contains(options)) 72 | LessFiles[file].Remove(options); 73 | } 74 | } 75 | 76 | private async Task AddFile(string lessFilePath) 77 | { 78 | if (LessFiles.Keys.Any(c => c.InputFilePath == lessFilePath)) 79 | return; 80 | 81 | if (!File.Exists(lessFilePath)) 82 | return; 83 | 84 | string lessContent = File.ReadAllText(lessFilePath); 85 | 86 | CompilerOptions options = await CompilerOptions.Parse(lessFilePath, lessContent); 87 | LessFiles.Add(options, new List()); 88 | 89 | await AddOption(options, lessContent); 90 | } 91 | 92 | private async Task AddOption(CompilerOptions options, string lessContent = null) 93 | { 94 | lessContent = lessContent ?? File.ReadAllText(options.InputFilePath); 95 | string lessDir = Path.GetDirectoryName(options.InputFilePath); 96 | 97 | foreach (Match match in _import.Matches(lessContent)) 98 | { 99 | string childFilePath = new FileInfo(Path.Combine(lessDir, match.Groups["url"].Value)).FullName; 100 | 101 | if (!File.Exists(childFilePath)) 102 | continue; 103 | 104 | CompilerOptions import = LessFiles.Keys.FirstOrDefault(c => c.InputFilePath == childFilePath); 105 | 106 | if (import == null) 107 | { 108 | import = await CompilerOptions.Parse(childFilePath); 109 | 110 | if (!ShouldIgnore(childFilePath)) 111 | await AddFile(childFilePath); 112 | } 113 | 114 | LessFiles[options].Add(import); 115 | } 116 | } 117 | 118 | private static IEnumerable FindLessFiles(ProjectItems items, List files = null) 119 | { 120 | if (files == null) 121 | files = new List(); 122 | 123 | foreach (ProjectItem item in items) 124 | { 125 | if (item.IsSupportedFile(out string filePath) && File.Exists(filePath)) 126 | { 127 | files.Add(filePath); 128 | } 129 | 130 | if (!ShouldIgnore(filePath) && item.ProjectItems != null) 131 | FindLessFiles(item.ProjectItems, files); 132 | } 133 | 134 | return files; 135 | } 136 | 137 | public static bool ShouldIgnore(string filePath) 138 | { 139 | return _ignore.Any(ign => filePath.IndexOf(ign, StringComparison.OrdinalIgnoreCase) > -1); 140 | } 141 | 142 | private void OnProjectItemRenamed(ProjectItem item, string OldName) 143 | { 144 | if (!item.IsSupportedFile(out string filePath)) 145 | return; 146 | 147 | ThreadHelper.Generic.BeginInvoke(DispatcherPriority.ApplicationIdle, () => 148 | { 149 | LessFiles.Clear(); 150 | 151 | Task.Run(async () => 152 | { 153 | await BuildMap(item.ContainingProject); 154 | }); 155 | }); 156 | } 157 | 158 | private void OnProjectItemRemoved(ProjectItem item) 159 | { 160 | if (!item.IsSupportedFile(out string filePath)) 161 | return; 162 | 163 | ThreadHelper.Generic.BeginInvoke(DispatcherPriority.ApplicationIdle, () => 164 | { 165 | CompilerOptions existing = LessFiles.Keys.FirstOrDefault(c => c.InputFilePath == filePath); 166 | 167 | RemoveFile(existing); 168 | }); 169 | } 170 | 171 | private void OnProjectItemAdded(ProjectItem item) 172 | { 173 | if (!item.IsSupportedFile(out string filePath)) 174 | return; 175 | 176 | ThreadHelper.Generic.BeginInvoke(DispatcherPriority.ApplicationIdle, async () => 177 | { 178 | await AddFile(filePath); 179 | }); 180 | } 181 | 182 | public void Dispose() 183 | { 184 | if (_events != null) 185 | { 186 | _events.ItemAdded -= OnProjectItemAdded; 187 | _events.ItemRemoved -= OnProjectItemRemoved; 188 | _events.ItemRenamed -= OnProjectItemRenamed; 189 | } 190 | 191 | LessFiles.Clear(); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Helpers/AsyncLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | // http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266983.aspx 7 | public class AsyncSemaphore 8 | { 9 | private readonly static Task s_completed = Task.FromResult(true); 10 | private readonly Queue> m_waiters = new Queue>(); 11 | private int m_currentCount; 12 | 13 | public AsyncSemaphore(int initialCount) 14 | { 15 | if (initialCount < 0) throw new ArgumentOutOfRangeException("initialCount"); 16 | m_currentCount = initialCount; 17 | } 18 | 19 | public Task WaitAsync() 20 | { 21 | lock (m_waiters) 22 | { 23 | if (m_currentCount > 0) 24 | { 25 | --m_currentCount; 26 | return s_completed; 27 | } 28 | else 29 | { 30 | var waiter = new TaskCompletionSource(); 31 | m_waiters.Enqueue(waiter); 32 | return waiter.Task; 33 | } 34 | } 35 | } 36 | 37 | public void Release() 38 | { 39 | TaskCompletionSource toRelease = null; 40 | lock (m_waiters) 41 | { 42 | if (m_waiters.Count > 0) 43 | toRelease = m_waiters.Dequeue(); 44 | else 45 | ++m_currentCount; 46 | } 47 | if (toRelease != null) 48 | toRelease.SetResult(true); 49 | } 50 | } 51 | 52 | // http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx 53 | public class AsyncLock 54 | { 55 | private readonly AsyncSemaphore m_semaphore; 56 | private readonly Task m_releaser; 57 | 58 | public AsyncLock() 59 | { 60 | m_semaphore = new AsyncSemaphore(1); 61 | m_releaser = Task.FromResult(new Releaser(this)); 62 | } 63 | 64 | public Task LockAsync() 65 | { 66 | Task wait = m_semaphore.WaitAsync(); 67 | return wait.IsCompleted ? 68 | m_releaser : 69 | wait.ContinueWith((_, state) => new Releaser((AsyncLock)state), 70 | this, CancellationToken.None, 71 | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); 72 | } 73 | 74 | public struct Releaser : IDisposable 75 | { 76 | private readonly AsyncLock m_toRelease; 77 | 78 | internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; } 79 | 80 | public void Dispose() 81 | { 82 | if (m_toRelease != null) 83 | m_toRelease.m_semaphore.Release(); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Helpers/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.Shell; 3 | using Microsoft.VisualStudio.Shell.Interop; 4 | 5 | internal static class Logger 6 | { 7 | private static IVsOutputWindowPane _pane; 8 | private static IVsOutputWindow _output = (IVsOutputWindow)ServiceProvider.GlobalProvider.GetService(typeof(SVsOutputWindow)); 9 | 10 | public static void Log(object message) 11 | { 12 | try 13 | { 14 | if (EnsurePane()) 15 | { 16 | _pane.OutputString(DateTime.Now.ToString() + ": " + message + Environment.NewLine); 17 | } 18 | } 19 | catch (Exception ex) 20 | { 21 | System.Diagnostics.Debug.Write(ex); 22 | } 23 | } 24 | 25 | private static bool EnsurePane() 26 | { 27 | if (_pane == null) 28 | { 29 | var guid = Guid.NewGuid(); 30 | _output.CreatePane(ref guid, LessCompiler.Vsix.Name, 1, 1); 31 | _output.GetPane(ref guid, out _pane); 32 | } 33 | 34 | return _pane != null; 35 | } 36 | } -------------------------------------------------------------------------------- /src/Helpers/VsHelpers.cs: -------------------------------------------------------------------------------- 1 | using EnvDTE; 2 | using EnvDTE80; 3 | using Microsoft.VisualStudio.Shell; 4 | using Microsoft.VisualStudio.Shell.Interop; 5 | using System; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | 9 | namespace LessCompiler 10 | { 11 | public static class VsHelpers 12 | { 13 | public static DTE2 DTE = ServiceProvider.GlobalProvider.GetService(typeof(DTE)) as DTE2; 14 | private static IVsStatusbar statusbar = (IVsStatusbar)ServiceProvider.GlobalProvider.GetService(typeof(SVsStatusbar)); 15 | 16 | public static void WriteStatus(string text) 17 | { 18 | statusbar.FreezeOutput(0); 19 | statusbar.SetText(text); 20 | statusbar.FreezeOutput(1); 21 | } 22 | 23 | public static void CheckFileOutOfSourceControl(string file) 24 | { 25 | if (!File.Exists(file) || DTE.Solution.FindProjectItem(file) == null) 26 | return; 27 | 28 | if (DTE.SourceControl.IsItemUnderSCC(file) && !DTE.SourceControl.IsItemCheckedOut(file)) 29 | DTE.SourceControl.CheckOutItem(file); 30 | 31 | var info = new FileInfo(file) 32 | { 33 | IsReadOnly = false 34 | }; 35 | } 36 | 37 | public static void AddFileToProject(this Project project, string file, string itemType = null) 38 | { 39 | if (project.IsKind(ProjectTypes.ASPNET_5, ProjectTypes.DOTNET_Core, ProjectTypes.SSDT)) 40 | return; 41 | 42 | try 43 | { 44 | if (DTE.Solution.FindProjectItem(file) == null) 45 | { 46 | ProjectItem item = project.ProjectItems.AddFromFile(file); 47 | 48 | if (string.IsNullOrEmpty(itemType) 49 | || project.IsKind(ProjectTypes.WEBSITE_PROJECT) 50 | || project.IsKind(ProjectTypes.UNIVERSAL_APP)) 51 | return; 52 | 53 | item.Properties.Item("ItemType").Value = "None"; 54 | } 55 | } 56 | catch (Exception ex) 57 | { 58 | Logger.Log(ex); 59 | } 60 | } 61 | 62 | public static void AddNestedFile(string parentFile, string newFile, bool force = false) 63 | { 64 | ProjectItem item = DTE.Solution.FindProjectItem(parentFile); 65 | 66 | try 67 | { 68 | if (item == null 69 | || item.ContainingProject == null 70 | || item.ContainingProject.IsKind(ProjectTypes.ASPNET_5)) 71 | return; 72 | 73 | if (item.ProjectItems == null || item.ContainingProject.IsKind(ProjectTypes.UNIVERSAL_APP)) 74 | { 75 | item.ContainingProject.AddFileToProject(newFile); 76 | } 77 | else if (DTE.Solution.FindProjectItem(newFile) == null || force) 78 | { 79 | item.ProjectItems.AddFromFile(newFile); 80 | } 81 | } 82 | catch (Exception ex) 83 | { 84 | Logger.Log(ex); 85 | } 86 | } 87 | 88 | public static bool IsKind(this Project project, params string[] kindGuids) 89 | { 90 | foreach (string guid in kindGuids) 91 | { 92 | if (project.Kind.Equals(guid, StringComparison.OrdinalIgnoreCase)) 93 | return true; 94 | } 95 | 96 | return false; 97 | } 98 | 99 | public static string GetRootFolder(this Project project) 100 | { 101 | if (project == null) 102 | return null; 103 | 104 | if (project.IsKind(ProjectKinds.vsProjectKindSolutionFolder)) 105 | return Path.GetDirectoryName(DTE.Solution.FullName); 106 | 107 | if (string.IsNullOrEmpty(project.FullName)) 108 | return null; 109 | 110 | string fullPath; 111 | 112 | try 113 | { 114 | fullPath = project.Properties.Item("FullPath").Value as string; 115 | } 116 | catch (ArgumentException) 117 | { 118 | try 119 | { 120 | // MFC projects don't have FullPath, and there seems to be no way to query existence 121 | fullPath = project.Properties.Item("ProjectDirectory").Value as string; 122 | } 123 | catch (ArgumentException) 124 | { 125 | // Installer projects have a ProjectPath. 126 | fullPath = project.Properties.Item("ProjectPath").Value as string; 127 | } 128 | } 129 | 130 | if (string.IsNullOrEmpty(fullPath)) 131 | return File.Exists(project.FullName) ? Path.GetDirectoryName(project.FullName) : null; 132 | 133 | if (Directory.Exists(fullPath)) 134 | return fullPath; 135 | 136 | if (File.Exists(fullPath)) 137 | return Path.GetDirectoryName(fullPath); 138 | 139 | return null; 140 | } 141 | 142 | public static string FilePath(this ProjectItem item) 143 | { 144 | return item.FileNames[1]; 145 | } 146 | 147 | public static bool IsSupportedFile(this ProjectItem item, out string FilePath) 148 | { 149 | FilePath = item.FilePath(); 150 | 151 | if (item.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFolder) 152 | return false; 153 | 154 | string ext = Path.GetExtension(item.FilePath()); 155 | return ext.Equals(".less", StringComparison.OrdinalIgnoreCase); 156 | } 157 | 158 | public static async Task ReadFileAsync(string filePath) 159 | { 160 | using (var stream = new StreamReader(filePath)) 161 | { 162 | return await stream.ReadToEndAsync(); 163 | } 164 | 165 | } 166 | } 167 | 168 | public static class ProjectTypes 169 | { 170 | public const string ASPNET_5 = "{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}"; 171 | public const string DOTNET_Core = "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"; 172 | public const string WEBSITE_PROJECT = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"; 173 | public const string UNIVERSAL_APP = "{262852C6-CD72-467D-83FE-5EEB1973A190}"; 174 | public const string NODE_JS = "{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}"; 175 | public const string SSDT = "{00d1a9c2-b5f0-4af3-8072-f6c62b433612}"; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/LessCompiler.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(VisualStudioVersion) 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | true 7 | v3 8 | Program 9 | $(DevEnvDir)\devenv.exe 10 | /rootsuffix Exp 11 | 12 | 13 | 14 | 15 | 16 | Debug 17 | AnyCPU 18 | 2.0 19 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 20 | {D1F752E5-6CAB-4513-980A-51B44E104CDF} 21 | Library 22 | Properties 23 | LessCompiler 24 | LessCompiler 25 | v4.5.2 26 | true 27 | true 28 | true 29 | true 30 | true 31 | false 32 | 33 | 34 | true 35 | full 36 | false 37 | bin\Debug\ 38 | DEBUG;TRACE 39 | prompt 40 | 4 41 | 42 | 43 | pdbonly 44 | true 45 | bin\Release\ 46 | TRACE 47 | prompt 48 | 4 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | True 71 | True 72 | source.extension.vsixmanifest 73 | 74 | 75 | True 76 | True 77 | VSCommandTable.vsct 78 | 79 | 80 | 81 | 82 | Resources\LICENSE 83 | true 84 | 85 | 86 | true 87 | 88 | 89 | Menus.ctmenu 90 | VsctGenerator 91 | VSCommandTable.cs 92 | 93 | 94 | 95 | Designer 96 | VsixManifestGenerator 97 | source.extension.cs 98 | 99 | 100 | 101 | 102 | False 103 | False 104 | 105 | 106 | False 107 | False 108 | 109 | 110 | False 111 | False 112 | 113 | 114 | False 115 | False 116 | 117 | 118 | 119 | 120 | False 121 | False 122 | 123 | 124 | ..\packages\Microsoft.VisualStudio.CoreUtility.14.3.25407\lib\net45\Microsoft.VisualStudio.CoreUtility.dll 125 | False 126 | 127 | 128 | ..\packages\Microsoft.VisualStudio.Editor.14.3.25407\lib\net45\Microsoft.VisualStudio.Editor.dll 129 | False 130 | 131 | 132 | ..\packages\Microsoft.VisualStudio.Imaging.14.3.25407\lib\net45\Microsoft.VisualStudio.Imaging.dll 133 | False 134 | 135 | 136 | ..\packages\Microsoft.VisualStudio.OLE.Interop.7.10.6070\lib\Microsoft.VisualStudio.OLE.Interop.dll 137 | False 138 | 139 | 140 | ..\packages\Microsoft.VisualStudio.Shell.14.0.14.3.25407\lib\Microsoft.VisualStudio.Shell.14.0.dll 141 | False 142 | 143 | 144 | ..\packages\Microsoft.VisualStudio.Shell.Immutable.10.0.10.0.30319\lib\net40\Microsoft.VisualStudio.Shell.Immutable.10.0.dll 145 | False 146 | 147 | 148 | ..\packages\Microsoft.VisualStudio.Shell.Immutable.11.0.11.0.50727\lib\net45\Microsoft.VisualStudio.Shell.Immutable.11.0.dll 149 | False 150 | 151 | 152 | ..\packages\Microsoft.VisualStudio.Shell.Immutable.12.0.12.0.21003\lib\net45\Microsoft.VisualStudio.Shell.Immutable.12.0.dll 153 | False 154 | 155 | 156 | ..\packages\Microsoft.VisualStudio.Shell.Immutable.14.0.14.3.25407\lib\net45\Microsoft.VisualStudio.Shell.Immutable.14.0.dll 157 | False 158 | 159 | 160 | ..\packages\Microsoft.VisualStudio.Shell.Interop.7.10.6071\lib\Microsoft.VisualStudio.Shell.Interop.dll 161 | False 162 | 163 | 164 | True 165 | ..\packages\Microsoft.VisualStudio.Shell.Interop.10.0.10.0.30319\lib\Microsoft.VisualStudio.Shell.Interop.10.0.dll 166 | True 167 | 168 | 169 | True 170 | ..\packages\Microsoft.VisualStudio.Shell.Interop.11.0.11.0.61030\lib\Microsoft.VisualStudio.Shell.Interop.11.0.dll 171 | True 172 | 173 | 174 | True 175 | ..\packages\Microsoft.VisualStudio.Shell.Interop.12.0.12.0.30110\lib\Microsoft.VisualStudio.Shell.Interop.12.0.dll 176 | True 177 | 178 | 179 | ..\packages\Microsoft.VisualStudio.Shell.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.Shell.Interop.8.0.dll 180 | False 181 | 182 | 183 | ..\packages\Microsoft.VisualStudio.Shell.Interop.9.0.9.0.30729\lib\Microsoft.VisualStudio.Shell.Interop.9.0.dll 184 | False 185 | 186 | 187 | ..\packages\Microsoft.VisualStudio.Text.Data.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Data.dll 188 | False 189 | 190 | 191 | ..\packages\Microsoft.VisualStudio.Text.Logic.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.Logic.dll 192 | False 193 | 194 | 195 | ..\packages\Microsoft.VisualStudio.Text.UI.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.dll 196 | False 197 | 198 | 199 | ..\packages\Microsoft.VisualStudio.Text.UI.Wpf.14.3.25407\lib\net45\Microsoft.VisualStudio.Text.UI.Wpf.dll 200 | False 201 | 202 | 203 | ..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll 204 | False 205 | 206 | 207 | ..\packages\Microsoft.VisualStudio.TextManager.Interop.8.0.8.0.50727\lib\Microsoft.VisualStudio.TextManager.Interop.8.0.dll 208 | False 209 | 210 | 211 | ..\packages\Microsoft.VisualStudio.Threading.14.1.111\lib\net45\Microsoft.VisualStudio.Threading.dll 212 | False 213 | 214 | 215 | ..\packages\Microsoft.VisualStudio.Utilities.14.3.25407\lib\net45\Microsoft.VisualStudio.Utilities.dll 216 | False 217 | 218 | 219 | ..\packages\Microsoft.VisualStudio.Validation.14.1.111\lib\net45\Microsoft.VisualStudio.Validation.dll 220 | False 221 | 222 | 223 | ..\packages\NUglify.1.5.5\lib\net40\NUglify.dll 224 | 225 | 226 | 227 | 228 | False 229 | False 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | true 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 251 | 252 | 253 | 254 | 255 | 262 | -------------------------------------------------------------------------------- /src/LessCompilerPackage.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Shell; 2 | using Microsoft.VisualStudio.Shell.Interop; 3 | using System; 4 | using System.ComponentModel.Design; 5 | using System.Runtime.InteropServices; 6 | using System.Threading; 7 | using Tasks = System.Threading.Tasks; 8 | 9 | namespace LessCompiler 10 | { 11 | [Guid(PackageGuids.guidPackageString)] 12 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 13 | [InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)] 14 | [ProvideMenuResource("Menus.ctmenu", 1)] 15 | public sealed class LessCompilerPackage : AsyncPackage 16 | { 17 | protected override async Tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 18 | { 19 | if (await GetServiceAsync(typeof(IMenuCommandService)) is OleMenuCommandService commandService) 20 | { 21 | ReCompileAll.Initialize(this, commandService); 22 | } 23 | } 24 | } 25 | 26 | [Guid(PackageGuids.guidInstallerPackageString)] 27 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 28 | [ProvideAutoLoad(UIContextGuids80.NoSolution, PackageAutoLoadFlags.BackgroundLoad)] 29 | public sealed class NpmInstallerPackage : AsyncPackage 30 | { 31 | protected override async Tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 32 | { 33 | if (!NodeProcess.IsReadyToExecute()) 34 | { 35 | await NodeProcess.EnsurePackageInstalled(); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using LessCompiler; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: AssemblyTitle(Vsix.Name)] 7 | [assembly: AssemblyDescription(Vsix.Description)] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany(Vsix.Author)] 10 | [assembly: AssemblyProduct(Vsix.Name)] 11 | [assembly: AssemblyCopyright(Vsix.Author)] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | [assembly: ComVisible(false)] 16 | [assembly: InternalsVisibleTo("LessCompilerTest")] 17 | 18 | [assembly: AssemblyVersion(Vsix.Version)] 19 | [assembly: AssemblyFileVersion(Vsix.Version)] 20 | -------------------------------------------------------------------------------- /src/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/LessCompiler/c0ea8fb39f54ab7d2f00ef76d1dbd048c63cd3e7/src/Resources/Icon.png -------------------------------------------------------------------------------- /src/Settings/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using EnvDTE; 3 | using EnvDTE80; 4 | using Microsoft.VisualStudio; 5 | using Microsoft.VisualStudio.Shell; 6 | using Microsoft.VisualStudio.Shell.Interop; 7 | 8 | namespace LessCompiler 9 | { 10 | public static class Settings 11 | { 12 | private const string SettingKey = "LessCompiler"; 13 | private static DTE2 _dte = VsHelpers.DTE; 14 | private static IVsSolution2 _solution = (IVsSolution2)ServiceProvider.GlobalProvider.GetService(typeof(SVsSolution)); 15 | 16 | public static void EnableLessCompilation(this Project project, bool isEnabled) 17 | { 18 | if (_dte.Solution == null) 19 | return; 20 | 21 | string guid = project.UniqueGuid(); 22 | 23 | if (string.IsNullOrEmpty(guid)) 24 | return; 25 | 26 | string value = guid; 27 | 28 | if (_dte.Solution.Globals.VariableExists[SettingKey]) 29 | { 30 | value = _dte.Solution.Globals[SettingKey].ToString(); 31 | 32 | if (isEnabled) 33 | { 34 | if (!value.Contains(guid)) 35 | value += "," + guid; 36 | } 37 | else 38 | { 39 | if (value.Contains(guid)) 40 | value = value.Replace(guid, "").Replace(",,", ","); 41 | } 42 | } 43 | 44 | _dte.Solution.Globals[SettingKey] = value.Trim(',', ' '); 45 | _dte.Solution.Globals.VariablePersists[SettingKey] = !string.IsNullOrEmpty(value); 46 | 47 | Changed?.Invoke(project, new SettingsChangedEventArgs(isEnabled)); 48 | } 49 | 50 | public static bool IsLessCompilationEnabled(this Project project) 51 | { 52 | if (project == null || _dte.Solution == null) 53 | return false; 54 | 55 | bool isSet = _dte.Solution.Globals.VariableExists[SettingKey]; 56 | string guid = project.UniqueGuid(); 57 | 58 | if (string.IsNullOrEmpty(guid)) 59 | return false; 60 | 61 | if (isSet && _dte.Solution.Globals[SettingKey].ToString().Contains(guid)) 62 | { 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | 69 | private static string UniqueGuid(this Project project) 70 | { 71 | if (project == null) 72 | return null; 73 | 74 | if (_solution.GetProjectOfUniqueName(project.UniqueName, out var hierarchy) == VSConstants.S_OK) 75 | if (_solution.GetGuidOfProject(hierarchy, out Guid projectGuid) == VSConstants.S_OK) 76 | return projectGuid.ToString(); 77 | 78 | return null; 79 | } 80 | 81 | public static event EventHandler Changed; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Settings/SettingsChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LessCompiler 4 | { 5 | public class SettingsChangedEventArgs : EventArgs 6 | { 7 | public SettingsChangedEventArgs(bool enabled) 8 | { 9 | Enabled = enabled; 10 | } 11 | 12 | public bool Enabled { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/VSCommandTable.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by Extensibility Tools v1.10.188 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace LessCompiler 7 | { 8 | using System; 9 | 10 | /// 11 | /// Helper class that exposes all GUIDs used across VS Package. 12 | /// 13 | internal sealed partial class PackageGuids 14 | { 15 | public const string guidPackageString = "5ac1b994-61c2-4d5c-ab54-3c64f095a843"; 16 | public const string guidInstallerPackageString = "a4aab915-6143-4ac5-b899-1bd64f6fd4c3"; 17 | public const string guidPackageCmdSetString = "e26be7ce-2c0c-4810-b1fe-27f364e2811a"; 18 | public static Guid guidPackage = new Guid(guidPackageString); 19 | public static Guid guidInstallerPackage = new Guid(guidInstallerPackageString); 20 | public static Guid guidPackageCmdSet = new Guid(guidPackageCmdSetString); 21 | } 22 | /// 23 | /// Helper class that encapsulates all CommandIDs uses across VS Package. 24 | /// 25 | internal sealed partial class PackageIds 26 | { 27 | public const int ReCompileAll = 0x1000; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/VSCommandTable.vsct: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/source.extension.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace LessCompiler 7 | { 8 | internal sealed partial class Vsix 9 | { 10 | public const string Id = "d32c5250-fa82-4da6-9732-5518fabebfef"; 11 | public const string Name = "LESS Compiler"; 12 | public const string Description = @"An alternative LESS compiler with no setup. Uses the official node.js based LESS compiler under the hood with AutoPrefixer and CSSComb built in."; 13 | public const string Language = "en-US"; 14 | public const string Version = "0.9"; 15 | public const string Author = "Mads Kristensen"; 16 | public const string Tags = "LESS, CSS, CSSComb, AutoPrefixer, Compiler"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | LESS Compiler 6 | An alternative LESS compiler with no setup. Uses the official node.js based LESS compiler under the hood with AutoPrefixer and CSSComb built in. 7 | https://github.com/madskristensen/LessCompiler 8 | Resources\LICENSE 9 | https://github.com/madskristensen/LessCompiler/blob/master/CHANGELOG.md 10 | Resources\Icon.png 11 | Resources\Icon.png 12 | LESS, CSS, CSSComb, AutoPrefixer, Compiler 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/website.pkgdef: -------------------------------------------------------------------------------- 1 | ; This adds support for file nesting in Website Projects 2 | 3 | ; LESS 4 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.less] 5 | "RelationType"=dword:00000001 6 | 7 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.less\.css] 8 | 9 | ; CSS 10 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.css] 11 | "RelationType"=dword:00000001 12 | 13 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.css\.min.css] 14 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.css\.css.map] -------------------------------------------------------------------------------- /test/LessCompiler.Test/CompilerOptionsTest.cs: -------------------------------------------------------------------------------- 1 | using LessCompiler; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace LessCompilerTest 7 | { 8 | [TestClass] 9 | public class CompilerOptionsTest 10 | { 11 | private string _lessFilePath; 12 | 13 | [TestInitialize] 14 | public void Setup() 15 | { 16 | _lessFilePath = new FileInfo("..\\..\\artifacts\\autoprefix.less").FullName; 17 | } 18 | 19 | [TestMethod] 20 | public async Task OverrideDefaults() 21 | { 22 | CompilerOptions args = await CompilerOptions.Parse(_lessFilePath, "// lessc --compress --csscomb=yandex --autoprefix=\">1%\""); 23 | 24 | Assert.AreEqual("\"autoprefix.less\" --compress --csscomb=yandex --autoprefix=\">1%\" \"autoprefix.css\"", args.Arguments); 25 | } 26 | 27 | [TestMethod] 28 | public async Task ShorthandSyntax() 29 | { 30 | CompilerOptions options = await CompilerOptions.Parse(_lessFilePath, "/* lessc -x out/hat.css */"); 31 | 32 | Assert.AreEqual("\"autoprefix.less\" -x out/hat.css", options.Arguments); 33 | Assert.AreEqual(options.OutputFilePath, Path.Combine(Path.GetDirectoryName(_lessFilePath), "out\\hat.css")); 34 | } 35 | 36 | [TestMethod] 37 | public async Task NoCompileNoMinify() 38 | { 39 | CompilerOptions options = await CompilerOptions.Parse(_lessFilePath, "/* no-compile no-minify */"); 40 | 41 | Assert.AreEqual("\"autoprefix.less\" --relative-urls --autoprefix=\"> 1%\" \"autoprefix.css\"", options.Arguments); 42 | Assert.AreEqual(options.OutputFilePath, Path.ChangeExtension(_lessFilePath, ".css")); 43 | Assert.IsFalse(options.Compile); 44 | Assert.IsFalse(options.Minify); 45 | } 46 | 47 | [TestMethod] 48 | public async Task NoCompileNoMinifyPlusCustom() 49 | { 50 | CompilerOptions options = await CompilerOptions.Parse(_lessFilePath, "/* no-compile no-minify lessc -ru */"); 51 | 52 | Assert.AreEqual("\"autoprefix.less\" -ru \"autoprefix.css\"", options.Arguments); 53 | Assert.AreEqual(options.OutputFilePath, Path.ChangeExtension(_lessFilePath, ".css")); 54 | Assert.IsFalse(options.Compile); 55 | Assert.IsFalse(options.Minify); 56 | } 57 | 58 | [TestMethod] 59 | public async Task OnlyOutFile() 60 | { 61 | CompilerOptions options = await CompilerOptions.Parse(_lessFilePath, "/* lessc out.css */"); 62 | 63 | Assert.AreEqual(options.OutputFilePath, Path.Combine(Path.GetDirectoryName(_lessFilePath), "out.css")); 64 | Assert.IsTrue(options.Compile); 65 | Assert.IsTrue(options.Minify); 66 | } 67 | 68 | [TestMethod] 69 | public async Task OutFileWithSpaces() 70 | { 71 | CompilerOptions options = await CompilerOptions.Parse(_lessFilePath, "/* lessc --source-map=foo.css.map \"out file.css\" */"); 72 | 73 | Assert.AreEqual(options.OutputFilePath, Path.Combine(Path.GetDirectoryName(_lessFilePath), "out file.css")); 74 | Assert.IsTrue(options.Compile); 75 | Assert.IsTrue(options.Minify); 76 | } 77 | 78 | [TestMethod] 79 | public async Task SourceMapOnly() 80 | { 81 | CompilerOptions options = await CompilerOptions.Parse(_lessFilePath, "/* lessc --source-map=foo.css.map */"); 82 | 83 | Assert.AreEqual(options.OutputFilePath, Path.ChangeExtension(_lessFilePath, ".css")); 84 | Assert.IsTrue(options.Compile); 85 | Assert.IsTrue(options.Minify); 86 | } 87 | 88 | [TestMethod] 89 | public async Task CustomDefaults() 90 | { 91 | string lessFile = new FileInfo("..\\..\\artifacts\\defaults\\customdefaults.less").FullName; 92 | CompilerOptions options = await CompilerOptions.Parse(lessFile); 93 | 94 | Assert.AreEqual("\"customdefaults.less\" --source-map \"customdefaults.css\"", options.Arguments); 95 | Assert.AreEqual(options.OutputFilePath, Path.ChangeExtension(lessFile, ".css")); 96 | Assert.IsTrue(options.Compile); 97 | Assert.IsFalse(options.Minify); 98 | } 99 | 100 | [TestMethod] 101 | public async Task Underscore() 102 | { 103 | string lessFile = new FileInfo("..\\..\\artifacts\\_underscore.less").FullName; 104 | CompilerOptions options = await CompilerOptions.Parse(lessFile); 105 | 106 | Assert.IsFalse(options.Compile); 107 | 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test/LessCompiler.Test/LessCompiler.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {44F8565C-6AED-433C-A2BE-221D7A03EFF8} 8 | Library 9 | Properties 10 | LessCompilerTest 11 | LessCompilerTest 12 | v4.5.2 13 | 512 14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 15.0 16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 18 | False 19 | UnitTest 20 | 21 | 22 | 23 | 24 | true 25 | full 26 | false 27 | bin\Debug\ 28 | DEBUG;TRACE 29 | prompt 30 | 4 31 | 32 | 33 | pdbonly 34 | true 35 | bin\Release\ 36 | TRACE 37 | prompt 38 | 4 39 | 40 | 41 | 42 | ..\..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll 43 | 44 | 45 | ..\..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {d1f752e5-6cab-4513-980a-51b44e104cdf} 67 | LessCompiler 68 | 69 | 70 | 71 | 72 | 73 | 74 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /test/LessCompiler.Test/NodeProcessTest.cs: -------------------------------------------------------------------------------- 1 | using LessCompiler; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace LessCompilerTest 7 | { 8 | [TestClass] 9 | public class NodeProcessTest 10 | { 11 | [TestCleanup] 12 | public void Cleanup() 13 | { 14 | var dir = new DirectoryInfo("..\\..\\artifacts\\"); 15 | 16 | foreach (FileInfo cssFile in dir.GetFiles("*.css*", SearchOption.AllDirectories)) 17 | { 18 | cssFile.Delete(); 19 | } 20 | } 21 | 22 | [TestMethod] 23 | public async Task AutoPrefix() 24 | { 25 | CompilerResult result = await Execute("autoprefix.less"); 26 | 27 | Assert.IsFalse(result.HasError); 28 | Assert.AreEqual("body {\n transition: ease;\n}\n", File.ReadAllText(result.OutputFile)); 29 | } 30 | 31 | [TestMethod] 32 | public async Task UndefinedVariable() 33 | { 34 | CompilerResult result = await Execute("undefined-variable.less"); 35 | 36 | Assert.IsTrue(result.HasError); 37 | Assert.IsFalse(File.Exists(result.OutputFile)); 38 | } 39 | 40 | [TestMethod, Ignore("Fails on AppVeyor")] 41 | public async Task SourceMap() 42 | { 43 | CompilerResult result = await Execute("sourcemap.less"); 44 | 45 | string mapFile = Path.ChangeExtension(result.OutputFile, ".css.map"); 46 | Assert.IsFalse(result.HasError); 47 | Assert.IsTrue(File.Exists(result.OutputFile)); 48 | Assert.IsTrue(File.Exists(mapFile)); 49 | } 50 | 51 | private static async Task Execute(string fileName) 52 | { 53 | var less = new FileInfo("..\\..\\artifacts\\" + fileName); 54 | CompilerOptions options = await CompilerOptions.Parse(less.FullName); 55 | return await NodeProcess.ExecuteProcess(options); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/LessCompiler.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("LessCompiler.Test")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("LessCompiler.Test")] 10 | [assembly: AssemblyCopyright("Copyright © 2017")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("44f8565c-6aed-433c-a2be-221d7a03eff8")] 17 | 18 | // [assembly: AssemblyVersion("1.0.*")] 19 | [assembly: AssemblyVersion("1.0.0.0")] 20 | [assembly: AssemblyFileVersion("1.0.0.0")] 21 | -------------------------------------------------------------------------------- /test/LessCompiler.Test/artifacts/_underscore.less: -------------------------------------------------------------------------------- 1 | @type: ease; 2 | 3 | body { 4 | transition: @type; 5 | } -------------------------------------------------------------------------------- /test/LessCompiler.Test/artifacts/autoprefix.less: -------------------------------------------------------------------------------- 1 | @type: ease; 2 | 3 | body { 4 | transition: @type; 5 | } -------------------------------------------------------------------------------- /test/LessCompiler.Test/artifacts/defaults/customdefaults.less: -------------------------------------------------------------------------------- 1 |  2 | body { 3 | display: block; 4 | } -------------------------------------------------------------------------------- /test/LessCompiler.Test/artifacts/defaults/less.defaults: -------------------------------------------------------------------------------- 1 | no-minify --source-map -------------------------------------------------------------------------------- /test/LessCompiler.Test/artifacts/sourcemap.less: -------------------------------------------------------------------------------- 1 | // lessc --source-map 2 | 3 | body { 4 | transition: all; 5 | } -------------------------------------------------------------------------------- /test/LessCompiler.Test/artifacts/undefined-variable.less: -------------------------------------------------------------------------------- 1 | body { 2 | transition: @type; 3 | } -------------------------------------------------------------------------------- /test/LessCompiler.Test/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | --------------------------------------------------------------------------------