├── .npm └── plugin │ └── less-build-plugin │ ├── .gitignore │ ├── README │ └── npm-shrinkwrap.json ├── .travis.yml ├── .versions ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── package.js ├── plugin.js └── tests ├── foo.html ├── foo.less └── tests.js /.npm/plugin/less-build-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npm/plugin/less-build-plugin/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /.npm/plugin/less-build-plugin/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "less": { 4 | "version": "2.6.0", 5 | "dependencies": { 6 | "errno": { 7 | "version": "0.1.4", 8 | "dependencies": { 9 | "prr": { 10 | "version": "0.0.0" 11 | } 12 | } 13 | }, 14 | "graceful-fs": { 15 | "version": "3.0.8" 16 | }, 17 | "image-size": { 18 | "version": "0.3.5" 19 | }, 20 | "mime": { 21 | "version": "1.3.4" 22 | }, 23 | "mkdirp": { 24 | "version": "0.5.1", 25 | "dependencies": { 26 | "minimist": { 27 | "version": "0.0.8" 28 | } 29 | } 30 | }, 31 | "promise": { 32 | "version": "6.1.0", 33 | "dependencies": { 34 | "asap": { 35 | "version": "1.0.0" 36 | } 37 | } 38 | }, 39 | "request": { 40 | "version": "2.69.0", 41 | "dependencies": { 42 | "aws-sign2": { 43 | "version": "0.6.0" 44 | }, 45 | "aws4": { 46 | "version": "1.2.1", 47 | "dependencies": { 48 | "lru-cache": { 49 | "version": "2.7.3" 50 | } 51 | } 52 | }, 53 | "bl": { 54 | "version": "1.0.1", 55 | "dependencies": { 56 | "readable-stream": { 57 | "version": "2.0.5", 58 | "dependencies": { 59 | "core-util-is": { 60 | "version": "1.0.2" 61 | }, 62 | "inherits": { 63 | "version": "2.0.1" 64 | }, 65 | "isarray": { 66 | "version": "0.0.1" 67 | }, 68 | "process-nextick-args": { 69 | "version": "1.0.6" 70 | }, 71 | "string_decoder": { 72 | "version": "0.10.31" 73 | }, 74 | "util-deprecate": { 75 | "version": "1.0.2" 76 | } 77 | } 78 | } 79 | } 80 | }, 81 | "caseless": { 82 | "version": "0.11.0" 83 | }, 84 | "combined-stream": { 85 | "version": "1.0.5", 86 | "dependencies": { 87 | "delayed-stream": { 88 | "version": "1.0.0" 89 | } 90 | } 91 | }, 92 | "extend": { 93 | "version": "3.0.0" 94 | }, 95 | "forever-agent": { 96 | "version": "0.6.1" 97 | }, 98 | "form-data": { 99 | "version": "1.0.0-rc3", 100 | "dependencies": { 101 | "async": { 102 | "version": "1.5.2" 103 | } 104 | } 105 | }, 106 | "har-validator": { 107 | "version": "2.0.6", 108 | "dependencies": { 109 | "chalk": { 110 | "version": "1.1.1", 111 | "dependencies": { 112 | "ansi-styles": { 113 | "version": "2.1.0" 114 | }, 115 | "escape-string-regexp": { 116 | "version": "1.0.4" 117 | }, 118 | "has-ansi": { 119 | "version": "2.0.0", 120 | "dependencies": { 121 | "ansi-regex": { 122 | "version": "2.0.0" 123 | } 124 | } 125 | }, 126 | "strip-ansi": { 127 | "version": "3.0.0", 128 | "dependencies": { 129 | "ansi-regex": { 130 | "version": "2.0.0" 131 | } 132 | } 133 | }, 134 | "supports-color": { 135 | "version": "2.0.0" 136 | } 137 | } 138 | }, 139 | "commander": { 140 | "version": "2.9.0", 141 | "dependencies": { 142 | "graceful-readlink": { 143 | "version": "1.0.1" 144 | } 145 | } 146 | }, 147 | "is-my-json-valid": { 148 | "version": "2.12.4", 149 | "dependencies": { 150 | "generate-function": { 151 | "version": "2.0.0" 152 | }, 153 | "generate-object-property": { 154 | "version": "1.2.0", 155 | "dependencies": { 156 | "is-property": { 157 | "version": "1.0.2" 158 | } 159 | } 160 | }, 161 | "jsonpointer": { 162 | "version": "2.0.0" 163 | }, 164 | "xtend": { 165 | "version": "4.0.1" 166 | } 167 | } 168 | }, 169 | "pinkie-promise": { 170 | "version": "2.0.0", 171 | "dependencies": { 172 | "pinkie": { 173 | "version": "2.0.4" 174 | } 175 | } 176 | } 177 | } 178 | }, 179 | "hawk": { 180 | "version": "3.1.3", 181 | "dependencies": { 182 | "hoek": { 183 | "version": "2.16.3" 184 | }, 185 | "boom": { 186 | "version": "2.10.1" 187 | }, 188 | "cryptiles": { 189 | "version": "2.0.5" 190 | }, 191 | "sntp": { 192 | "version": "1.0.9" 193 | } 194 | } 195 | }, 196 | "http-signature": { 197 | "version": "1.1.1", 198 | "dependencies": { 199 | "assert-plus": { 200 | "version": "0.2.0" 201 | }, 202 | "jsprim": { 203 | "version": "1.2.2", 204 | "dependencies": { 205 | "extsprintf": { 206 | "version": "1.0.2" 207 | }, 208 | "json-schema": { 209 | "version": "0.2.2" 210 | }, 211 | "verror": { 212 | "version": "1.3.6" 213 | } 214 | } 215 | }, 216 | "sshpk": { 217 | "version": "1.7.3", 218 | "dependencies": { 219 | "asn1": { 220 | "version": "0.2.3" 221 | }, 222 | "dashdash": { 223 | "version": "1.12.2" 224 | }, 225 | "jsbn": { 226 | "version": "0.1.0" 227 | }, 228 | "tweetnacl": { 229 | "version": "0.13.3" 230 | }, 231 | "jodid25519": { 232 | "version": "1.0.2" 233 | }, 234 | "ecc-jsbn": { 235 | "version": "0.1.1" 236 | } 237 | } 238 | } 239 | } 240 | }, 241 | "is-typedarray": { 242 | "version": "1.0.0" 243 | }, 244 | "isstream": { 245 | "version": "0.1.2" 246 | }, 247 | "json-stringify-safe": { 248 | "version": "5.0.1" 249 | }, 250 | "mime-types": { 251 | "version": "2.1.9", 252 | "dependencies": { 253 | "mime-db": { 254 | "version": "1.21.0" 255 | } 256 | } 257 | }, 258 | "node-uuid": { 259 | "version": "1.4.7" 260 | }, 261 | "oauth-sign": { 262 | "version": "0.8.1" 263 | }, 264 | "qs": { 265 | "version": "6.0.2" 266 | }, 267 | "stringstream": { 268 | "version": "0.0.5" 269 | }, 270 | "tough-cookie": { 271 | "version": "2.2.1" 272 | }, 273 | "tunnel-agent": { 274 | "version": "0.4.2" 275 | } 276 | } 277 | }, 278 | "source-map": { 279 | "version": "0.4.4", 280 | "dependencies": { 281 | "amdefine": { 282 | "version": "1.0.0" 283 | } 284 | } 285 | } 286 | } 287 | }, 288 | "less-plugin-autoprefix": { 289 | "version": "1.5.1", 290 | "dependencies": { 291 | "autoprefixer": { 292 | "version": "6.3.1", 293 | "dependencies": { 294 | "postcss-value-parser": { 295 | "version": "3.2.3" 296 | }, 297 | "normalize-range": { 298 | "version": "0.1.2" 299 | }, 300 | "num2fraction": { 301 | "version": "1.2.2" 302 | }, 303 | "browserslist": { 304 | "version": "1.1.1" 305 | }, 306 | "caniuse-db": { 307 | "version": "1.0.30000403" 308 | } 309 | } 310 | }, 311 | "postcss": { 312 | "version": "5.0.14", 313 | "dependencies": { 314 | "supports-color": { 315 | "version": "3.1.2", 316 | "dependencies": { 317 | "has-flag": { 318 | "version": "1.0.0" 319 | } 320 | } 321 | }, 322 | "source-map": { 323 | "version": "0.5.3" 324 | }, 325 | "js-base64": { 326 | "version": "2.1.9" 327 | } 328 | } 329 | } 330 | } 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - "curl -L http://git.io/ejPSng | /bin/sh" -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | caching-compiler@1.0.0 9 | caching-html-compiler@1.0.2 10 | callback-hook@1.0.4 11 | check@1.1.0 12 | coffeescript@1.0.11 13 | ddp@1.2.2 14 | ddp-client@1.2.1 15 | ddp-common@1.2.2 16 | ddp-server@1.2.2 17 | deps@1.0.9 18 | diff-sequence@1.0.1 19 | ecmascript@0.1.6 20 | ecmascript-runtime@0.2.6 21 | ejson@1.0.7 22 | geojson-utils@1.0.4 23 | grove:less@0.2.0 24 | html-tools@1.0.5 25 | htmljs@1.0.5 26 | id-map@1.0.4 27 | jquery@1.11.4 28 | local-test:grove:less@0.2.0 29 | logging@1.0.8 30 | meteor@1.1.10 31 | minifiers@1.1.7 32 | minimongo@1.0.10 33 | mongo@1.1.3 34 | mongo-id@1.0.1 35 | npm-mongo@1.4.39_1 36 | observe-sequence@1.0.7 37 | ordered-dict@1.0.4 38 | practicalmeteor:chai@1.9.2_3 39 | practicalmeteor:loglevel@1.1.0_3 40 | practicalmeteor:munit@2.1.2 41 | practicalmeteor:sinon@1.10.3_2 42 | promise@0.5.1 43 | random@1.0.5 44 | reactive-var@1.0.6 45 | retry@1.0.4 46 | routepolicy@1.0.6 47 | spacebars@1.0.7 48 | spacebars-compiler@1.0.7 49 | templating@1.1.5 50 | templating-tools@1.0.0 51 | test-helpers@1.0.5 52 | tinytest@1.0.6 53 | tracker@1.0.9 54 | ui@1.0.8 55 | underscore@1.0.4 56 | webapp@1.2.3 57 | webapp-hashing@1.0.5 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.2.0 2 | * Now works with Windows filepaths thanks to @AirBorne04 3 | * Bumps `less` version from 2.4.0 to 2.6.0 4 | * Bumps `less-plugin-autoprefix` from 1.4.1 to 1.5.1 5 | 6 | ### v0.1.0 7 | * Initial release 8 | * Has a test, but doesn't have tests covering usage with a configuration file, i.e. the main use case. 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Grove Labs 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor Less Build Plugin 2 | 3 | This a build plugin for [Less](http://lesscss.org) for Meteor that replaces the `less` package [suggested by MDG](http://docs.meteor.com/#/full/less) with one where you can control load order and run [Autoprefixer](https://github.com/postcss/autoprefixer) on the output CSS to get all of your needed vendor prefixes. 4 | 5 | ## Why 6 | Less is an awesome CSS preprocessor. It has variables, functions (mixins), and importing — all things needed to be able to write modular, reusable stylesheets. But the behavior of the MDG Less package to process every `.less` file independently makes it very tough to implement modular styles across your application. Given an application like such: 7 | 8 | ```js 9 | /client/stylesheets/globals/body.less // Global styling 10 | /client/stylesheets/vendor/normalize.less // Reset stylesheet 11 | /client/stylesheets/classes.less // Normal CSS class declarations 12 | /client/stylesheets/variables.less // Shared theme variables 13 | /client/stylesheets/mixins.less // Helpful mixins 14 | /client/templates/awesome-widget/awesome-widget.less 15 | ``` 16 | 17 | The order you would want to load those files in is 18 | 19 | 1. `normalize.less` — reset the styles 20 | 2. `variables.less`, `mixins.less`, and `classes.less` — setup your toolkit 21 | 3. `body.less` — set your global state 22 | 4. `awesome-widget.less` — bring in your components 23 | 24 | With the default `less` package, if you wanted to control the order in which your application stylesheets are loaded in you would have to make every Less file have an extension of `.import.less` and have one plain `.less` file where you explicitly `@import` every stylesheet you want. This package basically automates that process, but instead of naming every file except one with a special name, just one has a special name — `main.less` (changeable with the `indexFilePath` option). 25 | 26 | 27 | ## Usage 28 | * In a Meteor application directory, in your terminal enter `meteor add grove:less`. Without any additional configuration , this package will automatically parse all `.less` files, compile them with Less, and add them to the client CSS bundle. Same behavior as with the MDG Less package. 29 | * Create `config/less.json`. Here's a template: 30 | 31 | ```js 32 | { 33 | "enableAutoprefixer": true, // defaults to false 34 | "autoprefixerOptions": { 35 | "browsers": ["> 2%", "last 2 versions"], 36 | "cascade": true 37 | }, 38 | "useIndex": true, // defaults to false 39 | "indexFilePath": "client/stylesheets/main.less" // defaults ./client/main.less 40 | } 41 | 42 | ``` 43 | * Run `meteor` — a file will be created at the path specified in the `indexFilePath` with all of your Less files expliclity imported. This is the only file that will actually be compiled by Less. It will never be overwritten, only appended to. Order the imports as you see fit. If a file is deleted or moved you will have to manually delete that now error-throwing import. 44 | 45 | ### Autoprefixer 46 | Autoprefixer is ran with the [Less plugin](https://github.com/less/less-plugin-autoprefix). See the [Autoprefixer docs](https://github.com/postcss/autoprefixer#browsers) for more information on the options you can set for that. 47 | 48 | ## Testing 49 | 50 | ```sh 51 | git clone https://github.com/grovelabs/meteor-less 52 | cd meteor-less 53 | meteor test-packages ./ 54 | # App running at http://localhost:3000 55 | ``` 56 | 57 | Open localhost:3000 in your browser and watch the tests run in the TinyTest HTML runner. The test Meteor process will not end on its own, you must kill it yourself (Ctrl+C) in your terminal to end the process. 58 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "grove:less", 3 | summary: "A better Less package. Everything the original does plus ordered imports and Autoprefixer", 4 | version: "0.2.0", 5 | git: 'https://github.com/grovelabs/meteor-less.git' 6 | }); 7 | 8 | Package.registerBuildPlugin({ 9 | name: 'less-build-plugin', 10 | use: [], 11 | sources: [ 12 | 'plugin.js', 13 | ], 14 | npmDependencies: { 15 | "less": "2.6.0", 16 | "less-plugin-autoprefix": "1.5.1" 17 | } 18 | }); 19 | 20 | Package.onTest(function (api) { 21 | api.use([ 22 | 'grove:less', 23 | 'practicalmeteor:munit', 24 | 'templating' 25 | ]); 26 | api.addFiles([ 27 | 'tests/foo.html', 28 | 'tests/foo.less', 29 | 'tests/tests.js', 30 | ], 'client'); 31 | }); 32 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | var fs = Npm.require('fs'); 2 | var less = Npm.require('less'); 3 | var path = Npm.require('path'); 4 | var LessPluginAutoPrefix = Npm.require('less-plugin-autoprefix'); 5 | 6 | var DEFAULT_INDEX_FILE_PATH = "./client/main.less"; 7 | var OPTIONS_FILE = "config/less.json"; 8 | 9 | var generatedMessage = [ 10 | "// This file is auto generated by the grove:less package", 11 | "// New .less files will be automatically imported at the bottom", 12 | "// Existing contents will not be touched", 13 | "// When you delete a less file you must manually delete the import from here as well", 14 | "", 15 | "" 16 | ].join("\n"); 17 | 18 | var loadJSONFile = function (compileStep, filePath) { 19 | try { 20 | return JSON.parse( fs.readFileSync(filePath) ); 21 | } catch (e) { 22 | compileStep.error({ 23 | message: "Failed to parse " + filePath + " as JSON" 24 | }); 25 | return {}; 26 | } 27 | }; 28 | 29 | var convertToPosixPath = function(filePath) { 30 | return filePath.split(path.sep).join('/'); 31 | }; 32 | 33 | Plugin.registerSourceHandler("less", {archMatching: 'web'}, function (compileStep) { 34 | // Reading in user configuration 35 | var config = {}; 36 | if ( fs.existsSync(OPTIONS_FILE) ) { 37 | config = loadJSONFile(compileStep, OPTIONS_FILE); 38 | } 39 | 40 | // Load order setup 41 | if ( config.useIndex ) { 42 | var indexFilePath = config.indexFilePath || DEFAULT_INDEX_FILE_PATH, 43 | posixInputPath = convertToPosixPath(compileStep.inputPath); 44 | // If this isn't the index file, add it to the index if need be 45 | if ( posixInputPath != indexFilePath ) { 46 | if ( fs.existsSync(indexFilePath) ) { 47 | var lessIndex = fs.readFileSync(indexFilePath, 'utf8'); 48 | if ( lessIndex.indexOf(posixInputPath) == -1 ) { 49 | fs.appendFileSync(indexFilePath, '\n@import "' + posixInputPath + '";', 'utf8'); 50 | } 51 | } else { 52 | var newFile = generatedMessage + '@import "' + posixInputPath + '";\n'; 53 | fs.writeFileSync(indexFilePath, newFile, 'utf8'); 54 | } 55 | return; // stop here, only compile the index file 56 | } 57 | } 58 | 59 | // Autoprefixing setup 60 | var autoprefixer; 61 | if ( config.enableAutoprefixer ) { 62 | if ( config.autoprefixerOptions ) { 63 | autoprefixer = new LessPluginAutoPrefix(config.autoprefixerOptions); 64 | } else { 65 | autoprefixer = new LessPluginAutoPrefix(); 66 | } 67 | } 68 | plugins = []; 69 | if (autoprefixer) plugins.push(autoprefixer); 70 | 71 | 72 | // Compiler options 73 | var options = { 74 | syncImport: true, 75 | paths: config.includePaths || [], 76 | plugins: plugins 77 | }; 78 | 79 | var source = compileStep.read().toString('utf8'); 80 | less.render(source, options, function(error, output) { 81 | if (error) { 82 | compileStep.error({ 83 | message: "Less compiler error: " + error.message, 84 | sourcePath: error.filename || compileStep.inputPath, 85 | line: error.line, 86 | column: error.column 87 | }); 88 | } else { 89 | compileStep.addStylesheet({ 90 | path: compileStep.inputPath + ".css", 91 | data: output.css, 92 | sourceMap: output.map 93 | }); 94 | } 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /tests/foo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/foo.less: -------------------------------------------------------------------------------- 1 | @width: 100px; 2 | div { 3 | // test basic nested behavior 4 | #something { 5 | width: @width; 6 | height: 100px; 7 | } 8 | } -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | describe('Less plugin tests', function(){ 2 | var div; 3 | beforeAll( function(test) { 4 | div = document.createElement('div'); 5 | document.body.appendChild(div); 6 | Blaze.render(Template.foo, div); 7 | }); 8 | 9 | afterAll( function(test) { 10 | document.body.removeChild(div); 11 | }); 12 | 13 | it('Simple loading and Less functionality', function(test, waitFor){ 14 | var h1 = div.querySelector('h1'); 15 | test.equal(getComputedStyle(h1).width, "100px"); 16 | }); 17 | 18 | // XX How do I write tests that need the configuration json files present to 19 | // be tested? Which is basically all of the tests that actually matter here 20 | 21 | }); --------------------------------------------------------------------------------