├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── icon.png ├── package.json ├── src ├── Configuration.ts ├── FileOptionsParser.ts ├── StatusBarMessage.ts ├── StatusBarMessageTypes.ts ├── compiles │ ├── less │ │ ├── CompileLessCommand.ts │ │ └── LessCompiler.ts │ ├── sass │ │ ├── CompileSassCommand.ts │ │ └── SassCompiler.ts │ └── typescript │ │ ├── CompileTsCommand.ts │ │ └── TsCompiler.ts ├── extension.ts ├── minify │ ├── css │ │ └── MinifyCssCommand.ts │ └── js │ │ └── MinifyJsCommand.ts ├── plugins │ ├── pluginCleanCss.ts │ ├── pluginGroup.ts │ └── pluginSass2Less.ts └── test │ ├── runTest.ts │ └── suite │ ├── extension.test.ts │ └── index.ts ├── tests ├── .vscode │ └── settings.json ├── css │ └── test.css ├── js │ └── test.js ├── less │ ├── helpers │ │ ├── helpers.less │ │ ├── mixins.less │ │ ├── sub │ │ │ └── variables.less │ │ └── test.scss │ ├── parent.less │ └── test.less ├── scss │ ├── helpers │ │ ├── _helpers.scss │ │ ├── _mixins.scss │ │ └── sub │ │ │ ├── _variables.scss │ │ │ └── test2.scss │ ├── parent.scss │ └── test.scss └── typescript │ └── test.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | package-lock.json 6 | **/.DS_Store 7 | tests/output/** -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "${workspaceFolder}/tests/", 12 | "--disable-extensions", 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], 16 | "preLaunchTask": "${defaultBuildTask}" 17 | }, 18 | { 19 | "name": "Extension Tests", 20 | "type": "extensionHost", 21 | "request": "launch", 22 | "runtimeExecutable": "${execPath}", 23 | "args": [ 24 | "${workspaceFolder}/tests/", 25 | "--disable-extensions", 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/src/test/suite/index" 28 | ], 29 | "outFiles": [ "${workspaceFolder}/out/src/test/**/*.js" ], 30 | "preLaunchTask": "${defaultBuildTask}" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "easycompile.compile": { 12 | "typescript": false, 13 | "less": false, 14 | "sass": false 15 | } 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/src/test/** 4 | src/** 5 | tests/** 6 | node_modules/** 7 | .gitignore 8 | **/tsconfig.json 9 | **/*.map 10 | package-lock.json 11 | webpack.config.js 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ### 1.2.4 3 | Fix issue [#42] 4 | ### 1.2.2 5 | Add inline lib option support for Typescript compile 6 | 7 | ### 1.2.1 8 | Fix multi main error for scss compile ([#30]) 9 | 10 | ### 1.2.0 11 | Fix folder settings have no effect 12 | Optimize diagnostic message 13 | 14 | ### 1.1.9 15 | Change minify js "surround" default value ([#27]) 16 | Fix typescript error message 17 | 18 | ### 1.1.8 19 | Fix sourceMap option ([#25]) 20 | Implement Minify Css/JS settings ([#20] [#26]) 21 | Optimize code 22 | 23 | ### 1.1.6 24 | Fix new version sass/scss import ([#24]) 25 | Add channel message output 26 | Set setting scope to resource 27 | 28 | ### 1.1.5 29 | Fix crash bug 30 | Improve compile error message 31 | 32 | ### 1.1.4 33 | Upgrade dependencies 34 | Speed up extension 35 | Fix sass/scss inline source map ([#19]) 36 | 37 | ### 1.1.2 38 | Upgrade Less complier to 3.8.1 ([#15]) 39 | 40 | ### 1.1.1 41 | Add minify on save option ([#1]) 42 | Integrate sass2less plugin. ([#12]) 43 | Fix sass/scss compile bug ([#9]) 44 | 45 | ### 1.1.0 46 | Fix compiler empty file throw error 47 | Fix sass/scss can not importing files from parent folder 48 | 49 | ### 1.0.9 50 | Fix sass/scss import files ([#2]) 51 | 52 | ### 1.0.8 53 | Fix sass import and sourceMap 54 | 55 | ### 1.0.7 56 | Clean cache after sass/scss compile 57 | 58 | ### 1.0.6 59 | Add sass/scss compile support 60 | 61 | ### 1.0.5 62 | suport '*' in main option 63 | 64 | ### 1.0.4 65 | Add compress option for TSCompiler 66 | 67 | ### 1.0.3 68 | Include "Workspace Root/node_modules/@types" with typescript compiler 69 | 70 | ### 1.0.2 71 | 72 | Add surround option to minify js. 73 | * Example: 74 | ```json 75 | "easycompile.js": { 76 | "surround": "(function (define){ ${code} })(define)" 77 | } 78 | ``` 79 | 80 | ### 1.0.1 81 | 82 | Add minify functions for JS and CSS 83 | 84 | ### 1.0.0 85 | 86 | Initial release 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 refgd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easy-compile README 2 | 3 | Easily work with LESS/SASS/SCSS/TYPESCRIPT files in Visual Studio Code. 4 | 5 | "Compile-on-save" for LESS/SASS/SCSS/TypeScript files without using a build task. 6 | 7 | ## Features 8 | 9 | * Compile TypeScript and Less/Sass/Scss on save 10 | * Support autoprefixer for Less/Sass/Scss 11 | * Support mearge all media queries 12 | * Support inline setting (Only for Complie) 13 | * minify .js and .css files 14 | 15 | ## Usage 16 | 17 | ### Complie 18 | For TypeScript, Only compile after you setup _outfile_ or _outdir_. 19 | 20 | ### Minify 21 | Run Command "Minify - Easy Complie" to minify files 22 | 23 | ## Extension Settings 24 | 25 | ### Settting 26 | easycompile.sass {} 27 | 28 | easycompile.less {} 29 | 30 | easycompile.typescript {} 31 | 32 | easycompile.css { 33 | 34 |  `"outDir": { string }` 35 | * Redirect output to a different folder 36 | * support ${workspaceRoot} 37 | 38 |  `"outExt": { string }` 39 | * allows you to specify an alternative output file extension 40 | * e.g. `.min.css` instead of `.css` 41 | 42 |  `"autoprefixer": { string }` 43 | * this enables the [autoprefixer plugin](https://github.com/postcss/autoprefixer) (included) 44 | * e,g. `> 5%; last 2 Chrome versions; not ie 6-9` 45 | 46 |  `"groupmedia": { boolean }` 47 | * This enables the [group media queries plugin](https://github.com/Se7enSky/group-css-media-queries) (included) 48 | 49 |  `"sourceMap": { boolean }` 50 | 51 |  `"sourceMapFileInline": { boolean }` 52 | 53 | } 54 | 55 | easycompile.js { 56 | 57 |  `"outDir": { string }` 58 | * Redirect output to a different folder 59 | * support ${workspaceRoot} 60 | 61 |  `"outExt": { string }` 62 | * allows you to specify an alternative output file extension 63 | * e.g. `.min.js` instead of `.js` 64 | 65 |  `"surround": { string }` 66 | * put string surround the code 67 | * e.g. `(function (){ ${code} })()` 68 | 69 |  `"compress": { object }` 70 | * implement UglifyJS Compress setting [[compress-options](https://github.com/mishoo/UglifyJS2#compress-options)] 71 | 72 | } 73 | 74 | ### Inline Setting (Only work for Less/Sass/Scss/Typescript) 75 | * Settings can also be specified per file as a comment on the _first_ line. 76 | * Settings are comma-separated and strings are _not_ "quoted". 77 | * Example: 78 | 79 | ```less 80 | // out: ../dist/app.css, compress: true, sourceMap: false, autoprefixer: last 5 versions, groupmedia: true 81 | 82 | body, html { 83 | ... 84 | } 85 | ``` 86 | 87 | ```typescript 88 | // outdir: ../../ 89 | 90 | import * ... 91 | ... 92 | ``` 93 | 94 | ### Settings[Less/Scss/Sass] 95 | `main: { filepath: string | string[] }` 96 | * Compiles a different less file _instead_ of this one. 97 | * All other settings are ignored. 98 | * Filepath is relative to the current file. 99 | * Multiple main files can be specified (see [FAQ](#faq)). 100 | 101 | `out: { boolean | filepath: string | folderpath: string }` 102 | * Redirects the css output to a different file. 103 | * This setting can be used to override a project-wide `"out": false` setting, where you only want certain `.less` files to be generated. 104 | * If filepath is used, but no file extension is specified, it will append `.css` 105 | * If folderpath is used, the less filename will be used, but with the `.css` extension 106 | * Filepath is relative to the current file. 107 | 108 | `outExt: { string }` 109 | * The default output extension is `.css`. 110 | * This allows you to specify an alternative output file extension (e.g. `.wxss` instead of `.css`) 111 | * This applies to the `.map` file also (e.g. `.wxss.map`) 112 | 113 | `compress: { boolean }` 114 | * Compresses the css output by removing surplus white-space. 115 | 116 | `autoprefixer: { string | string[] }` 117 | * When present, this enables the [autoprefixer plugin](https://github.com/postcss/autoprefixer) (included). 118 | * This plugin automatically adds/removes vendor-prefixes needed to support a set of browsers which you specify. 119 | * The `autoprefixer` option _is_ the comma-separated list of `browsers` for autoprefixer to use (or alternatively a string array of them). 120 | * See [browserslist](https://github.com/ai/browserslist#queries) documentation for further examples of browser queries. 121 | * **NOTE**: If used with the inline setting, the browsers listed _must_ be unquoted and semi-colon separated (because comma is already the directive separator): e.g.
122 | `// autoprefixer: > 5%; last 2 Chrome versions; not ie 6-9, sourceMap: true, out: ../css/style.css` 123 | 124 | `groupmedia: { boolean }` 125 | * This enables the [group media queries plugin](https://github.com/Se7enSky/group-css-media-queries) (included). 126 | 127 | 128 | ### Tips 129 | * Ignore files 130 | ```json 131 | "easycompile.compile": { 132 | "ignore" : [ 133 | "**/_*.scss" 134 | ] 135 | } 136 | ``` 137 | * Enable minify on save 138 | ```json 139 | "easycompile.compile": { 140 | "minifyJsOnSave": true, 141 | "minifyCssOnSave": true 142 | } 143 | ``` 144 | 145 | 146 | ----------------------------------------------------------------------------------------------------------- 147 | 148 | ## Acknowledgements 149 | * Configuration concepts borrowed from [mrcrowl's](#https://github.com/mrcrowl) [vscode-easy-less](https://github.com/mrcrowl/vscode-easy-less). 150 | 151 | **Enjoy!** -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/refgd/easy-complie/2e570732314f2ded042d7ee268a8702003e99c01/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-compile", 3 | "displayName": "Easy Compile", 4 | "description": "Easy to compile TypeScript/Less/Sass/Scss, Minify JS/CSS", 5 | "version": "1.2.6-beta", 6 | "publisher": "refgd", 7 | "engines": { 8 | "vscode": "^1.34.0" 9 | }, 10 | "license": "MIT", 11 | "categories": [ 12 | "Other" 13 | ], 14 | "icon": "icon.png", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/refgd/easy-complie.git" 18 | }, 19 | "activationEvents": [ 20 | "onLanguage:less", 21 | "onLanguage:typescript", 22 | "onLanguage:scss", 23 | "onLanguage:sass", 24 | "onLanguage:css", 25 | "onLanguage:javascript", 26 | "onCommand:easyCompile.compile", 27 | "onCommand:easyCompile.minifydir", 28 | "onCommand:easyCompile.minify" 29 | ], 30 | "main": "./out/src/extension", 31 | "contributes": { 32 | "commands": [ 33 | { 34 | "command": "easyCompile.compile", 35 | "title": "Compile - Easy Compile" 36 | }, 37 | { 38 | "command": "easyCompile.minify", 39 | "title": "Minify - Easy Compile" 40 | }, 41 | { 42 | "command": "easyCompile.minifydir", 43 | "title": "Minify Directory - Easy Compile" 44 | } 45 | ], 46 | "configuration": { 47 | "type": "object", 48 | "title": "Easy Compile configuration", 49 | "scope": "resource", 50 | "properties": { 51 | "easycompile.compile": { 52 | "type": "object", 53 | "description": "Configuration options for easycompile.", 54 | "scope": "resource", 55 | "properties": { 56 | "less": { 57 | "type": "boolean", 58 | "description": "Compile Less?", 59 | "default": true 60 | }, 61 | "sass": { 62 | "type": "boolean", 63 | "description": "Compile Sass/Scss?", 64 | "default": true 65 | }, 66 | "typescript": { 67 | "type": "boolean", 68 | "description": "Compile Typescript?", 69 | "default": true 70 | }, 71 | "minifyJsOnSave": { 72 | "type": "boolean", 73 | "description": "Minify JS file on save?", 74 | "default": false 75 | }, 76 | "minifyCssOnSave": { 77 | "type": "boolean", 78 | "description": "Minify CSS file on save?", 79 | "default": false 80 | }, 81 | "ignore": { 82 | "type": "array", 83 | "description": "Minify CSS file on save?", 84 | "default": [] 85 | } 86 | } 87 | }, 88 | "easycompile.js": { 89 | "type": "object", 90 | "description": "Configuration options for minify Javascript.", 91 | "scope": "resource", 92 | "properties": { 93 | "surround": { 94 | "type": "string", 95 | "description": "", 96 | "default": "" 97 | } 98 | } 99 | }, 100 | "easycompile.css": { 101 | "type": "object", 102 | "description": "Configuration options for Css.", 103 | "scope": "resource", 104 | "properties": {} 105 | }, 106 | "easycompile.typescript": { 107 | "type": "object", 108 | "description": "Configuration options for TypeScript.", 109 | "scope": "resource", 110 | "properties": { 111 | "surround": { 112 | "type": "string", 113 | "description": "", 114 | "default": "(function (define){ ${code} })(define)" 115 | } 116 | } 117 | }, 118 | "easycompile.sass": { 119 | "type": "object", 120 | "description": "Configuration options for SASS/SCSS.", 121 | "scope": "resource", 122 | "properties": { 123 | "compress": { 124 | "type": "boolean", 125 | "description": "Compress .css files? (removes unnecessary white-space)", 126 | "default": false 127 | }, 128 | "sourceMap": { 129 | "type": "boolean", 130 | "description": "Should .map files be generated?", 131 | "default": false 132 | }, 133 | "sourceMapFileInline": { 134 | "type": "boolean", 135 | "description": "Should source maps be inlined within the .css file? (requires sourceMap: true)", 136 | "default": false 137 | }, 138 | "out": { 139 | "type": [ 140 | "boolean", 141 | "string", 142 | "null" 143 | ], 144 | "description": "Default 'out' setting. Set to false to default to no output.", 145 | "default": null 146 | }, 147 | "main": { 148 | "type": [ 149 | "string", 150 | "array" 151 | ], 152 | "description": "Compile specific .sass/.scss file(s) when any .sass/.scss file is saved.", 153 | "default": "main.scss" 154 | }, 155 | "autoprefixer": { 156 | "type": [ 157 | "string", 158 | "array", 159 | "null" 160 | ], 161 | "description": "The 'browsers' argument for autoprefixer plugin (see https://github.com/ai/browserslist#queries)", 162 | "default": "last 5 versions" 163 | }, 164 | "groupmedia": { 165 | "type": "boolean", 166 | "description": "Mearge all media queries", 167 | "default": true 168 | } 169 | } 170 | }, 171 | "easycompile.less": { 172 | "type": "object", 173 | "description": "Configuration options for LESS.", 174 | "scope": "resource", 175 | "properties": { 176 | "compress": { 177 | "type": "boolean", 178 | "description": "Compress .css files? (removes unnecessary white-space)", 179 | "default": false 180 | }, 181 | "ieCompat": { 182 | "type": "boolean", 183 | "description": "IE8 compatiblity mode? (restricts size of data-uri to 32KB)", 184 | "default": true 185 | }, 186 | "sourceMap": { 187 | "type": "boolean", 188 | "description": "Should .map files be generated?", 189 | "default": false 190 | }, 191 | "sourceMapFileInline": { 192 | "type": "boolean", 193 | "description": "Should source maps be inlined within the .css file? (requires sourceMap: true)", 194 | "default": false 195 | }, 196 | "out": { 197 | "type": [ 198 | "boolean", 199 | "string", 200 | "null" 201 | ], 202 | "description": "Default 'out' setting. Set to false to default to no output.", 203 | "default": null 204 | }, 205 | "outExt": { 206 | "type": "string", 207 | "description": "The file extension to use for generated .css files", 208 | "default": ".css" 209 | }, 210 | "main": { 211 | "type": [ 212 | "string", 213 | "array" 214 | ], 215 | "description": "Compile specific .less file(s) when any .less file is saved.", 216 | "default": "main.less" 217 | }, 218 | "relativeUrls": { 219 | "type": "boolean", 220 | "description": "Rewrite URLs from imported files, relative to the importing file (default: false)", 221 | "default": false 222 | }, 223 | "autoprefixer": { 224 | "type": [ 225 | "string", 226 | "array", 227 | "null" 228 | ], 229 | "description": "The 'browsers' argument for autoprefixer plugin (see https://github.com/ai/browserslist#queries)", 230 | "default": "last 5 versions" 231 | }, 232 | "groupmedia": { 233 | "type": "boolean", 234 | "description": "Mearge all media queries", 235 | "default": true 236 | }, 237 | "sass2less": { 238 | "type": "boolean", 239 | "description": "Convert SASS files to LESS", 240 | "default": true 241 | } 242 | } 243 | } 244 | } 245 | } 246 | }, 247 | "scripts": { 248 | "vscode:prepublish": "npm run compile", 249 | "webpack": "SET NODE_OPTIONS=--openssl-legacy-provider && webpack --mode production", 250 | "webpack-dev": "webpack --mode development --openssl-legacy-provider", 251 | "compile": "tsc -p ./", 252 | "watch": "tsc -watch -p ./", 253 | "pretest": "npm run compile", 254 | "test": "node ./out/src/test/runTest.js" 255 | }, 256 | "devDependencies": { 257 | "@types/mocha": "^5.2.7", 258 | "@types/node": "^12.12.22", 259 | "@types/vscode": "^1.34.0", 260 | "fs-plus": "^3.1.1", 261 | "mocha": "^6.2.2", 262 | "ts-loader": "^6.2.1", 263 | "vscode-test": "^1.3.0", 264 | "webpack": "^4.41.4", 265 | "webpack-cli": "^3.3.10", 266 | "webpack-node-externals": "^1.7.2" 267 | }, 268 | "dependencies": { 269 | "clean-css": "^4.2.1", 270 | "group-css-media-queries": "^1.4.1", 271 | "ignore": "^5.0.2", 272 | "impor": "^0.1.1", 273 | "less": "^4.1.0", 274 | "less-plugin-autoprefix": "^2.0.0", 275 | "less-plugin-functions": "^1.0.0", 276 | "mkpath": "^1.0.0", 277 | "sass.js": "^0.11.1", 278 | "typescript": "^4.9.3", 279 | "uglify-js": "^3.7.2" 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/Configuration.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | 5 | 6 | 7 | export function getGlobalOptions(filename: string, key: string = 'compile', projectDefault: object = {}):any { 8 | let filenamePath: path.ParsedPath = path.parse(filename); 9 | let defaultOptions = { 10 | plugins: [], 11 | rootFileInfo: getRootFileInfo(filenamePath), 12 | relativeUrls: false 13 | }; 14 | 15 | let configuredOptions = vscode.workspace.getConfiguration("easycompile", vscode.Uri.parse(filename)).get(key); 16 | return Object.assign({}, projectDefault, defaultOptions, configuredOptions); 17 | } 18 | 19 | export function getRootFileInfo(parsedPath: path.ParsedPath) { 20 | parsedPath.ext = ".less"; 21 | parsedPath.base = parsedPath.name + ".less"; 22 | 23 | return { 24 | filename: parsedPath.base, 25 | currentDirectory: parsedPath.dir, 26 | relativeUrls: false, 27 | entryPath: parsedPath.dir + "/", 28 | rootpath: null, 29 | rootFilename: null 30 | } 31 | } 32 | 33 | export function getNodeMPath() { 34 | return path.resolve(__dirname+'/../../node_modules/'); 35 | } 36 | 37 | export function formatPath(path: string){ 38 | //fix path on windows 39 | return path.replace(/^\/([a-zA-Z]+:\/)/g, "$1"); 40 | } 41 | 42 | export function intepolatePath(this: void, path: string): string 43 | { 44 | if(vscode.workspace.workspaceFolders){ 45 | let rootPath = vscode.workspace.workspaceFolders[0]; 46 | path = formatPath((path).replace(/\$\{workspaceRoot\}/g, rootPath.uri.path)); 47 | } 48 | return path; 49 | } 50 | 51 | export function resolveFilePath(this: void, main: string, tsPath: string, currentTsFile: string): string 52 | { 53 | const interpolatedFilePath: string = intepolatePath(main); 54 | const resolvedFilePath: string = path.resolve(tsPath, interpolatedFilePath); 55 | if (resolvedFilePath.indexOf(currentTsFile) >= 0) 56 | { 57 | return ''; // avoid infinite loops 58 | } 59 | return resolvedFilePath; 60 | } 61 | 62 | 63 | 64 | export function resolveMainFilePaths(this: void, main: string | string[], lessPath: string, currentLessFile: string): string[] 65 | { 66 | let mainFiles: string[]; 67 | if (typeof main === "string") 68 | { 69 | mainFiles = [main]; 70 | } 71 | else if (Array.isArray(main)) 72 | { 73 | mainFiles = main; 74 | } 75 | else 76 | { 77 | mainFiles = []; 78 | } 79 | 80 | const interpolatedMainFilePaths: string[] = mainFiles.map(mainFile => intepolatePath(mainFile)); 81 | const resolvedMainFilePaths: string[] = interpolatedMainFilePaths.map(mainFile => path.resolve(lessPath, mainFile)); 82 | if (resolvedMainFilePaths.indexOf(currentLessFile) >= 0) 83 | { 84 | return []; // avoid infinite loops 85 | } 86 | 87 | return resolvedMainFilePaths; 88 | } -------------------------------------------------------------------------------- /src/FileOptionsParser.ts: -------------------------------------------------------------------------------- 1 | const ARRAY_OPTS = { 2 | "main": true, 3 | "lib": true, 4 | }; 5 | 6 | export function parse(line: string, defaults) 7 | { 8 | // does line start with a comment?: // 9 | let commentMatch: RegExpExecArray | null = /^\s*\/\/\s*(.+)/.exec(line); 10 | if (!commentMatch) 11 | { 12 | return defaults; 13 | } 14 | 15 | let options = Object.assign({}, defaults); 16 | let optionLine: string = commentMatch[1]; 17 | let seenKeys: Object = {}; 18 | for (let item of optionLine.split(',')) // string[] 19 | { 20 | let i: number = item.indexOf(':'); 21 | if (i < 0) 22 | { 23 | continue; 24 | } 25 | let key: string = item.substr(0, i).trim(); 26 | 27 | let value: string = item.substr(i + 1).trim(); 28 | if (value.match(/^(""|''|true|false|undefined|null|[0-9]+)$/)) 29 | { 30 | value = eval(value); 31 | } 32 | 33 | if (seenKeys[key] === true && ARRAY_OPTS[key]) 34 | { 35 | let existingValue: any = options[key]; 36 | if (!Array.isArray(existingValue)) 37 | { 38 | existingValue = options[key] = [existingValue]; 39 | } 40 | existingValue.push(value); 41 | } 42 | else 43 | { 44 | options[key] = value; 45 | seenKeys[key] = true; 46 | } 47 | } 48 | 49 | return options; 50 | } -------------------------------------------------------------------------------- /src/StatusBarMessage.ts: -------------------------------------------------------------------------------- 1 | import {StatusBarMessageTypes} from "./StatusBarMessageTypes"; 2 | import * as vscode from 'vscode'; 3 | 4 | const ERROR_COLOR_CSS = "rgba(255,125,0,1)"; 5 | const ERROR_DURATION_MS = 10000; 6 | const SUCCESS_DURATION_MS = 1500; 7 | 8 | const channel:vscode.OutputChannel = vscode.window.createOutputChannel("Easy Compile"); 9 | // channel.show(); 10 | 11 | let errorMessage: vscode.StatusBarItem | null; 12 | 13 | export function hideError() 14 | { 15 | if (errorMessage) 16 | { 17 | errorMessage.hide(); 18 | errorMessage = null; 19 | } 20 | } 21 | 22 | export function show(message: string, type: StatusBarMessageTypes) 23 | { 24 | this.hideError(); 25 | 26 | channel.appendLine(message); 27 | 28 | switch (type) 29 | { 30 | case StatusBarMessageTypes.SUCCESS: 31 | return vscode.window.setStatusBarMessage(message, SUCCESS_DURATION_MS); 32 | 33 | case StatusBarMessageTypes.INDEFINITE: 34 | return vscode.window.setStatusBarMessage(message); 35 | 36 | case StatusBarMessageTypes.ERROR: 37 | errorMessage = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0); 38 | errorMessage.text = message; 39 | errorMessage.command = "workbench.action.showErrorsWarnings"; 40 | errorMessage.color = ERROR_COLOR_CSS; 41 | errorMessage.show(); 42 | setTimeout(hideError, ERROR_DURATION_MS); 43 | 44 | return errorMessage; 45 | } 46 | } 47 | 48 | export function output(message: string) 49 | { 50 | channel.appendLine(message); 51 | } 52 | 53 | export function getDiagnostic(error):vscode.Diagnostic 54 | { 55 | let message: string = 'Unknow error'; 56 | let range: vscode.Range = new vscode.Range(0, 0, 0, 0); 57 | 58 | if (error.code) 59 | { 60 | switch (error.code) 61 | { 62 | case 'EACCES': 63 | case 'ENOENT': 64 | message = `Cannot open file '${error.path}'`; 65 | break; 66 | default: 67 | if(error.message) message = error.message; 68 | } 69 | 70 | channel.appendLine(message); 71 | } 72 | else if (error.line !== undefined && error.column !== undefined) 73 | { 74 | // typescript errors, try to highlight the affected range 75 | let lineIndex: number = error.line - 1; 76 | if(lineIndex<0) lineIndex = 0; 77 | range = new vscode.Range(lineIndex, error.column, lineIndex, 0); 78 | message = error.message; 79 | }else{ 80 | console.log(error); 81 | } 82 | 83 | let diagnosis = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error); 84 | 85 | return diagnosis; 86 | } 87 | 88 | export function formatDiagnostic(diagnostic, file, alld) 89 | { 90 | file = file.replace(/^([a-zA-Z]+:\/)/g, "/$1"); 91 | diagnostic.set(vscode.Uri.parse(file), alld); 92 | } -------------------------------------------------------------------------------- /src/StatusBarMessageTypes.ts: -------------------------------------------------------------------------------- 1 | 2 | export const enum StatusBarMessageTypes 3 | { 4 | SUCCESS, 5 | INDEFINITE, 6 | ERROR 7 | } -------------------------------------------------------------------------------- /src/compiles/less/CompileLessCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as Configuration from "../../Configuration"; 4 | import * as StatusBarMessage from "../../StatusBarMessage"; 5 | import {StatusBarMessageTypes} from "../../StatusBarMessageTypes"; 6 | 7 | import * as LessCompiler from "./LessCompiler"; 8 | // const impor = require('impor')(__dirname); 9 | // const LessCompiler = impor("./LessCompiler") as typeof import('./LessCompiler'); 10 | export class CompileLessCommand 11 | { 12 | public constructor( 13 | private filePath: string, 14 | private lessDiagnosticCollection: vscode.DiagnosticCollection) 15 | { 16 | } 17 | 18 | public execute(callback = () => {}) 19 | { 20 | StatusBarMessage.hideError(); 21 | 22 | let globalOptions = Configuration.getGlobalOptions(this.filePath, 'less'); 23 | let compilingMessage = StatusBarMessage.show("$(zap) Compiling less --> css", StatusBarMessageTypes.INDEFINITE); 24 | let startTime: number = Date.now(); 25 | let renderPromise = LessCompiler.compile(this.filePath, globalOptions) 26 | .then(() => 27 | { 28 | compilingMessage.dispose(); 29 | let elapsedTime: number = (Date.now() - startTime); 30 | this.lessDiagnosticCollection.set(vscode.Uri.parse(this.filePath), []); 31 | 32 | StatusBarMessage.show(`$(check) Less compiled in ${elapsedTime}ms`, StatusBarMessageTypes.SUCCESS); 33 | callback(); 34 | }) 35 | .catch((error: any) => 36 | { 37 | compilingMessage.dispose(); 38 | 39 | let file:string; 40 | if(error.filename && this.filePath != error.filename){ 41 | file = error.filename; 42 | }else{ 43 | file = this.filePath; 44 | } 45 | 46 | StatusBarMessage.formatDiagnostic(this.lessDiagnosticCollection, file, [StatusBarMessage.getDiagnostic(error)]); 47 | 48 | StatusBarMessage.show("$(alert) Error compiling less (more detail in Errors and Warnings)", StatusBarMessageTypes.ERROR); 49 | callback(); 50 | }); 51 | } 52 | } -------------------------------------------------------------------------------- /src/compiles/less/LessCompiler.ts: -------------------------------------------------------------------------------- 1 | import less from 'less' 2 | import * as mkpath from 'mkpath' 3 | import * as path from 'path' 4 | import * as fs from 'fs' 5 | 6 | import * as Configuration from "../../Configuration"; 7 | import * as FileOptionsParser from "../../FileOptionsParser"; 8 | 9 | const DEFAULT_EXT = ".css"; 10 | 11 | // compile the given less file 12 | export function compile(lessFile: string, defaults): Promise 13 | { 14 | return readFilePromise(lessFile).then(buffer => 15 | { 16 | const content: string = buffer.toString(); 17 | const options = FileOptionsParser.parse(content, defaults); 18 | const lessPath: string = path.dirname(lessFile); 19 | 20 | // main is set: compile the referenced file instead 21 | if (options.main) 22 | { 23 | const mainFilePaths: string[] = Configuration.resolveMainFilePaths(options.main, lessPath, lessFile); 24 | if(!options.exclude) options.exclude = []; 25 | if(options.excludes) options.exclude = Object.assign([], options.exclude, options.excludes); 26 | const excludePaths: string[] = Configuration.resolveMainFilePaths(options.exclude, lessPath, lessFile); 27 | let lastPromise: Promise | null = null; 28 | if (mainFilePaths && mainFilePaths.length > 0) 29 | { 30 | for (const filePath of mainFilePaths) 31 | { 32 | if(filePath.indexOf('*')>-1){ 33 | const paths = filePath.split('*'); 34 | const subfiles = getSubFiles(paths[0], paths[1], excludePaths); 35 | 36 | for (const fileP of subfiles) 37 | { 38 | lastPromise = compilenext(fileP, defaults, lastPromise); 39 | } 40 | }else{ 41 | lastPromise = compilenext(filePath, defaults, lastPromise); 42 | } 43 | } 44 | return lastPromise; 45 | } 46 | } 47 | 48 | // out 49 | if (options.out === null || options.out === false) 50 | { 51 | // is null or false: do not compile 52 | return null; 53 | } 54 | 55 | const out: string | boolean | undefined = options.out; 56 | const extension: string = chooseExtension(options); 57 | let cssRelativeFilename: string; 58 | const baseFilename: string = path.parse(lessFile).name; 59 | 60 | if (typeof out === "string") 61 | { 62 | // out is set: output to the given file name 63 | // check whether is a folder first 64 | let interpolatedOut = Configuration.intepolatePath(out); 65 | 66 | cssRelativeFilename = interpolatedOut; 67 | let lastCharacter = cssRelativeFilename.slice(-1); 68 | if (lastCharacter === '/' || lastCharacter === '\\') 69 | { 70 | cssRelativeFilename += baseFilename + extension; 71 | } 72 | else if (path.extname(cssRelativeFilename) === '') 73 | { 74 | cssRelativeFilename += extension; 75 | } 76 | } 77 | else 78 | { 79 | // out is not set: output to the same basename as the less file 80 | cssRelativeFilename = baseFilename + extension; 81 | } 82 | 83 | const cssFile = path.resolve(lessPath, cssRelativeFilename); 84 | delete options.out; 85 | 86 | // sourceMap 87 | let sourceMapFile: string; 88 | if (options.sourceMap) 89 | { 90 | // currently just has support for writing .map file to same directory 91 | const lessPath: string = path.parse(lessFile).dir; 92 | const cssPath: string = path.parse(cssFile).dir; 93 | const lessRelativeToCss: string = path.relative(cssPath, lessPath); 94 | 95 | const sourceMapOptions = { 96 | outputSourceFiles: false, 97 | sourceMapBasepath: lessPath, 98 | sourceMapFileInline: options.sourceMapFileInline, 99 | sourceMapRootpath: lessRelativeToCss, 100 | sourceMapURL: '', 101 | }; 102 | 103 | if (!sourceMapOptions.sourceMapFileInline) 104 | { 105 | sourceMapFile = cssFile + '.map'; 106 | // sourceMapOptions.sourceMapURL = "./" + baseFilename + extension + ".map"; 107 | } 108 | options.sourceMap = sourceMapOptions; 109 | } 110 | 111 | // plugins 112 | options.plugins = []; 113 | if (options.sass2less !== false) 114 | { 115 | const LessPluginSass2less = require('../../plugins/pluginSass2Less'); 116 | const sass2lessPlugin = new LessPluginSass2less(); 117 | options.plugins.push(sass2lessPlugin); 118 | } 119 | if (options.functions !== false) 120 | { 121 | const LessPluginFunctions = require('less-plugin-functions'); 122 | const pluginFunctions = new LessPluginFunctions(); 123 | options.plugins.push(pluginFunctions); 124 | } 125 | 126 | if (options.autoprefixer) 127 | { 128 | const LessPluginAutoPrefix = require('less-plugin-autoprefix'); 129 | const browsers: string[] = cleanBrowsersList(options.autoprefixer); 130 | const autoprefixPlugin = new LessPluginAutoPrefix({ browsers }); 131 | 132 | options.plugins.push(autoprefixPlugin); 133 | } 134 | 135 | if (options.groupmedia) 136 | { 137 | const LessPluginGroupMedia = require('../../plugins/pluginGroup'); 138 | const lessGroupPlugin = new LessPluginGroupMedia(); 139 | options.plugins.push(lessGroupPlugin); 140 | } 141 | 142 | if (options.compress) 143 | { 144 | options.compress = false; 145 | const LessPluginCleanCSS = require('../../plugins/pluginCleanCss'); 146 | const cleanCSSPlugin = new LessPluginCleanCSS({advanced: true}); 147 | options.plugins.push(cleanCSSPlugin); 148 | } 149 | 150 | // set up the parser 151 | return less.render(content, options).then(output => 152 | { 153 | if (options.sourceMap && output.map && sourceMapFile){ 154 | const mapFileUrl: string = path.basename(sourceMapFile); 155 | output.css += '/*# sourceMappingURL='+mapFileUrl+' */'; 156 | } 157 | 158 | return writeFileContents(cssFile, output.css).then(() => 159 | { 160 | if (options.sourceMap && output.map && sourceMapFile) 161 | { 162 | return writeFileContents(sourceMapFile, output.map); 163 | } 164 | }); 165 | }); 166 | }); 167 | } 168 | 169 | function cleanBrowsersList(autoprefixOption: string | string[]): string[] 170 | { 171 | let browsers: string[]; 172 | if (Array.isArray(autoprefixOption)) 173 | { 174 | browsers = autoprefixOption; 175 | } 176 | else 177 | { 178 | browsers = ("" + autoprefixOption).split(/,|;/); 179 | } 180 | 181 | return browsers.map(browser => browser.trim()); 182 | } 183 | 184 | // writes a file's contents in a path where directories may or may not yet exist 185 | function writeFileContents(this: void, filepath: string, content: any): Promise 186 | { 187 | return new Promise((resolve, reject) => 188 | { 189 | mkpath(path.dirname(filepath), err => 190 | { 191 | if (err) 192 | { 193 | return reject(err); 194 | } 195 | 196 | fs.writeFile(filepath, content, err => err ? reject(err) : resolve()); 197 | }); 198 | }); 199 | } 200 | 201 | function readFilePromise(this: void, filename: string): Promise 202 | { 203 | return new Promise((resolve, reject) => 204 | { 205 | fs.readFile(filename, (err: any, buffer: Buffer) => 206 | { 207 | if (err) 208 | { 209 | reject(err) 210 | } 211 | else 212 | { 213 | resolve(buffer); 214 | } 215 | }); 216 | }); 217 | } 218 | 219 | function chooseExtension(this: void, options): string 220 | { 221 | if (options && options.outExt) 222 | { 223 | if (options.outExt === "") 224 | { 225 | // special case for no extension (no idea if anyone would really want this?) 226 | return ""; 227 | } 228 | 229 | return ensureDotPrefixed(options.outExt) || DEFAULT_EXT; 230 | } 231 | 232 | return DEFAULT_EXT; 233 | } 234 | 235 | function ensureDotPrefixed(this: void, extension: string): string 236 | { 237 | if (extension.startsWith(".")) 238 | { 239 | return extension; 240 | } 241 | 242 | return extension ? `.${extension}` : ""; 243 | } 244 | 245 | function getSubFiles(parent, file, excludePaths){ 246 | let dirList = fs.readdirSync(parent); 247 | let paths:string[] = []; 248 | dirList.forEach(function(item){ 249 | let p = path.join(parent, item); 250 | if(excludePaths.indexOf(p)<0){ 251 | p = path.join(p, file); 252 | if(fs.existsSync(p)){ 253 | paths.push(p); 254 | } 255 | } 256 | }); 257 | return paths; 258 | } 259 | 260 | function promiseChainer(lastPromise: Promise, nextPromise: Promise): Promise{ 261 | lastPromise.then(() => nextPromise); 262 | return nextPromise; 263 | } 264 | 265 | function compilenext(filePath, defaults, lastPromise): Promise{ 266 | const mainPath: path.ParsedPath = path.parse(filePath); 267 | const mainRootFileInfo = Configuration.getRootFileInfo(mainPath); 268 | const mainDefaults = Object.assign({}, defaults, { rootFileInfo: mainRootFileInfo }); 269 | const compilePromise = compile(filePath, mainDefaults); 270 | if (lastPromise) 271 | { 272 | lastPromise = promiseChainer(lastPromise, compilePromise); 273 | } 274 | else 275 | { 276 | lastPromise = compilePromise; 277 | } 278 | return lastPromise; 279 | } -------------------------------------------------------------------------------- /src/compiles/sass/CompileSassCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import * as Configuration from "../../Configuration"; 4 | import * as StatusBarMessage from "../../StatusBarMessage"; 5 | import {StatusBarMessageTypes} from "../../StatusBarMessageTypes"; 6 | 7 | import * as SassCompiler from "./SassCompiler"; 8 | // const impor = require('impor')(__dirname); 9 | // const SassCompiler = impor("./SassCompiler") as typeof import('./SassCompiler'); 10 | 11 | export class CompileSassCommand 12 | { 13 | public constructor( 14 | private filePath: string, 15 | private sassDiagnosticCollection: vscode.DiagnosticCollection) 16 | { 17 | } 18 | 19 | public execute(callback = () => {}) 20 | { 21 | StatusBarMessage.hideError(); 22 | let globalOptions = Configuration.getGlobalOptions(this.filePath, 'sass'); 23 | let compilingMessage = StatusBarMessage.show("$(zap) Compiling sass --> css", StatusBarMessageTypes.INDEFINITE); 24 | let startTime: number = Date.now(); 25 | let renderPromise = SassCompiler.compile(this.filePath, globalOptions) 26 | .then((sass: any) => 27 | { 28 | compilingMessage.dispose(); 29 | if(sass) sass.clearFiles(); 30 | let elapsedTime: number = (Date.now() - startTime); 31 | this.sassDiagnosticCollection.set(vscode.Uri.parse(this.filePath), []); 32 | 33 | StatusBarMessage.show(`$(check) Sass compiled in ${elapsedTime}ms`, StatusBarMessageTypes.SUCCESS); 34 | callback(); 35 | }) 36 | .catch((error: any) => 37 | { 38 | compilingMessage.dispose(); 39 | if(error.sass) error.sass.clearFiles(); 40 | 41 | let file:string; 42 | if(error.filename && this.filePath != error.filename){ 43 | file = error.filename; 44 | }else{ 45 | file = this.filePath; 46 | } 47 | 48 | StatusBarMessage.formatDiagnostic(this.sassDiagnosticCollection, file, [StatusBarMessage.getDiagnostic(error)]); 49 | 50 | StatusBarMessage.show("$(alert) Error compiling sass (more detail in Errors and Warnings)", StatusBarMessageTypes.ERROR); 51 | callback(); 52 | }); 53 | } 54 | } -------------------------------------------------------------------------------- /src/compiles/sass/SassCompiler.ts: -------------------------------------------------------------------------------- 1 | import * as sassCompiler from 'sass.js'; 2 | import * as mkpath from 'mkpath'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import * as vscode from 'vscode'; 6 | 7 | import * as Configuration from "../../Configuration"; 8 | import * as FileOptionsParser from "../../FileOptionsParser"; 9 | 10 | const DEFAULT_EXT = ".css"; 11 | 12 | // compile the given sass file 13 | export function compile(sassFile: string, defaults): Promise 14 | { 15 | return readFilePromise(sassFile).then(buffer => 16 | { 17 | const content: string = buffer.toString(); 18 | const options = FileOptionsParser.parse(content, defaults); 19 | const sassPath: string = path.dirname(sassFile); 20 | 21 | // main is set: compile the referenced file instead 22 | if (options.main) 23 | { 24 | const mainFilePaths: string[] = Configuration.resolveMainFilePaths(options.main, sassPath, sassFile); 25 | if(!options.exclude) options.exclude = []; 26 | if(options.excludes) options.exclude = Object.assign([], options.exclude, options.excludes); 27 | const excludePaths: string[] = Configuration.resolveMainFilePaths(options.exclude, sassPath, sassFile); 28 | let lastPromise: Promise | null = null; 29 | if (mainFilePaths && mainFilePaths.length > 0) 30 | { 31 | for (const filePath of mainFilePaths) 32 | { 33 | if(filePath.indexOf('*')>-1){ 34 | const paths = filePath.split('*'); 35 | const subfiles = getSubFiles(paths[0], paths[1], excludePaths); 36 | 37 | for (const fileP of subfiles) 38 | { 39 | lastPromise = compilenext(fileP, defaults, lastPromise); 40 | } 41 | }else{ 42 | lastPromise = compilenext(filePath, defaults, lastPromise); 43 | } 44 | } 45 | return lastPromise; 46 | } 47 | } 48 | 49 | // out 50 | if (options.out === null || options.out === false) 51 | { 52 | // is null or false: do not compile 53 | return null; 54 | } 55 | 56 | const out: string | boolean | undefined = options.out; 57 | const extension: string = chooseExtension(options); 58 | let cssRelativeFilename: string; 59 | const baseFilename: string = path.parse(sassFile).name; 60 | 61 | if (typeof out === "string") 62 | { 63 | // out is set: output to the given file name 64 | // check whether is a folder first 65 | let interpolatedOut = Configuration.intepolatePath(out); 66 | 67 | cssRelativeFilename = interpolatedOut; 68 | let lastCharacter = cssRelativeFilename.slice(-1); 69 | if (lastCharacter === '/' || lastCharacter === '\\') 70 | { 71 | cssRelativeFilename += baseFilename + extension; 72 | } 73 | else if (path.extname(cssRelativeFilename) === '') 74 | { 75 | cssRelativeFilename += extension; 76 | } 77 | } 78 | else 79 | { 80 | // out is not set: output to the same basename as the less file 81 | cssRelativeFilename = baseFilename + extension; 82 | } 83 | 84 | const cssFile = path.resolve(sassPath, cssRelativeFilename); 85 | const cssPath: string = path.parse(cssFile).dir; 86 | delete options.out; 87 | //need change "module.exports = factory()" to "module.exports = factory" in node_modules/sass.js/dist/sass.sync.js to make this work 88 | const sass = new sassCompiler(); 89 | let opts = { 90 | style: sass.style.expanded 91 | }; 92 | 93 | let sourceMapFile: string; 94 | let replaceList:any = { 95 | "stdin": path.relative(cssPath, sassFile) 96 | }; 97 | if (options.sourceMap) 98 | { 99 | const sassRelativeToCss: string = path.relative(cssPath, sassPath); 100 | 101 | const sourceMapOptions = { 102 | sourceMapContents: false, 103 | sourceMapEmbed: options.sourceMapFileInline, 104 | sourceMapRoot: sassRelativeToCss 105 | // inputPath: path.basename(sassFile) 106 | }; 107 | 108 | if(!options.sourceMapFileInline){ 109 | sourceMapFile = cssFile + '.map'; 110 | } 111 | 112 | opts = Object.assign({}, opts, sourceMapOptions); 113 | } 114 | 115 | sass._path = '/'+sassPath.replace(/\\/g, '/').replace(/\:/g, '')+'/'; 116 | sass.importer(function(request, done) { 117 | if (request.path) { 118 | done(); 119 | }else{ 120 | let requestedPath = sassPath; 121 | if(request.previous != 'stdin') 122 | requestedPath = path.resolve(sassPath, path.dirname(request.previous)); 123 | let paths = getPathVariations(request.current); 124 | let x, file; 125 | for(x in paths){ 126 | let realPath = path.resolve(requestedPath, paths[x]); 127 | if(fileExists(realPath)){ 128 | file = realPath; 129 | break; 130 | } 131 | } 132 | if (!file) { 133 | done({ 134 | error: 'File "' + request.current + '" not found', 135 | }); 136 | return; 137 | } 138 | 139 | readFilePromise(file).then(buffer => 140 | { 141 | replaceList[request.current] = path.relative(sassPath, file); 142 | const content: string = buffer.toString(); 143 | sass.writeFile(request.resolved, content, function() { 144 | done({ 145 | path: replaceList[request.current], 146 | content: content 147 | }); 148 | }); 149 | }).catch((error: any) => 150 | { 151 | done({ 152 | error: error.message, 153 | }); 154 | }); 155 | } 156 | }); 157 | 158 | return new Promise((resolve, reject) => 159 | { 160 | sass.compile(content, opts, result => { 161 | if(result.status == 1){ 162 | result.sass = sass; 163 | reject(result); 164 | }else{ 165 | let css = result.text; 166 | let sourceMap; 167 | if(css){ 168 | if (options.autoprefixer) 169 | { 170 | const LessPluginAutoPrefix = require('less-plugin-autoprefix'); 171 | const browsers: string[] = cleanBrowsersList(options.autoprefixer); 172 | const autoprefixPlugin = new LessPluginAutoPrefix({ browsers }); 173 | 174 | autoprefixPlugin.install(result, { 175 | addPostProcessor: function (postProcessor){ 176 | css = postProcessor.process(css, {}); 177 | } 178 | }); 179 | } 180 | 181 | if (options.groupmedia) 182 | { 183 | const SassPluginGroupMedia = require('../../plugins/pluginGroup'); 184 | const sassGroupPlugin = new SassPluginGroupMedia(); 185 | 186 | sassGroupPlugin.install(result, { 187 | addPostProcessor: function (postProcessor){ 188 | css = postProcessor.process(css, {}); 189 | } 190 | }); 191 | } 192 | 193 | if (options.compress) 194 | { 195 | options.compress = false; 196 | const LessPluginCleanCSS = require('../../plugins/pluginCleanCss'); 197 | const cleanCSSPlugin = new LessPluginCleanCSS({advanced: true}); 198 | 199 | cleanCSSPlugin.install(result, { 200 | addPostProcessor: function (postProcessor){ 201 | css = postProcessor.process(css, {sourceMap: result.map?{ 202 | getExternalSourceMap: function(){ 203 | return JSON.stringify(result.map) 204 | }, 205 | setExternalSourceMap: function(map){ 206 | result.map = JSON.parse(map); 207 | }, 208 | }:false, options: options}); 209 | } 210 | }); 211 | } 212 | if (options.sourceMap && result.map){ 213 | sourceMap = result.map; 214 | let x; 215 | for(x in sourceMap.sources){ 216 | let path = sourceMap.sources[x]; 217 | if(replaceList[path]) sourceMap.sources[x] = replaceList[path]; 218 | } 219 | 220 | if(sourceMapFile){ 221 | const mapFileUrl: string = path.basename(sourceMapFile); 222 | css += "\n"+'/*# sourceMappingURL='+mapFileUrl+' */'; 223 | }else{ 224 | css += "\n"+'/*# sourceMappingURL=data:application/json;charset=utf-8;base64,'+Buffer.from(JSON.stringify(sourceMap)).toString("base64")+' */'; 225 | } 226 | } 227 | }else{ 228 | css = ""; 229 | } 230 | 231 | return writeFileContents(cssFile, css).then(() => 232 | { 233 | if (sourceMap && sourceMapFile) 234 | { 235 | return writeFileContents(sourceMapFile, JSON.stringify(sourceMap)).then(() => { 236 | resolve(sass); 237 | }); 238 | }else{ 239 | resolve(sass); 240 | } 241 | }); 242 | } 243 | }); 244 | }); 245 | }); 246 | } 247 | 248 | function fileExists(path) { 249 | try { 250 | var stat = fs.statSync(path); 251 | return stat && stat.isFile(); 252 | }catch(err) { 253 | } 254 | return false; 255 | } 256 | 257 | function getPathVariations(currentPath) { 258 | // [importer,include_path] this is where we would add the ability to 259 | // examine the include_path (if we ever use that in Sass.js) 260 | currentPath = path.normalize(currentPath); 261 | var directory = path.dirname(currentPath); 262 | 263 | var basename = path.basename(currentPath); 264 | var extensions = ['.scss', '.sass', '.css']; 265 | // basically what is done by resolve_and_load() in file.cpp 266 | // Resolution order for ambiguous imports: 267 | var list = [ 268 | // (1) filename as given 269 | currentPath, 270 | // (2) underscore + given 271 | directory + path.sep + '_' + basename 272 | ].concat(extensions.map(function(extension) { 273 | // (3) underscore + given + extension 274 | return directory + path.sep + '_' + basename + extension; 275 | })).concat(extensions.map(function(extension) { 276 | // (4) given + extension 277 | return directory + path.sep + '_' + basename + extension; 278 | })); 279 | 280 | return list; 281 | }; 282 | 283 | 284 | function cleanBrowsersList(autoprefixOption: string | string[]): string[] 285 | { 286 | let browsers: string[]; 287 | if (Array.isArray(autoprefixOption)) 288 | { 289 | browsers = autoprefixOption; 290 | } 291 | else 292 | { 293 | browsers = ("" + autoprefixOption).split(/,|;/); 294 | } 295 | 296 | return browsers.map(browser => browser.trim()); 297 | } 298 | 299 | // writes a file's contents in a path where directories may or may not yet exist 300 | function writeFileContents(this: void, filepath: string, content: any): Promise 301 | { 302 | return new Promise((resolve, reject) => 303 | { 304 | mkpath(path.dirname(filepath), err => 305 | { 306 | if (err) 307 | { 308 | return reject(err); 309 | } 310 | 311 | fs.writeFile(filepath, content, err => err ? reject(err) : resolve()); 312 | }); 313 | }); 314 | } 315 | 316 | function readFilePromise(this: void, filename: string): Promise 317 | { 318 | return new Promise((resolve, reject) => 319 | { 320 | fs.readFile(filename, (err: any, buffer: Buffer) => 321 | { 322 | if (err) 323 | { 324 | reject(err) 325 | } 326 | else 327 | { 328 | resolve(buffer); 329 | } 330 | }); 331 | }); 332 | } 333 | 334 | function chooseExtension(this: void, options): string 335 | { 336 | if (options && options.outExt) 337 | { 338 | if (options.outExt === "") 339 | { 340 | // special case for no extension (no idea if anyone would really want this?) 341 | return ""; 342 | } 343 | 344 | return ensureDotPrefixed(options.outExt) || DEFAULT_EXT; 345 | } 346 | 347 | return DEFAULT_EXT; 348 | } 349 | 350 | function ensureDotPrefixed(this: void, extension: string): string 351 | { 352 | if (extension.startsWith(".")) 353 | { 354 | return extension; 355 | } 356 | 357 | return extension ? `.${extension}` : ""; 358 | } 359 | 360 | function getSubFiles(parent, file, excludePaths){ 361 | let dirList = fs.readdirSync(parent); 362 | let paths:string[] = []; 363 | dirList.forEach(function(item){ 364 | let p = path.join(parent, item); 365 | if(excludePaths.indexOf(p)<0){ 366 | p = path.join(p, file); 367 | if(fs.existsSync(p)){ 368 | paths.push(p); 369 | } 370 | } 371 | }); 372 | return paths; 373 | } 374 | 375 | function promiseChainer(lastPromise: Promise, nextPromise: Promise): Promise{ 376 | lastPromise.then(() => nextPromise); 377 | return nextPromise; 378 | } 379 | 380 | function compilenext(filePath, defaults, lastPromise): Promise{ 381 | const mainPath: path.ParsedPath = path.parse(filePath); 382 | const mainRootFileInfo = Configuration.getRootFileInfo(mainPath); 383 | const mainDefaults = Object.assign({}, defaults, { rootFileInfo: mainRootFileInfo }); 384 | const compilePromise = compile(filePath, mainDefaults); 385 | if (lastPromise) 386 | { 387 | lastPromise = promiseChainer(lastPromise, compilePromise); 388 | } 389 | else 390 | { 391 | lastPromise = compilePromise; 392 | } 393 | return lastPromise; 394 | } -------------------------------------------------------------------------------- /src/compiles/typescript/CompileTsCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | import * as Configuration from "../../Configuration"; 5 | import * as StatusBarMessage from "../../StatusBarMessage"; 6 | import {StatusBarMessageTypes} from "../../StatusBarMessageTypes"; 7 | import * as TsCompiler from "./TsCompiler"; 8 | 9 | const defaultOpts = { 10 | 'surround': '(function (define){ ${code} })(define)' 11 | }; 12 | export class CompileTsCommand 13 | { 14 | public constructor( 15 | private filePath: string, 16 | private tsDiagnosticCollection: vscode.DiagnosticCollection) 17 | { 18 | } 19 | 20 | public execute(callback = () => {}) 21 | { 22 | StatusBarMessage.hideError(); 23 | 24 | let globalOptions = Configuration.getGlobalOptions(this.filePath, 'typescript', defaultOpts); 25 | let compilingMessage = StatusBarMessage.show("$(zap) Compiling ts --> js", StatusBarMessageTypes.INDEFINITE); 26 | let startTime: number = Date.now(); 27 | let renderPromise = TsCompiler.compile(this.filePath, globalOptions) 28 | .then((allDiagnostics) => 29 | { 30 | compilingMessage.dispose(); 31 | if(allDiagnostics !== null){ 32 | if(allDiagnostics.length>0){ 33 | let files = {}; 34 | allDiagnostics.forEach(diagnostic => { 35 | let file = diagnostic.filename?diagnostic.filename:this.filePath; 36 | if(!files[file]) files[file] = []; 37 | files[file].push(StatusBarMessage.getDiagnostic(diagnostic)); 38 | }); 39 | 40 | for (const key in files) { 41 | const alld = files[key]; 42 | StatusBarMessage.formatDiagnostic(this.tsDiagnosticCollection, key, alld); 43 | } 44 | 45 | StatusBarMessage.show("$(alert) Error compiling typescript (more detail in Errors and Warnings)", StatusBarMessageTypes.ERROR); 46 | }else{ 47 | let elapsedTime: number = (Date.now() - startTime); 48 | this.tsDiagnosticCollection.set(vscode.Uri.parse(this.filePath), []); 49 | 50 | StatusBarMessage.show(`$(check) Typescript compiled in ${elapsedTime}ms`, StatusBarMessageTypes.SUCCESS); 51 | } 52 | } 53 | else { 54 | StatusBarMessage.show(`$(check) Typescript compiling disabled`, StatusBarMessageTypes.SUCCESS); 55 | } 56 | callback(); 57 | }) 58 | .catch((error: any) => 59 | { 60 | compilingMessage.dispose(); 61 | 62 | let file:string; 63 | if(error.filename && this.filePath != error.filename){ 64 | file = error.filename; 65 | }else{ 66 | file = this.filePath; 67 | } 68 | 69 | StatusBarMessage.formatDiagnostic(this.tsDiagnosticCollection, file, [StatusBarMessage.getDiagnostic(error)]); 70 | 71 | StatusBarMessage.show("$(alert) Error compiling typescript (more detail in Errors and Warnings)", StatusBarMessageTypes.ERROR); 72 | callback(); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/compiles/typescript/TsCompiler.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript" 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | import * as vscode from 'vscode'; 5 | 6 | import * as Configuration from "../../Configuration"; 7 | import * as FileOptionsParser from "../../FileOptionsParser"; 8 | import {MinifyJsCommand} from "../../minify/js/MinifyJsCommand"; 9 | import { isArray } from "util" 10 | 11 | const DEFAULT_EXT = ".js"; 12 | 13 | // compile the given ts file 14 | export function compile(tsFile: string, defaults): Promise 15 | { 16 | return readFilePromise(tsFile).then(buffer => 17 | { 18 | const content: string = buffer.toString(); 19 | const options = FileOptionsParser.parse(content, defaults); 20 | const tsPath: string = path.dirname(tsFile); 21 | 22 | // main is set: compile the referenced file instead 23 | if (options.main) 24 | { 25 | const mainFilePaths: string[] = Configuration.resolveMainFilePaths(options.main, tsPath, tsFile); 26 | if(!options.exclude) options.exclude = []; 27 | if(options.excludes) options.exclude = Object.assign([], options.exclude, options.excludes); 28 | const excludePaths: string[] = Configuration.resolveMainFilePaths(options.exclude, tsPath, tsFile); 29 | let lastPromise: Promise | null = null; 30 | if (mainFilePaths && mainFilePaths.length > 0) 31 | { 32 | for (const filePath of mainFilePaths) 33 | { 34 | if(filePath.indexOf('*')>-1){ 35 | const paths = filePath.split('*'); 36 | const subfiles = getSubFiles(paths[0], paths[1], excludePaths); 37 | for (const fileP of subfiles) 38 | { 39 | lastPromise = compilenext(fileP, defaults, lastPromise); 40 | } 41 | }else{ 42 | lastPromise = compilenext(filePath, defaults, lastPromise); 43 | } 44 | } 45 | return lastPromise; 46 | } 47 | } 48 | 49 | // out 50 | if ( typeof options.outfile !== "string" 51 | && typeof options.outdir !== "string") 52 | { 53 | // is null or false: do not compile 54 | return null; 55 | } 56 | 57 | const outfile: string | undefined = options.outfile; 58 | const outdir: string | undefined = options.outdir; 59 | 60 | const baseFilename: string = path.parse(tsFile).name; 61 | const pathToTypes = path.resolve(Configuration.intepolatePath('${workspaceRoot}/node_modules/@types')); 62 | let tsOptions:any = { 63 | noEmitOnError: true, noImplicitAny: false, sourceMap: false, 64 | allowJs: true, removeComments: true, listEmittedFiles: true, 65 | target: ts.ScriptTarget.ES5, module: ts.ModuleKind.AMD, 66 | typeRoots: [pathToTypes] 67 | } 68 | tsOptions = Object.assign({}, tsOptions, options); 69 | if (typeof outfile === "string") 70 | { 71 | tsOptions = Object.assign({}, tsOptions, {outFile: Configuration.resolveFilePath(outfile, tsPath, tsFile)}); 72 | } 73 | else if (typeof outdir === "string") 74 | { 75 | tsOptions = Object.assign({}, tsOptions, {outDir: Configuration.resolveFilePath(outdir, tsPath, tsFile)}); 76 | } 77 | 78 | if(isArray(tsOptions.lib)){ 79 | tsOptions.lib.forEach((o, i, a) => o.indexOf('.d.')>-1?null:a[i]="lib."+o+".d.ts"); 80 | } 81 | 82 | let program = ts.createProgram([tsFile], tsOptions); 83 | let emitResult = program.emit(); 84 | 85 | let allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); 86 | let alld: Array = []; 87 | 88 | allDiagnostics.forEach(diagnostic => { 89 | let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 90 | if(diagnostic.file){ 91 | let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); 92 | alld.push({ 93 | line: line+1, 94 | column: character, 95 | message: message, 96 | filename: diagnostic.file.fileName 97 | }); 98 | }else{ 99 | alld.push({ 100 | code: diagnostic.code, 101 | message: message 102 | }); 103 | } 104 | }); 105 | 106 | if( alld.length==0 && options.compress && emitResult.emittedFiles){ 107 | return new Promise((resolve, reject) => 108 | { 109 | if(emitResult.emittedFiles && emitResult.emittedFiles.length>0){ 110 | let surround = options.surround; 111 | let total = emitResult.emittedFiles.length; 112 | let done = function(){ 113 | if(--total==0){ 114 | resolve(alld); 115 | } 116 | } 117 | emitResult.emittedFiles.forEach(file => { 118 | let minifyLib = new MinifyJsCommand(undefined, undefined, file, surround); 119 | minifyLib.execute(function (){ 120 | done(); 121 | }); 122 | }); 123 | }else{ 124 | resolve(alld); 125 | } 126 | }); 127 | } 128 | 129 | return returnPromise(alld); 130 | }); 131 | } 132 | 133 | function returnPromise(obj:any): Promise 134 | { 135 | return new Promise((resolve, reject) => 136 | { 137 | resolve(obj); 138 | }); 139 | } 140 | 141 | function readFilePromise(this: void, filename: string): Promise 142 | { 143 | return new Promise((resolve, reject) => 144 | { 145 | fs.readFile(filename, (err: any, buffer: Buffer) => 146 | { 147 | if (err) 148 | { 149 | reject(err) 150 | } 151 | else 152 | { 153 | resolve(buffer); 154 | } 155 | }); 156 | }); 157 | } 158 | 159 | function getSubFiles(parent, file, excludePaths){ 160 | let dirList = fs.readdirSync(parent); 161 | let paths:string[] = []; 162 | if(dirList){ 163 | dirList.forEach(function(item){ 164 | let p = path.join(parent, item); 165 | if(excludePaths.indexOf(p)<0){ 166 | p = path.join(p, file); 167 | if(fs.existsSync(p)){ 168 | paths.push(p); 169 | } 170 | } 171 | }); 172 | } 173 | return paths; 174 | } 175 | 176 | function promiseChainer(lastPromise: Promise, nextPromise: Promise): Promise{ 177 | lastPromise.then(() => nextPromise); 178 | return nextPromise; 179 | } 180 | 181 | function compilenext(filePath, defaults, lastPromise): Promise{ 182 | const mainPath: path.ParsedPath = path.parse(filePath); 183 | const mainRootFileInfo = Configuration.getRootFileInfo(mainPath); 184 | const mainDefaults = Object.assign({}, defaults, { rootFileInfo: mainRootFileInfo }); 185 | const compilePromise = compile(filePath, mainDefaults); 186 | if (lastPromise) 187 | { 188 | lastPromise = promiseChainer(lastPromise, compilePromise); 189 | } 190 | else 191 | { 192 | lastPromise = compilePromise; 193 | } 194 | return lastPromise; 195 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import * as Configuration from "./Configuration"; 4 | import ignore from 'ignore'; 5 | 6 | import * as StatusBarMessage from "./StatusBarMessage"; 7 | import {StatusBarMessageTypes} from "./StatusBarMessageTypes"; 8 | 9 | const impor = require('impor')(__dirname); 10 | 11 | const LESS_EXT = ".less"; 12 | const SASS_EXT = ".sass"; 13 | const SCSS_EXT = ".scss"; 14 | const TS_EXT = ".ts"; 15 | const CSS_EXT = ".css"; 16 | const JS_EXT = ".js"; 17 | const COMPILE_COMMAND = "easyCompile.compile"; 18 | const MINIFY_COMMAND = "easyCompile.minify"; 19 | const MINIFYDIR_COMMAND = "easyCompile.minifydir"; 20 | const queue: { type: string, filePath: string }[] = []; 21 | 22 | let DiagnosticCollection: vscode.DiagnosticCollection; 23 | 24 | export function activate(context: vscode.ExtensionContext) { 25 | DiagnosticCollection = vscode.languages.createDiagnosticCollection(); 26 | 27 | let running = false; 28 | let addQueue = function (type: string, filePath: string){ 29 | for (let i = queue.length - 1; i >= 0; i--) { 30 | if (queue[i].filePath == filePath) { 31 | queue.splice(i, 1); 32 | } 33 | } 34 | 35 | queue.push({ 36 | type: type, 37 | filePath: filePath 38 | }); 39 | 40 | runQueue(); 41 | } 42 | 43 | let runQueue = function (force = false){ 44 | StatusBarMessage.show(`Queue size ${queue.length}`, StatusBarMessageTypes.SUCCESS); 45 | 46 | if(running && !force) return; 47 | running = true; 48 | 49 | if(queue.length == 0){ 50 | running = false; 51 | return; 52 | } 53 | 54 | const curAction = queue.shift(); 55 | if(curAction){ 56 | DiagnosticCollection.clear(); 57 | let organise; 58 | if(curAction.type == LESS_EXT){ 59 | const LessCompiler = impor("./compiles/less/CompileLessCommand") as typeof import('./compiles/less/CompileLessCommand'); 60 | organise = new LessCompiler.CompileLessCommand(curAction.filePath, DiagnosticCollection); 61 | }else if(curAction.type == SASS_EXT){ 62 | const SassCompiler = impor("./compiles/sass/CompileSassCommand") as typeof import('./compiles/sass/CompileSassCommand'); 63 | organise = new SassCompiler.CompileSassCommand(curAction.filePath, DiagnosticCollection); 64 | }else if(curAction.type == TS_EXT){ 65 | const TsCompiler = impor("./compiles/typescript/CompileTsCommand") as typeof import('./compiles/typescript/CompileTsCommand'); 66 | organise = new TsCompiler.CompileTsCommand(curAction.filePath, DiagnosticCollection); 67 | }else if(curAction.type == CSS_EXT){ 68 | const CssCompiler = impor("./minify/css/MinifyCssCommand") as typeof import("./minify/css/MinifyCssCommand"); 69 | organise = new CssCompiler.MinifyCssCommand(curAction.filePath, DiagnosticCollection); 70 | }else if(curAction.type == JS_EXT){ 71 | const JsCompiler = impor("./minify/js/MinifyJsCommand") as typeof import('./minify/js/MinifyJsCommand'); 72 | organise = new JsCompiler.MinifyJsCommand(curAction.filePath, DiagnosticCollection, undefined, undefined); 73 | } 74 | organise.execute(function (){ 75 | runQueue(true); 76 | }); 77 | }else{ 78 | runQueue(true); 79 | } 80 | } 81 | 82 | let runCommand = function (type: string, filePath: string){ 83 | filePath = Configuration.formatPath(filePath); 84 | addQueue(type, filePath); 85 | } 86 | 87 | let runCompileCommand = function(filePath: string){ 88 | let compileOptions = Configuration.getGlobalOptions(filePath); 89 | if(filePath.endsWith(LESS_EXT)){ 90 | if(typeof compileOptions.less === "undefined" || compileOptions.less === true){ 91 | runCommand(LESS_EXT, filePath); 92 | } 93 | } 94 | else if(filePath.endsWith(TS_EXT)){ 95 | if(typeof compileOptions.typescript === "undefined" || compileOptions.typescript === true){ 96 | runCommand(TS_EXT, filePath); 97 | } 98 | } 99 | else if(filePath.endsWith(SASS_EXT) || filePath.endsWith(SCSS_EXT)){ 100 | if(typeof compileOptions.sass === "undefined" || compileOptions.sass === true){ 101 | runCommand(SASS_EXT, filePath); 102 | } 103 | } 104 | else 105 | { 106 | vscode.window.showWarningMessage("This command not work for this file."); 107 | } 108 | } 109 | 110 | // compile command 111 | let compileCommand = vscode.commands.registerCommand(COMPILE_COMMAND, (fileUri?:vscode.Uri) => 112 | { 113 | let filePath: string | undefined; 114 | if(fileUri){ 115 | filePath = fileUri.path; 116 | }else{ 117 | let activeEditor = vscode.window.activeTextEditor; 118 | if (activeEditor){ 119 | let document: vscode.TextDocument = activeEditor.document; 120 | if (document){ 121 | filePath = document.fileName; 122 | } 123 | } 124 | } 125 | 126 | if (filePath) 127 | { 128 | runCompileCommand(filePath); 129 | } 130 | else 131 | { 132 | vscode.window.showInformationMessage("This command not available for this file."); 133 | } 134 | }); 135 | 136 | // minify command 137 | let minifyCommand = vscode.commands.registerCommand(MINIFY_COMMAND, (fileUri?:vscode.Uri) => 138 | { 139 | let filePath: string | undefined; 140 | if(fileUri){ 141 | filePath = fileUri.path; 142 | }else{ 143 | let activeEditor = vscode.window.activeTextEditor; 144 | if (activeEditor){ 145 | let document: vscode.TextDocument = activeEditor.document; 146 | if (document){ 147 | filePath = document.fileName; 148 | } 149 | } 150 | } 151 | 152 | if (filePath) 153 | { 154 | if(filePath.endsWith(JS_EXT)){ 155 | runCommand(JS_EXT, filePath); 156 | } 157 | else if(filePath.endsWith(CSS_EXT)){ 158 | runCommand(CSS_EXT, filePath); 159 | } 160 | else 161 | { 162 | vscode.window.showWarningMessage("This command not work for this file."); 163 | } 164 | } 165 | else 166 | { 167 | vscode.window.showInformationMessage("This command only available after file opened."); 168 | } 169 | }); 170 | 171 | let minifydirCommand = vscode.commands.registerCommand(MINIFYDIR_COMMAND, () => 172 | { 173 | vscode.window.showInformationMessage("Not implement."); 174 | }); 175 | 176 | // automatically compile/minfy on save 177 | let didSaveEvent = vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => 178 | { 179 | let filePath: string | undefined; 180 | let activeEditor = vscode.window.activeTextEditor; 181 | if (activeEditor){ 182 | let document: vscode.TextDocument = activeEditor.document; 183 | if (document){ 184 | filePath = document.fileName; 185 | } 186 | } 187 | 188 | if(filePath === document.fileName){ 189 | let compileOptions = Configuration.getGlobalOptions(filePath); 190 | if(compileOptions.ignore){ 191 | const ig = ignore().add(compileOptions.ignore); 192 | if(ig.ignores(filePath)) return; 193 | } 194 | if (filePath.endsWith(LESS_EXT) || filePath.endsWith(TS_EXT) || filePath.endsWith(SASS_EXT) || filePath.endsWith(SCSS_EXT)) 195 | { 196 | runCompileCommand(filePath); 197 | }else if (filePath.endsWith(JS_EXT) && compileOptions.minifyJsOnSave) 198 | { 199 | runCommand(JS_EXT, filePath); 200 | }else if (filePath.endsWith(CSS_EXT) && compileOptions.minifyCssOnSave) 201 | { 202 | runCommand(CSS_EXT, filePath); 203 | } 204 | } 205 | }); 206 | 207 | // dismiss less/sass/scss errors on file close 208 | let didCloseEvent = vscode.workspace.onDidCloseTextDocument((document: vscode.TextDocument) => 209 | { 210 | if (document.fileName.endsWith(TS_EXT) 211 | || document.fileName.endsWith(CSS_EXT) 212 | || document.fileName.endsWith(JS_EXT) 213 | || document.fileName.endsWith(LESS_EXT) 214 | || document.fileName.endsWith(SASS_EXT) 215 | || document.fileName.endsWith(SCSS_EXT) 216 | ){ 217 | DiagnosticCollection.delete(vscode.Uri.parse(document.fileName)); 218 | } 219 | }) 220 | 221 | context.subscriptions.push(compileCommand); 222 | context.subscriptions.push(minifyCommand); 223 | context.subscriptions.push(minifydirCommand); 224 | context.subscriptions.push(didSaveEvent); 225 | context.subscriptions.push(didCloseEvent); 226 | } 227 | 228 | // this method is called when your extension is deactivated 229 | export function deactivate() 230 | { 231 | if (DiagnosticCollection) 232 | { 233 | DiagnosticCollection.dispose(); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/minify/css/MinifyCssCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import * as mkpath from 'mkpath'; 5 | 6 | import * as Configuration from "../../Configuration"; 7 | import * as StatusBarMessage from "../../StatusBarMessage"; 8 | import {StatusBarMessageTypes} from "../../StatusBarMessageTypes"; 9 | 10 | const CleanCSS = require("clean-css"); 11 | 12 | const defaultOpts = { 13 | processImport: false, 14 | rebase: false, 15 | advanced: true, 16 | groupmedia: false, 17 | sourceMap: false, 18 | sourceMapFileInline: false 19 | } 20 | 21 | export class MinifyCssCommand 22 | { 23 | public constructor( 24 | private filePath: string, 25 | private cssDiagnosticCollection: vscode.DiagnosticCollection) 26 | { 27 | } 28 | 29 | public execute(callback = () => {}) 30 | { 31 | StatusBarMessage.hideError(); 32 | const cssFile = this.filePath; 33 | const cssPath: string = path.dirname(cssFile); 34 | 35 | let opts = Configuration.getGlobalOptions(cssFile, 'css', defaultOpts); 36 | let compilingMessage = StatusBarMessage.show("$(zap) Minifing css", StatusBarMessageTypes.INDEFINITE); 37 | let startTime: number = Date.now(); 38 | 39 | readFilePromise(cssFile).then(buffer => 40 | { 41 | const outExt = opts.outExt?opts.outExt:'.css'; 42 | const baseFilename: string = path.parse(cssFile).name; 43 | 44 | let outDir = cssPath; 45 | if(opts.outDir){ 46 | outDir = Configuration.resolveFilePath(opts.outDir, cssPath, cssFile); 47 | } 48 | const outFile = path.resolve(outDir, baseFilename+outExt); 49 | 50 | let content: string = buffer.toString(); 51 | if(opts.groupmedia){ 52 | let grouper = require('group-css-media-queries'); 53 | content = grouper(content); 54 | } 55 | let output = new CleanCSS(opts).minify(content); 56 | let css = output.styles; 57 | 58 | let sourceMapFile: string | undefined, sourceMap; 59 | let replaceList:any = { 60 | "$stdin": path.relative(outDir, cssFile) 61 | }; 62 | if (opts.sourceMap && output.sourceMap) 63 | { 64 | if(!opts.sourceMapFileInline){ 65 | sourceMapFile = outFile + '.map'; 66 | } 67 | 68 | sourceMap = JSON.parse(output.sourceMap); 69 | let x; 70 | for(x in sourceMap.sources){ 71 | let path = sourceMap.sources[x]; 72 | if(replaceList[path]) sourceMap.sources[x] = replaceList[path]; 73 | } 74 | 75 | if(sourceMapFile){ 76 | const mapFileUrl: string = path.basename(sourceMapFile); 77 | css += "\n"+'/*# sourceMappingURL='+mapFileUrl+' */'; 78 | }else{ 79 | css += "\n"+'/*# sourceMappingURL=data:application/json;charset=utf-8;base64,'+Buffer.from(JSON.stringify(sourceMap)).toString("base64")+' */'; 80 | } 81 | } 82 | 83 | return writeFileContents(outFile, css).then(() => 84 | { 85 | if (sourceMapFile && sourceMap) 86 | { 87 | return writeFileContents(sourceMapFile, JSON.stringify(sourceMap)); 88 | } 89 | }); 90 | }).then(() => 91 | { 92 | compilingMessage.dispose(); 93 | let elapsedTime: number = (Date.now() - startTime); 94 | this.cssDiagnosticCollection.set(vscode.Uri.parse(this.filePath), []); 95 | 96 | StatusBarMessage.show(`$(check) Css minified in ${elapsedTime}ms`, StatusBarMessageTypes.SUCCESS); 97 | 98 | callback(); 99 | }).catch((error: any) => 100 | { 101 | compilingMessage.dispose(); 102 | 103 | let file:string; 104 | if(error.filename && this.filePath != error.filename){ 105 | file = error.filename; 106 | }else{ 107 | file = this.filePath; 108 | } 109 | 110 | StatusBarMessage.formatDiagnostic(this.cssDiagnosticCollection, file, [StatusBarMessage.getDiagnostic(error)]); 111 | 112 | StatusBarMessage.show("$(alert) Error minify css (more detail in Errors and Warnings)", StatusBarMessageTypes.ERROR); 113 | 114 | callback(); 115 | }); 116 | } 117 | } 118 | 119 | 120 | function writeFileContents(this: void, filepath: string, content: any): Promise 121 | { 122 | return new Promise((resolve, reject) => 123 | { 124 | mkpath(path.dirname(filepath), err => 125 | { 126 | if (err) 127 | { 128 | return reject(err); 129 | } 130 | 131 | fs.writeFile(filepath, content, err => err ? reject(err) : resolve(filepath)); 132 | }); 133 | }); 134 | } 135 | 136 | function readFilePromise(this: void, filename: string): Promise 137 | { 138 | return new Promise((resolve, reject) => 139 | { 140 | fs.readFile(filename, (err: any, buffer: Buffer) => 141 | { 142 | if (err) 143 | { 144 | reject(err) 145 | } 146 | else 147 | { 148 | resolve(buffer); 149 | } 150 | }); 151 | }); 152 | } -------------------------------------------------------------------------------- /src/minify/js/MinifyJsCommand.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import * as mkpath from 'mkpath' 5 | 6 | import * as Configuration from "../../Configuration"; 7 | import * as StatusBarMessage from "../../StatusBarMessage"; 8 | import {StatusBarMessageTypes} from "../../StatusBarMessageTypes"; 9 | 10 | const minjs = require('uglify-js'); 11 | const defaultOpts = { 12 | "mangle": { 13 | "properties":{ 14 | "regex": /^_/ 15 | } 16 | }, 17 | "surround": "", 18 | "compress": {} 19 | } 20 | export class MinifyJsCommand 21 | { 22 | public constructor( 23 | private oriFile: string | undefined, 24 | private jsDiagnosticCollection: vscode.DiagnosticCollection | undefined, 25 | private file: string | undefined, 26 | private surround: string | undefined 27 | ){ 28 | } 29 | 30 | public execute(callback = () => {}) 31 | { 32 | StatusBarMessage.hideError(); 33 | 34 | let filename; 35 | if(this.file) filename = this.file; 36 | else filename = this.oriFile; 37 | 38 | let opts = Configuration.getGlobalOptions(filename, 'js', defaultOpts); 39 | let compilingMessage = StatusBarMessage.show("$(zap) Minifing js", StatusBarMessageTypes.INDEFINITE); 40 | let startTime: number = Date.now(); 41 | 42 | if(this.surround){ 43 | opts.surround = this.surround; 44 | } 45 | 46 | readFilePromise(filename).then(buffer => 47 | { 48 | let content: string = buffer.toString(); 49 | if(typeof opts.surround == "string" && opts.surround != ''){ 50 | content = opts.surround.replace(/\$\{code\}/g, content.replace(/\$/g, '$$$$')); 51 | } 52 | let uglifyOptions = { 53 | "mangle": opts.mangle, 54 | "compress": opts.compress, 55 | }; 56 | if(opts.hasOwnProperty('uglifyOptions')){ 57 | uglifyOptions = Object.assign({}, uglifyOptions, opts['uglifyOptions']); 58 | } 59 | let results = minjs.minify(content, uglifyOptions); 60 | if(results.error) throw new Error(results.error); 61 | 62 | let outFile = filename; 63 | if(!this.file){ 64 | const filePath: string = path.dirname(filename); 65 | const outExt = opts.outExt?opts.outExt:'.js'; 66 | const baseFilename: string = path.parse(filename).name; 67 | 68 | let outDir = filePath; 69 | if(opts.outDir){ 70 | outDir = Configuration.resolveFilePath(opts.outDir, filePath, filename); 71 | } 72 | outFile = path.resolve(outDir, baseFilename+outExt); 73 | } 74 | return writeFileContents(outFile, results.code); 75 | }).then(() => 76 | { 77 | let elapsedTime: number = (Date.now() - startTime); 78 | compilingMessage.dispose(); 79 | if(this.jsDiagnosticCollection) 80 | this.jsDiagnosticCollection.set(vscode.Uri.parse(filename), []); 81 | 82 | StatusBarMessage.show(`$(check) Js minified in ${elapsedTime}ms`, StatusBarMessageTypes.SUCCESS); 83 | 84 | callback(); 85 | }).catch((error: any) => 86 | { 87 | if(this.jsDiagnosticCollection){ 88 | 89 | let file:string; 90 | if(error.filename && filename != error.filename){ 91 | file = error.filename; 92 | }else{ 93 | file = filename; 94 | } 95 | 96 | StatusBarMessage.formatDiagnostic(this.jsDiagnosticCollection, file, [StatusBarMessage.getDiagnostic(error)]); 97 | 98 | }else{ 99 | vscode.window.showErrorMessage(error.message); 100 | } 101 | 102 | compilingMessage.dispose(); 103 | StatusBarMessage.show("$(alert) Error minify js (more detail in Errors and Warnings)", StatusBarMessageTypes.ERROR); 104 | 105 | callback(); 106 | }); 107 | } 108 | } 109 | 110 | 111 | function writeFileContents(this: void, filepath: string, content: any): Promise 112 | { 113 | return new Promise((resolve, reject) => 114 | { 115 | mkpath(path.dirname(filepath), err => 116 | { 117 | if (err) 118 | { 119 | return reject(err); 120 | } 121 | 122 | fs.writeFile(filepath, content, err => err ? reject(err) : resolve(filepath)); 123 | }); 124 | }); 125 | } 126 | 127 | function readFilePromise(this: void, filename: string): Promise 128 | { 129 | return new Promise((resolve, reject) => 130 | { 131 | fs.readFile(filename, (err: any, buffer: Buffer) => 132 | { 133 | if (err) 134 | { 135 | reject(err) 136 | } 137 | else 138 | { 139 | resolve(buffer); 140 | } 141 | }); 142 | }); 143 | } -------------------------------------------------------------------------------- /src/plugins/pluginCleanCss.ts: -------------------------------------------------------------------------------- 1 | var CleanCSS = require("clean-css"); 2 | 3 | function getCleanCSSProcessor(less) { 4 | function CleanCSSProcessor(options) { 5 | this.options = options || {}; 6 | } 7 | 8 | CleanCSSProcessor.prototype = { 9 | process: function (css, extra) { 10 | var options = this.options, 11 | sourceMap = extra.sourceMap, 12 | filename = '$stdin', 13 | sourceMapContent; 14 | 15 | if(extra.options && extra.options.rootFileInfo){ 16 | filename = extra.options.rootFileInfo.filename; 17 | } 18 | 19 | if (typeof options.keepSpecialComments === "undefined") { 20 | options.keepSpecialComments = "*"; 21 | } 22 | options.processImport = false; 23 | 24 | if (typeof options.rebase === "undefined") { 25 | options.rebase = false; 26 | } 27 | 28 | if (typeof options.advanced === "undefined") { 29 | options.advanced = false; 30 | } 31 | 32 | if (sourceMap) { 33 | sourceMapContent = sourceMap.getExternalSourceMap(); 34 | 35 | if (filename && sourceMapContent) { 36 | options.sourceMap = true; 37 | var sourceMapObj = JSON.parse(sourceMapContent); 38 | for (const key in sourceMapObj.sources) { 39 | if(sourceMapObj.sources[key] == filename){ 40 | sourceMapObj.sources[key] = '$stdin'; 41 | } 42 | } 43 | sourceMapContent = JSON.stringify(sourceMapObj); 44 | } 45 | } 46 | 47 | var output; 48 | if(options.sourceMap && sourceMapContent){ 49 | output = new CleanCSS(options).minify(css, sourceMapContent); 50 | }else{ 51 | output = new CleanCSS(options).minify(css); 52 | } 53 | 54 | if (options.sourceMap) { 55 | var sourceMapObj = JSON.parse(output.sourceMap); 56 | for (const key in sourceMapObj.sources) { 57 | if(sourceMapObj.sources[key] == '$stdin'){ 58 | sourceMapObj.sources[key] = filename; 59 | } 60 | } 61 | 62 | sourceMap.setExternalSourceMap(JSON.stringify(sourceMapObj)); 63 | } 64 | 65 | var css = output.styles; 66 | 67 | return css; 68 | } 69 | }; 70 | 71 | return CleanCSSProcessor; 72 | } 73 | 74 | function parseOptions(options) { 75 | if (typeof options === "string") { 76 | var cleanOptionArgs = options.split(" "); 77 | options = {}; 78 | 79 | for (var i = 0; i < cleanOptionArgs.length; i++) { 80 | var argSplit = cleanOptionArgs[i].split("="), 81 | argName = argSplit[0].replace(/^-+/, ""); 82 | 83 | switch (argName) { 84 | case "keep-line-breaks": 85 | case "b": 86 | options.keepBreaks = true; 87 | break; 88 | case "s0": 89 | options.keepSpecialComments = 0; 90 | break; 91 | case "s1": 92 | options.keepSpecialComments = 1; 93 | break; 94 | case "keepSpecialComments": 95 | var specialCommentOption:any = argSplit[1]; 96 | if (specialCommentOption !== "*") { 97 | specialCommentOption = Number(specialCommentOption); 98 | } 99 | options.keepSpecialComments = specialCommentOption; 100 | break; 101 | // for compatibility - does nothing 102 | case "skip-advanced": 103 | options.advanced = false; 104 | break; 105 | case "advanced": 106 | options.advanced = true; 107 | break; 108 | case "skip-rebase": 109 | options.rebase = false; 110 | break; 111 | case "rebase": 112 | options.rebase = true; 113 | break; 114 | case "skip-aggressive-merging": 115 | options.aggressiveMerging = false; 116 | break; 117 | case "skip-restructuring": 118 | options.restructuring = false; 119 | break; 120 | case "skip-shorthand-compacting": 121 | options.shorthandCompacting = false; 122 | break; 123 | case "c": 124 | case "compatibility": 125 | options.compatibility = argSplit[1]; 126 | break; 127 | case "rounding-precision": 128 | options.roundingPrecision = Number(argSplit[1]); 129 | break; 130 | default: 131 | throw new Error("unrecognised clean-css option '" + argSplit[0] + "'"); 132 | } 133 | } 134 | } 135 | return options; 136 | } 137 | 138 | function pluginCleanCss(options) { 139 | this.options = options; 140 | } 141 | 142 | pluginCleanCss.prototype = { 143 | install: function(less, pluginManager) { 144 | var CleanCSSProcessor = getCleanCSSProcessor(less); 145 | pluginManager.addPostProcessor(new CleanCSSProcessor(this.options)); 146 | }, 147 | setOptions: function(options) { 148 | this.options = parseOptions(options); 149 | }, 150 | minVersion: [2, 1, 0] 151 | }; 152 | 153 | module.exports = pluginCleanCss; -------------------------------------------------------------------------------- /src/plugins/pluginGroup.ts: -------------------------------------------------------------------------------- 1 | var grouper = require('group-css-media-queries'); 2 | 3 | function getGroupProcessor(less) { 4 | function GroupProcessor() { }; 5 | GroupProcessor.prototype = { 6 | process: function (css, extra) { 7 | return grouper(css); 8 | } 9 | }; 10 | 11 | return GroupProcessor; 12 | }; 13 | 14 | function pluginGroupMediaQuery() {} 15 | 16 | pluginGroupMediaQuery.prototype = { 17 | install: function(less, pluginManager) { 18 | var GroupProcessor = getGroupProcessor(less); 19 | pluginManager.addPostProcessor(new GroupProcessor()); 20 | }, 21 | printUsage: function () { 22 | }, 23 | minVersion: [2, 0, 0] 24 | }; 25 | 26 | module.exports = pluginGroupMediaQuery; -------------------------------------------------------------------------------- /src/plugins/pluginSass2Less.ts: -------------------------------------------------------------------------------- 1 | let replacements = function () { 2 | let results = [ 3 | { 4 | pattern: /@for\s([\w$]+)\sfrom\s([\w$]+)\s(through|to)\s(.*)\s\{((?:[^}{]+|\{(?:[^}{]+|\{[^}{]*\})*\})*)\}/gi, 5 | replacement: function(match, iterator, initial, through, to, body) { 6 | let operator = through === 'through' ? '<=' : '<' 7 | return `.for(${iterator}: ${initial}) when (${iterator} ${operator} ${to}) {` + 8 | `${body.replace(new RegExp('(?:\#{)?' + iterator + '}?', 'gi'), '@{' + iterator + '}')}` + 9 | ` .for((${iterator} + 1)); 10 | } 11 | .for();` 12 | }, 13 | order: 0 14 | }, 15 | { 16 | pattern: /@if\s([()\w\s$=> { 20 | return new Promise(resolve => { 21 | setTimeout(resolve, ms) 22 | }) 23 | } 24 | 25 | function checkDiagnostics(ext){ 26 | let diagnostics = vscode.languages.getDiagnostics(); 27 | 28 | assert.notEqual(diagnostics.length, 0, "Should have diagnostics"); 29 | diagnostics.forEach(diagnostic => { 30 | if(diagnostic[0].path.indexOf(ext)>0){ 31 | assert.equal(diagnostic[1].length, 0, JSON.stringify(diagnostic[1], null, 4)); 32 | } 33 | }); 34 | } 35 | 36 | //output 37 | suite('Extension Test Suite', () => { 38 | vscode.window.showInformationMessage('Start all tests.'); 39 | 40 | test('Minify JS File', async () => { 41 | const filePath = basePath+'/js/test.js'; 42 | await vscode.commands.executeCommand("easyCompile.minify", vscode.Uri.parse(filePath)); 43 | await sleep(2000); 44 | checkDiagnostics('.js'); 45 | assert.equal(fs.existsSync(basePath+'/output/js/test.min.js'), true, 'Minify JS file not exists'); 46 | assert.equal(fs.md5ForPath(basePath+'/output/js/test.min.js'), '59a9c639b7f0a44381f8c94c3558558d', 'Minify JS file incorrect'); 47 | 48 | }).timeout(5000); 49 | 50 | test('Minify CSS File', async () => { 51 | const filePath = basePath+'/css/test.css'; 52 | await vscode.commands.executeCommand("easyCompile.minify", vscode.Uri.parse(filePath)); 53 | await sleep(2000); 54 | checkDiagnostics('.css'); 55 | assert.equal(fs.existsSync(basePath+'/output/css/test.min.css'), true, 'Minify CSS file not exists'); 56 | assert.equal(fs.md5ForPath(basePath+'/output/css/test.min.css'), 'bc52f4f98586e5d077b8b6bd8ccafd19', 'Minify CSS file incorrect'); 57 | 58 | assert.equal(fs.existsSync(basePath+'/output/css/test.min.css.map'), true, 'CSS Map file not exists'); 59 | assert.equal(fs.md5ForPath(basePath+'/output/css/test.min.css.map'), '8254df72f787af6271f541d62f1a4178', 'CSS Map file incorrect'); 60 | }).timeout(5000); 61 | 62 | test('Compile Less File', async () => { 63 | const filePath = basePath+'/less/test.less'; 64 | await vscode.commands.executeCommand("easyCompile.compile", vscode.Uri.parse(filePath)); 65 | await sleep(3000); 66 | checkDiagnostics('.less'); 67 | assert.equal(fs.existsSync(basePath+'/output/less/test.css'), true, 'Less Compiled file not exists'); 68 | assert.equal(fs.md5ForPath(basePath+'/output/less/test.css'), '3b0fa3aec59cd488423941d5631fbd54', 'Less Compiled file incorrect'); 69 | 70 | assert.equal(fs.existsSync(basePath+'/output/less/test.css.map'), true, 'Less Map file not exists'); 71 | assert.equal(fs.md5ForPath(basePath+'/output/less/test.css.map'), 'a8852f90515c625f531a56c2561b11ff', 'Less Map file incorrect'); 72 | }).timeout(5000); 73 | 74 | test('Compile Scss File', async () => { 75 | const filePath = basePath+'/scss/parent.scss'; 76 | await vscode.commands.executeCommand("easyCompile.compile", vscode.Uri.parse(filePath)); 77 | await sleep(3000); 78 | checkDiagnostics('.scss'); 79 | assert.equal(fs.existsSync(basePath+'/output/scss/test.css'), true, 'Scss Compiled file not exists'); 80 | assert.equal(fs.md5ForPath(basePath+'/output/scss/test.css'), '56bb6d47f01e4fdfd5f68808c50088fe', 'Scss Compiled file incorrect'); 81 | 82 | assert.equal(fs.existsSync(basePath+'/output/scss/test.css.map'), true, 'Scss Map file not exists'); 83 | assert.equal(fs.md5ForPath(basePath+'/output/scss/test.css.map'), '401654ea3d7a2a2f244091e5865b98ea', 'Scss Map file incorrect'); 84 | 85 | assert.equal(fs.existsSync(basePath+'/output/scss/test2.css'), true, 'Scss Compiled file 2 not exists'); 86 | assert.equal(fs.md5ForPath(basePath+'/output/scss/test2.css'), '8a6b7cbe328051984978511c4c89b084', 'Scss Compiled file 2 incorrect'); 87 | 88 | assert.equal(fs.existsSync(basePath+'/output/scss/test2.css.map'), true, 'Scss Map file 2 not exists'); 89 | assert.equal(fs.md5ForPath(basePath+'/output/scss/test2.css.map'), 'd561b2ecece217ec04e590c5ef8c4f48', 'Scss Map file 2 incorrect'); 90 | }).timeout(5000); 91 | 92 | test('Compile TS File', async () => { 93 | const filePath = basePath+'/typescript/test.ts'; 94 | await vscode.commands.executeCommand("easyCompile.compile", vscode.Uri.parse(filePath)); 95 | await sleep(3500); 96 | checkDiagnostics('.ts'); 97 | assert.equal(fs.existsSync(basePath+'/output/ts/test.js'), true, 'TS Compiled file not exists'); 98 | assert.equal(fs.md5ForPath(basePath+'/output/ts/test.js'), '5314f8a256348360ed0617eb7e55bf45', 'TS Compiled file incorrect'); 99 | }).timeout(5000); 100 | }); 101 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | }); 10 | mocha.useColors(true); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | e(err); 34 | } 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /tests/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "easycompile.css": { 3 | "outDir": "${workspaceRoot}/output/css", 4 | "outExt": ".min.css", 5 | "sourceMap": true 6 | }, 7 | "easycompile.js": { 8 | "outDir": "${workspaceRoot}/output/js", 9 | "outExt": ".min.js" 10 | } 11 | } -------------------------------------------------------------------------------- /tests/css/test.css: -------------------------------------------------------------------------------- 1 | .test{ 2 | color: #000; 3 | } -------------------------------------------------------------------------------- /tests/js/test.js: -------------------------------------------------------------------------------- 1 | function test(){ 2 | return 'asd'; 3 | } 4 | var a = test(); 5 | console.log(a); -------------------------------------------------------------------------------- /tests/less/helpers/helpers.less: -------------------------------------------------------------------------------- 1 | //main: ../test.less 2 | @import './mixins'; 3 | @import './sub/variables.less'; 4 | @import './test.scss'; 5 | 6 | .helper { 7 | color: #001; 8 | } 9 | 10 | .scss2{ 11 | color: @test_color; 12 | } -------------------------------------------------------------------------------- /tests/less/helpers/mixins.less: -------------------------------------------------------------------------------- 1 | //main: helpers.less 2 | .mixins{ 3 | color: #002; 4 | } -------------------------------------------------------------------------------- /tests/less/helpers/sub/variables.less: -------------------------------------------------------------------------------- 1 | //main: ../helpers.less 2 | .test1{ 3 | color: #003; 4 | } -------------------------------------------------------------------------------- /tests/less/helpers/test.scss: -------------------------------------------------------------------------------- 1 | //out: false 2 | $test_color: #008; 3 | 4 | .scss{ 5 | color: $test_color; 6 | } -------------------------------------------------------------------------------- /tests/less/parent.less: -------------------------------------------------------------------------------- 1 | //main: test.less 2 | .parent{ 3 | color: #004; 4 | } -------------------------------------------------------------------------------- /tests/less/test.less: -------------------------------------------------------------------------------- 1 | //out: ../output/less/test.css, sourceMap: true, compress: true, autoprefixer: last 5 versions, groupmedia: true 2 | @import './helpers/helpers'; 3 | 4 | .test{ 5 | color: #000; 6 | flex: 1; 7 | } -------------------------------------------------------------------------------- /tests/scss/helpers/_helpers.scss: -------------------------------------------------------------------------------- 1 | //main: ../test.scss 2 | @import './mixins'; 3 | @import './sub/variables.scss'; 4 | 5 | .helper { 6 | color: #001; 7 | } -------------------------------------------------------------------------------- /tests/scss/helpers/_mixins.scss: -------------------------------------------------------------------------------- 1 | //main: _helpers.scss 2 | .mixins{ 3 | color: #002; 4 | } -------------------------------------------------------------------------------- /tests/scss/helpers/sub/_variables.scss: -------------------------------------------------------------------------------- 1 | //main: ../_helpers.scss 2 | .test1{ 3 | color: #003; 4 | } -------------------------------------------------------------------------------- /tests/scss/helpers/sub/test2.scss: -------------------------------------------------------------------------------- 1 | //out: ../../../output/scss/test2.css, sourceMap: true, compress: true, autoprefixer: last 5 versions, groupmedia: true 2 | @import '../helpers'; 3 | 4 | .test{ 5 | color: #000; 6 | flex: 1; 7 | } -------------------------------------------------------------------------------- /tests/scss/parent.scss: -------------------------------------------------------------------------------- 1 | //main: test.scss, main:helpers/sub/test2.scss 2 | .parent{ 3 | color: #004; 4 | } -------------------------------------------------------------------------------- /tests/scss/test.scss: -------------------------------------------------------------------------------- 1 | //out: ../output/scss/test.css, sourceMap: true, compress: true, autoprefixer: last 5 versions, groupmedia: true 2 | @import 'helpers/helpers'; 3 | @import 'parent'; 4 | 5 | .test{ 6 | color: #000; 7 | flex: 1; 8 | } -------------------------------------------------------------------------------- /tests/typescript/test.ts: -------------------------------------------------------------------------------- 1 | //outfile: ../output/ts/test.js, compress: true, target: es6, lib: es2018, lib: dom 2 | function testfun(){ 3 | return 'asd'; 4 | } 5 | var a = testfun(); 6 | console.log(a); 7 | 8 | class TestClass{ 9 | testCall() { 10 | return new Promise(resolve => { 11 | setTimeout(() => { 12 | resolve('test async'); 13 | }, 2000); 14 | }) 15 | } 16 | 17 | async testAwait() { 18 | let r = await this.testCall() 19 | console.log(r); 20 | } 21 | } 22 | 23 | var t = new TestClass(); 24 | t.testAwait(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out/src", 7 | "lib": [ 8 | "es6" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strictNullChecks": true, 13 | "skipLibCheck": true, 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "tests", 18 | ".vscode-test" 19 | ] 20 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const cp = require('child_process'); 5 | const fs = require('fs-plus'); 6 | 7 | function replaceContent(oldContent, newContent, oldFile, newFile){ 8 | let content = fs.readFileSync(oldFile, 'utf8'); 9 | content = content.replace(oldContent, newContent); 10 | fs.writeFileSync(newFile, content, 'utf8'); 11 | } 12 | 13 | function getEntry() { 14 | const entry = {}; 15 | fs.removeSync('out/node_modules'); 16 | const npmListRes = cp.execSync('npm list -only prod -json', { 17 | encoding: 'utf8' 18 | }); 19 | const mod = JSON.parse(npmListRes); 20 | const unbundledModule = { 21 | 'impor': ['out/index.js'], 22 | 'typescript':['lib'], 23 | 'sass.js':['dist/sass.sync.js'], 24 | 'uglify-js':['tools', 'lib'] 25 | }; 26 | for (const mod in unbundledModule) { 27 | unbundledModule[mod].push('package.json'); 28 | for (const sub of unbundledModule[mod]) { 29 | const p = mod + '/' + sub; 30 | if(fs.isDirectorySync('node_modules/' + p)){ 31 | fs.copySync('node_modules/' + p, 'out/node_modules/' + p) 32 | }else{ 33 | if(sub === 'dist/sass.sync.js'){ 34 | replaceContent('module.exports = factory()', 'module.exports = factory', 'node_modules/' + p, 'out/node_modules/' + p) 35 | }else{ 36 | fs.copyFileSync('node_modules/' + p, 'out/node_modules/' + p) 37 | } 38 | } 39 | } 40 | } 41 | 42 | const list = getDependeciesFromNpm(mod); 43 | const moduleList = list.filter((value, index, self) => { 44 | return self.indexOf(value) === index && !unbundledModule[value] && !/^@types\//.test(value); 45 | }); 46 | 47 | for (const mod of moduleList) { 48 | if(mod!='commander') entry[mod] = './node_modules/' + mod; 49 | } 50 | 51 | return entry; 52 | } 53 | 54 | function getDependeciesFromNpm(mod) { 55 | let list = []; 56 | const deps = mod.dependencies; 57 | if (!deps) { 58 | return list; 59 | } 60 | for (const m of Object.keys(deps)) { 61 | list.push(m); 62 | list = list.concat(getDependeciesFromNpm(deps[m])); 63 | } 64 | return list; 65 | } 66 | 67 | /**@type {import('webpack').Configuration}*/ 68 | const config = { 69 | target: 'node', 70 | node: { 71 | __dirname: false, 72 | __filename: false, 73 | }, 74 | entry: getEntry(), 75 | output: { 76 | path: path.resolve(__dirname, 'out/node_modules'), 77 | filename: '[name].js', 78 | libraryTarget: "commonjs2", 79 | devtoolModuleFilenameTemplate: "../[resource-path]", 80 | }, 81 | externals: { 82 | vscode: "commonjs vscode", 83 | commander: "commonjs commander" 84 | }, 85 | resolve: { 86 | extensions: ['.js', '.json'] 87 | } 88 | } 89 | 90 | module.exports = config; --------------------------------------------------------------------------------