diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82bde64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +coverage/ +.nyc_output/ +yarn.lock +package-lock.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..47b3eaa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: node_js +node_js: + - 'node' + - '10' + - '8' +os: + - windows + - linux + - osx +after_success: + - 'npm run coveralls' \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..235d03c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2017-08-22 + +### Added + +- Everything \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4a74f3b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Dennis Schad + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..31499e0 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# rosid-handler-less + +[![Travis Build Status](https://travis-ci.org/freedeebee/rosid-handler-less.svg?branch=master)](https://travis-ci.org/freedeebee/rosid-handler-less) [![Coverage Status](https://coveralls.io/repos/github/freedeebee/rosid-handler-less/badge.svg?branch=master)](https://coveralls.io/github/freedeebee/rosid-handler-less?branch=master) [![Dependencies](https://david-dm.org/freedeebee/rosid-handler-less.svg)](https://david-dm.org/freedeebee/rosid-handler-less#info=dependencies) [![Greenkeeper badge](https://badges.greenkeeper.io/freedeebee/rosid-handler-less.svg)](https://greenkeeper.io/) + +A function that loads a LESS file, transforms it to CSS, adds vendor prefixes and minifies the output. + +## Install + +``` +npm install rosid-handler-less +``` + +## Usage + +### API + +```js +const handler = require('rosid-handler-less') + +handler('main.less').then((data) => {}) +handler('main.css', { optimize: true }).then((data) => {}) +``` + +### Rosid + +Add the following object to your `rosidfile.json`, `rosidfile.js` or [routes array](https://github.com/electerious/Rosid/blob/master/docs/Routes.md). `rosid-handler-less` will transform all matching LESS files in your source folder to CSS. + +```json +{ + "name" : "LESS", + "path" : "[^_]*.{css,less}*", + "handler" : "rosid-handler-less" +} +``` + +```less +/* main.less */ +.class { + color: white; +} +``` + +```css +/* main.css (output) */ +.class { color: white; } +``` + +## Parameters + +- `filePath` `{String}` Absolute path to file. +- `opts` `{?Object}` Options. + - `optimize` `{?Boolean}` - Optimize output. Defaults to `false`. + +## Returns + +- `{Promise}` The transformed file content. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c6f3771 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "rosid-handler-less", + "version": "1.0.0", + "authors": [ + "Dennis Schad " + ], + "description": "Load LESS and transform to CSS, add vendor prefixes and minify", + "main": "src/index.js", + "keywords": [ + "rosid", + "handler", + "transform", + "compile", + "less", + "postcss", + "autoprefixer", + "cssnano" + ], + "license": "MIT", + "homepage": "https://github.com/freedeebee/rosid-handler-less", + "repository": { + "type": "git", + "url": "https://github.com/freedeebee/rosid-handler-less.git" + }, + "files": [ + "src" + ], + "scripts": { + "coveralls": "nyc report --reporter=text-lcov | coveralls", + "test": "nyc node_modules/mocha/bin/_mocha" + }, + "dependencies": { + "autoprefixer": "^9.4.8", + "cssnano": "^4.1.10", + "less": "^3.10.2", + "postcss": "^7.0.14" + }, + "devDependencies": { + "chai": "^4.2.0", + "coveralls": "^3.0.3", + "fsify": "^3.0.0", + "mocha": "^6.0.0", + "nyc": "^14.0.0" + }, + "mocha": { + "timeout": "100000" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..5e70ca2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,69 @@ +'use strict' + +const util = require('util') +const path = require('path') +const fs = require('fs') +const less = require('./less') +const postcss = require('./postcss') + +/** + * Load LESS and transform to CSS, add vendor prefixes and minify. + * @public + * @param {String} filePath - Absolute path to file. + * @param {?Object} opts - Options. + * @returns {Promise} CSS. + */ +module.exports = async function(filePath, opts = {}) { + + if (typeof filePath !== 'string') throw new Error(`'filePath' must be a string`) + if (typeof opts !== 'object') throw new Error(`'opts' must be undefined or an object`) + + const folderPath = path.dirname(filePath) + + opts = Object.assign({ + optimize: false + }, opts) + + let output = null + + output = await util.promisify(fs.readFile)(filePath, 'utf8') + output = await less(folderPath, output, opts) + output = await postcss(filePath, output, opts) + + return output + +} + +/** + * Tell Rosid with which file extension it should load the file. + * @public + * @param {?Object} opts - Options. + * @returns {String} File extension. + */ +module.exports.in = function(opts) { + + return (opts != null && opts.in != null) ? opts.in : '.less' + +} + +/** + * Tell Rosid with which file extension it should save the file. + * @public + * @param {?Object} opts - Options. + * @returns {String} File extension. + */ +module.exports.out = function(opts) { + + return (opts != null && opts.out != null) ? opts.out : '.css' + +} + +/** + * Attach an array to the function, which contains a list of + * file patterns used by the handler. The array will be used by Rosid for caching purposes. + * @public + */ +module.exports.cache = [ + '**/*.less', + '**/*.css' +] \ No newline at end of file diff --git a/src/less.js b/src/less.js new file mode 100644 index 0000000..1bdbd6b --- /dev/null +++ b/src/less.js @@ -0,0 +1,31 @@ +'use strict' + +const util = require('util') +const less = require('less') + +/** + * Transform LESS to CSS. + * @public + * @param {String} folderPath - Path to the folder containing the LESS file. + * @param {String} str - LESS. + * @param {Object} opts - Optional options for the task. + * @returns {Promise} CSS. + */ +module.exports = async function(folderPath, str, opts) { + + // LESS can't handle empty files + if (str === '') return str + + // Dismiss sourceMap when output should be optimized + const sourceMap = opts.optimize !== true + + const result = await less.render(str, { + paths: [folderPath], + sourceMap: { + sourceMapFileInline: sourceMap, + } + }) + + return result.css + +} \ No newline at end of file diff --git a/src/postcss.js b/src/postcss.js new file mode 100644 index 0000000..d2cc54a --- /dev/null +++ b/src/postcss.js @@ -0,0 +1,35 @@ +'use strict' + +const postcss = require('postcss') +const autoprefixer = require('autoprefixer') +const cssnano = require('cssnano') + +/** + * Add vendor prefixes and minify CSS. + * @public + * @param {String} filePath - Absolute path to file. + * @param {String} str - CSS. + * @param {Object} opts - Optional options for the task. + * @returns {Promise} Vendor prefixed and minified CSS. + */ +module.exports = async function(filePath, str, opts) { + + // Dismiss sourceMap when output should be optimized + const sourceMap = opts.optimize !== true + + const result = await postcss([ + + autoprefixer({ remove: false }), + cssnano({ safe: true }) + + ]).process(str, { + + from: filePath, + to: filePath, + map: sourceMap + + }) + + return result.css + +} \ No newline at end of file diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..b232f85 --- /dev/null +++ b/test/index.js @@ -0,0 +1,162 @@ +'use strict' + +const os = require('os') +const assert = require('chai').assert +const uuid = require('uuid/v4') +const index = require('./../src/index') + +const fsify = require('fsify')({ + cwd: os.tmpdir() +}) + +describe('index()', function() { + + it('should return an error when called without a filePath', async function() { + + return index().then(() => { + + throw new Error('Returned without error') + + }, (err) => { + + assert.strictEqual(err.message, `'filePath' must be a string`) + + }) + + }) + + it('should return an error when called with invalid options', async function() { + + const structure = await fsify([ + { + type: fsify.FILE, + name: `${ uuid() }.less` + } + ]) + + return index(structure[0].name, '').then(() => { + + throw new Error('Returned without error') + + }, (err) => { + + assert.strictEqual(err.message, `'opts' must be undefined or an object`) + + }) + + }) + + it('should return an error when called with a fictive filePath', async function() { + + return index(`${ uuid() }.less`).then(() => { + + throw new Error('Returned without error') + + }, (err) => { + + assert.isNotNull(err) + assert.isDefined(err) + + }) + + }) + + it('should load LESS and transform it to CSS', async function() { + + const structure = await fsify([ + { + type: fsify.FILE, + name: `${ uuid() }.less`, + contents: '' + } + ]) + + const result = await index(structure[0].name) + + assert.include(result, 'sourceMappingURL') + + }) + + it('should load LESS and transform it to optimized CSS when optimization enabled', async function() { + + const structure = await fsify([ + { + type: fsify.FILE, + name: `${ uuid() }.less`, + contents: '' + } + ]) + + const result = await index(structure[0].name, { optimize: true }) + + assert.strictEqual(result, '') + + }) + + describe('.in()', function() { + + it('should be a function', function() { + + assert.isFunction(index.in) + + }) + + it('should return a default extension', function() { + + assert.strictEqual(index.in(), '.less') + + }) + + it('should return a default extension when called with invalid options', function() { + + assert.strictEqual(index.in(''), '.less') + + }) + + it('should return a custom extension when called with options', function() { + + assert.strictEqual(index.in({ in: '.css' }), '.css') + + }) + + }) + + describe('.out()', function() { + + it('should be a function', function() { + + assert.isFunction(index.in) + + }) + + it('should return a default extension', function() { + + assert.strictEqual(index.out(), '.css') + + }) + + it('should return a default extension when called with invalid options', function() { + + assert.strictEqual(index.out(''), '.css') + + }) + + it('should return a custom extension when called with options', function() { + + assert.strictEqual(index.out({ out: '.less' }), '.less') + + }) + + }) + + describe('.cache', function() { + + it('should be an array', function() { + + assert.isArray(index.cache) + + }) + + }) + +}) \ No newline at end of file diff --git a/test/less.js b/test/less.js new file mode 100644 index 0000000..bfcd1ad --- /dev/null +++ b/test/less.js @@ -0,0 +1,54 @@ +'use strict' + +const assert = require('chai').assert +const less = require('./../src/less') + +describe('less()', function() { + + it('should return an empty string when called with an empty LESS string', async function() { + + const input = '' + const result = await less('.', input, { optimize: false }) + + assert.strictEqual(result, input) + + }) + + it('should return an error when called with incorrect LESS', async function() { + + const input = 'test' + + return less('.', input, { optimize: false }).then(() => { + + throw new Error('Returned without error') + + }, (err) => { + + assert.isNotNull(err) + assert.isDefined(err) + + }) + + }) + + it('should return CSS with a source map when called with valid LESS', async function() { + + const input = '.test { color: black; }' + const result = await less('.', input, { optimize: false }) + + assert.isString(result) + assert.include(result, 'sourceMappingURL') + + }) + + it('should return CSS without a source map when called with valid LESS and optimization enabled', async function() { + + const input = '.test { color: black; }' + const result = await less('.', input, { optimize: true }) + + assert.isString(result) + assert.notInclude(result, 'sourceMappingURL') + + }) + +}) \ No newline at end of file diff --git a/test/postcss.js b/test/postcss.js new file mode 100644 index 0000000..4a4291d --- /dev/null +++ b/test/postcss.js @@ -0,0 +1,71 @@ +'use strict' + +const os = require('os') +const assert = require('chai').assert +const uuid = require('uuid/v4') +const postcss = require('./../src/postcss') + +const fsify = require('fsify')({ + cwd: os.tmpdir() +}) + +describe('postcss()', function() { + + it('should return an error when called with incorrect CSS', async function() { + + const structure = await fsify([ + { + type: fsify.FILE, + name: `${ uuid() }.css`, + contents: 'test' + } + ]) + + return postcss(structure[0].name, structure[0].contents, { optimize: false }).then(() => { + + throw new Error('Returned without error') + + }, (err) => { + + assert.isNotNull(err) + assert.isDefined(err) + + }) + + }) + + it('should return CSS with a source map when called with valid CSS', async function() { + + const structure = await fsify([ + { + type: fsify.FILE, + name: `${ uuid() }.css`, + contents: '.test { color: black; }' + } + ]) + + const result = await postcss(structure[0].name, structure[0].contents, { optimize: false }) + + assert.isString(result) + assert.include(result, 'sourceMappingURL') + + }) + + it('should return CSS without a source map when called with valid LESS and optimization enabled', async function() { + + const structure = await fsify([ + { + type: fsify.FILE, + name: `${ uuid() }.css`, + contents: '.test { color: black; }' + } + ]) + + const result = await postcss(structure[0].name, structure[0].contents, { optimize: true }) + + assert.isString(result) + assert.notInclude(result, 'sourceMappingURL') + + }) + +}) \ No newline at end of file