Skip to content

Commit 080b627

Browse files
committedApr 2, 2017
start porting js-to-less-var-loader
0 parents  commit 080b627

17 files changed

+540
-0
lines changed
 

‎.babelrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["es2015"]
3+
}

‎.gitignore

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.DS_Store
2+
3+
# Logs
4+
logs
5+
*.log
6+
npm-debug.log*
7+
8+
# Runtime data
9+
pids
10+
*.pid
11+
*.seed
12+
13+
# Temporary files
14+
*.swp
15+
*.swo
16+
17+
# Directory for instrumented libs generated by jscoverage/JSCover
18+
lib-cov
19+
20+
# Coverage directory used by tools like istanbul
21+
coverage
22+
coverage.data
23+
.coveralls.yml
24+
25+
# Dependency directories
26+
node_modules
27+
28+
# Optional npm cache directory
29+
.npm
30+
31+
# Optional REPL history
32+
.node_repl_history

‎.travis.yml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
language: node_js
2+
node_js:
3+
- "4.0"
4+
script:
5+
- npm run test:coverage

‎README.md

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
![Build Status](https://travis-ci.org/tompascall/js-to-sass-var-loader.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/tompascall/js-to-sass-var-loader/badge.svg?branch=master)](https://coveralls.io/github/tompascall/js-to-sass-var-loader?branch=master)
2+
3+
## js-to-sass-var-loader
4+
5+
### A [Webpack]() loader to share data for sass variables between javascript modules and sass files
6+
7+
This loader is for that special case when you would like to import data from a javascript module into a sass file. The [sass loader](https://github.com/webpack-contrib/sass-loader) complains, because importing js module is not a valid sass instruction.
8+
9+
##### The loader only handles the case when you want to inject sass variables into a sass file via a javascript module.
10+
11+
#### Prerequisites
12+
13+
- Nodejs >= 4.0
14+
- [sass](http://sass-lang.com/) for css pre-processing
15+
- Webpack for module bundle
16+
17+
18+
#### Setting up Webpack config
19+
20+
Probably you use [sass-loader](https://github.com/webpack-contrib/sass-loader) with Webpack. The usage in this case is pretty simple: just put this loader before sass-loader in your webpack config:
21+
22+
```js
23+
{
24+
rules: [
25+
test: /\.sass$/,
26+
use: [
27+
{
28+
loader: "style-loader"
29+
},
30+
{
31+
loader: "css-loader"
32+
},
33+
{
34+
loader: "sass-loader"
35+
},
36+
{
37+
loader: "js-to-sass-var-loader"
38+
}
39+
]
40+
]
41+
}
42+
```
43+
44+
#### Usage
45+
46+
Let's assume we would like to store some variable data in a module like this:
47+
48+
```js
49+
// colors.js
50+
51+
const colors = {
52+
'fancy-white': '#FFFFFE',
53+
'fancy-black': '#000001'
54+
};
55+
56+
module.exports = colors;
57+
```
58+
59+
You can use this module in your favorite templates / frameworks etc., and you don't want to repeat yourself when using these colors in a sass file as variable (e.g. `$fancy-white: #FFFFFE; /*...*/ background-color: $fancy-white`). In this situation just require your module in the beginning of your sass module:
60+
```js
61+
require('relative/path/to/colors.js');
62+
63+
// ...
64+
.some-class {
65+
background-color: $fancy-white
66+
}
67+
// ...
68+
```
69+
70+
**The form of the required data is important**: it must be an object with key/values pair, the key will be the name of the sass variable.
71+
72+
#### Misc
73+
74+
You can use other require form (`require('relative/path/to/module').someProperty`), too.
75+
76+
#### Demo
77+
78+
You can try the loader via a small fake app in the `demo` folder:
79+
```sh
80+
cd demo
81+
npm i
82+
npm run demo
83+
```
84+
The webpack dev server serves the app on `localhost:8030`. In the app we share data between js and sass modules.
85+
86+
#### Development
87+
88+
Run tests with `npm test` or `npm run test:watch`.
89+
90+
The transformer is developed with tdd, so if you would like to contribute, please, write your tests for your new functionality, and send pull request to integrate your changes.

‎__tests__/index.spec.js

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import path from 'path';
2+
const loader = require('../index').default;
3+
const { operator } = require('../index');
4+
5+
describe('js-to-sass-vars-loader', () => {
6+
7+
describe('module', () => {
8+
const context = {
9+
context: path.resolve()
10+
};
11+
12+
it('exports a function', () => {
13+
expect(typeof loader).toEqual('function');
14+
});
15+
16+
it('calls operator.mergeVarsToContent with content and loader context', () => {
17+
spyOn(operator, 'mergeVarsToContent');
18+
loader.call(context, 'asdf');
19+
expect(operator.mergeVarsToContent).toHaveBeenCalledWith('asdf', context);
20+
21+
});
22+
23+
it('returns the result of mergeVarsToContent', () => {
24+
const content = 'require("./mocks/colors.js")\n' + '.someClass {\ncolor: $nice;\n}';
25+
const merged = operator.mergeVarsToContent(content, context);
26+
const result = loader.call(context, content);
27+
expect(result).toEqual(merged);
28+
});
29+
});
30+
31+
describe('divideContent', () => {
32+
it('divides the require (if it exists) from the content', () => {
33+
const content = "require('colors.js');\n" +
34+
".someClass { color: #fff;}";
35+
expect(operator.divideContent(content)[0]).toEqual("require('colors.js');");
36+
expect(operator.divideContent(content)[1]).toEqual("\n.someClass { color: #fff;}");
37+
});
38+
39+
it('gives back content if there is no require in content', () => {
40+
const content = ".someClass { color: #fff;}";
41+
expect(operator.divideContent(content)[0]).toEqual("");
42+
expect(operator.divideContent(content)[1]).toEqual(content);
43+
});
44+
45+
it('handles more requires when divide', () => {
46+
const content = "require('colors.js');\n" +
47+
"require('sizes.js');\n" +
48+
".someClass { color: #fff;}";
49+
expect(operator.divideContent(content)[0]).toEqual("require('colors.js');\n" + "require('sizes.js');");
50+
expect(operator.divideContent(content)[1]).toEqual("\n.someClass { color: #fff;}");
51+
});
52+
53+
it('handles the form of request("asdf").someProp', () => {
54+
const content = "require('corners.js').typeOne;\n" + ".someClass { color: #fff;}";
55+
expect(operator.divideContent(content)[0]).toEqual("require('corners.js').typeOne;");
56+
});
57+
});
58+
59+
describe('getModulePath', () => {
60+
it('extracts module paths and methodName into an array', () => {
61+
expect(operator.getModulePath('require("./mocks/colors.js");\n')).toEqual([{path: "./mocks/colors.js"}]);
62+
63+
expect(operator.getModulePath('require("./mocks/colors.js");\n' + 'require("./mocks/sizes.js");')).toEqual([{path: "./mocks/colors.js"}, {path:"./mocks/sizes.js"}]);
64+
65+
expect(operator.getModulePath('require("./mocks/corners.js").typeTwo;\n')).toEqual([{path: "./mocks/corners.js", methodName: 'typeTwo'}]);
66+
});
67+
});
68+
69+
describe('getVarData', () => {
70+
const context = { context: path.resolve()};
71+
72+
it('gets variable data by modulePath with context', () => {
73+
const varData = operator.getVarData([{path: './mocks/colors.js' }], context);
74+
expect(varData).toEqual({ white: '#fff', black: '#000'});
75+
});
76+
77+
it('merges module data if there are more requests', () => {
78+
const varData = operator.getVarData([{path:'./mocks/colors.js'}, {path:'./mocks/sizes.js'}], context);
79+
expect(varData).toEqual({ white: '#fff', black: '#000', small: '10px', large: '50px'});
80+
});
81+
82+
it('handles methodName if it is given', () => {
83+
const varData = operator.getVarData([{ path:'./mocks/corners.js', methodName: 'typeOne'}], context);
84+
expect(varData).toEqual({ tiny: '1%', medium: '3%'});
85+
});
86+
});
87+
88+
describe('transformToSassVars', () => {
89+
it('takes a hash object and transforms it to sass variables', () => {
90+
const colors = require('../mocks/colors.js');
91+
expect(operator.transformToSassVars(colors)).toEqual('$white: #fff;\n$black: #000;\n');
92+
});
93+
});
94+
95+
describe('mergeVarsToContent', () => {
96+
const context = {
97+
context: path.resolve()
98+
};
99+
100+
it('inserst vars to sass content', () => {
101+
const content = "require('./mocks/colors.js');\n" +
102+
".someClass { color: #fff;}";
103+
const [ moduleData, sassContent ] = operator.divideContent(content);
104+
const modulePath = operator.getModulePath(moduleData);
105+
const varData = operator.getVarData(modulePath, context);
106+
const sassVars = operator.transformToSassVars(varData);
107+
108+
expect(operator.mergeVarsToContent(content, context)).toEqual(sassVars + sassContent);
109+
});
110+
111+
it('gives back content as is if there is no requre', () => {
112+
const content = ".someClass { color: #fff;}";
113+
expect(operator.mergeVarsToContent(content, context)).toEqual(content);
114+
});
115+
});
116+
});

‎demo/colors.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
'fancy-red': '#db0f3b',
3+
'fancy-blue': '#0f9adb',
4+
'fancy-yellow': '#e5c63b',
5+
'fancy-green': '#4fdd59',
6+
'fancy-white': '#fcfff7',
7+
'fancy-black': '#1f2120',
8+
'fancy-pink': '#d326c8',
9+
'fancy-lilac': '#941ece'
10+
};

‎demo/index.html

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html>
2+
<head>
3+
<title>Loader Demo</title>
4+
<meta charset="UTF-8">
5+
</head>
6+
<body>
7+
<div id="selected-squares-container"></div>
8+
<span style="font-family: monospace; font-size: small">Choose color:</span>
9+
<select name="colors"></select>
10+
<button id="add-square-button">Add square</button>
11+
12+
<script src="bundle.js"></script>
13+
</body>
14+
</html>

‎demo/index.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import './style.scss';
2+
import colors from './colors';
3+
4+
const select = document.querySelector('select');
5+
const container = document.getElementById('selected-squares-container');
6+
7+
function createOptions () {
8+
const fragment = document.createDocumentFragment();
9+
for (let key in colors) {
10+
const option = document.createElement('option');
11+
option.textContent = key;
12+
option.setAttribute('value', colors[key]);
13+
fragment.appendChild(option);
14+
}
15+
select.appendChild(fragment);
16+
}
17+
18+
function createSquare (type) {
19+
const square = document.createElement('div');
20+
square.className = `square ${type}`;
21+
return square;
22+
}
23+
24+
function addSquare (e) {
25+
e.preventDefault();
26+
const type = select.options[select.selectedIndex].text;
27+
container.appendChild(createSquare(type));
28+
}
29+
30+
function setupButtonHandler () {
31+
document.getElementById('add-square-button')
32+
.addEventListener('click', addSquare);
33+
}
34+
35+
createOptions();
36+
setupButtonHandler();

‎demo/package.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "js-to-sass-var-loader-demo",
3+
"version": "1.0.1",
4+
"description": "Demo for js-to-sass-var-loader",
5+
"main": "index.js",
6+
"scripts": {
7+
"demo": "./node_modules/.bin/webpack-dev-server --config webpack.config.js"
8+
},
9+
"keywords": [
10+
"demo"
11+
],
12+
"author": "tompascall",
13+
"license": "ISC",
14+
"devDependencies": {
15+
"babel-core": "^6.24.0",
16+
"babel-loader": "^6.4.1",
17+
"babel-preset-env": "^1.3.2",
18+
"css-loader": "^0.28.0",
19+
"js-to-sass-var-loader": "^1.0.0",
20+
"node-sass": "^4.5.2",
21+
"sass-loader": "^6.0.3",
22+
"style-loader": "^0.16.1",
23+
"webpack": "^2.3.2",
24+
"webpack-dev-server": "^2.4.2"
25+
}
26+
}

‎demo/style.scss

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
require('./colors.js');
2+
3+
.square {
4+
width: 100px;
5+
height: 100px;
6+
display: inline-block;
7+
}
8+
9+
.fancy-blue {
10+
background-color: $fancy-blue;
11+
}
12+
13+
.fancy-red {
14+
background-color: $fancy-red;
15+
}
16+
17+
.fancy-yellow {
18+
background-color: $fancy-yellow;
19+
}
20+
21+
.fancy-green {
22+
background-color: $fancy-green;
23+
}
24+
25+
.fancy-white {
26+
background-color: $fancy-white;
27+
}
28+
29+
.fancy-black {
30+
background-color: $fancy-black;
31+
}
32+
33+
.fancy-lilac {
34+
background-color: $fancy-lilac;
35+
}
36+
37+
.fancy-pink {
38+
background-color: $fancy-pink;
39+
}
40+

‎demo/webpack.config.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const path = require('path');
2+
const webpack = require('webpack');
3+
4+
module.exports = {
5+
entry: './index.js',
6+
output: {
7+
filename: 'bundle.js',
8+
//path: path.resolve(__dirname)
9+
},
10+
module: {
11+
rules: [
12+
{ test: /\.(js)$/, use: 'babel-loader' },
13+
{
14+
test: /\.scss$/,
15+
use: [{
16+
loader: "style-loader"
17+
}, {
18+
loader: "css-loader"
19+
}, {
20+
loader: "sass-loader"
21+
},
22+
{
23+
loader: "js-to-sass-var-loader"
24+
}]
25+
}
26+
]
27+
},
28+
devServer: {
29+
inline: true,
30+
hot: true,
31+
port: 8030
32+
},
33+
plugins: [
34+
new webpack.HotModuleReplacementPlugin()
35+
]
36+
}

‎index.js

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const path = require('path');
2+
3+
const requireReg = /require\s*\(['|"](.+)['|"]\)(?:\.([^;\s]+))?[;\s]/g;
4+
5+
const operator = {
6+
divideContent (content) {
7+
let match;
8+
let lastIndex;
9+
const reg = new RegExp(requireReg);
10+
while (match = reg.exec(content)) {
11+
lastIndex = reg.lastIndex;
12+
}
13+
if (typeof lastIndex !== 'undefined') {
14+
return [
15+
content.slice(0, lastIndex),
16+
content.slice(lastIndex)
17+
];
18+
}
19+
else {
20+
return ['', content];
21+
}
22+
},
23+
24+
getModulePath (modulePart) {
25+
const reg = new RegExp(requireReg);
26+
const modulePaths = [];
27+
let match;
28+
while (match = reg.exec(modulePart)) {
29+
modulePaths.push({
30+
path: match[1],
31+
methodName: match[2]
32+
});
33+
}
34+
return modulePaths;
35+
},
36+
37+
getVarData (modulePath, webpackContext) {
38+
return modulePath.reduce( (accumulator, currentPath) => {
39+
const moduleData = (currentPath.methodName)? require(path.join(webpackContext.context, currentPath.path))[currentPath.methodName] : require(path.join(webpackContext.context, currentPath.path));
40+
return Object.assign(accumulator, moduleData);
41+
}, {});
42+
},
43+
44+
transformToSassVars (varData) {
45+
const keys = Object.keys(varData);
46+
return keys.reduce( (result, key) => {
47+
result += `$${key}: ${varData[key]};\n`;
48+
return result;
49+
}, '');
50+
},
51+
52+
mergeVarsToContent (content, webpackContext) {
53+
const [ moduleData, sassContent ] = this.divideContent(content);
54+
if (moduleData) {
55+
const modulePath = this.getModulePath(moduleData);
56+
const varData = this.getVarData(modulePath, webpackContext);
57+
const sassVars = this.transformToSassVars(varData);
58+
return sassVars + sassContent;
59+
}
60+
else return content;
61+
}
62+
};
63+
64+
exports.operator = operator;
65+
66+
const loader = function (content) {
67+
const webpackContext = this;
68+
const merged = operator.mergeVarsToContent(content, webpackContext);
69+
return merged;
70+
};
71+
72+
exports.default = loader;

‎jest.config

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"verbose": true
3+
}

‎mocks/colors.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const colors = {
2+
white: '#fff',
3+
black: '#000'
4+
};
5+
6+
module.exports = colors;

‎mocks/corners.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
exports.typeOne = {
2+
tiny: '1%',
3+
medium: '3%'
4+
};
5+
6+
exports.typeTwo = {
7+
tiny: '2%',
8+
medium: '5%'
9+
};

‎mocks/sizes.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
const sizes = {
3+
large: '50px',
4+
small: '10px'
5+
};
6+
7+
module.exports = sizes;

‎package.json

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "js-to-styles-var-loader",
3+
"version": "0.0.1",
4+
"description": "Webpack loader for sharing data amongst (sass || less) && javascript modules",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "jest --config jest.config",
8+
"test:coverage": "jest --config jest.config --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
9+
"test:watch": "jest --watch --config jest.config"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/tompascall/js-to-styles-var-loader.git"
14+
},
15+
"keywords": [
16+
"webpack",
17+
"loader",
18+
"sass",
19+
"less",
20+
"variables",
21+
"DRY"
22+
],
23+
"author": "tompascall",
24+
"license": "ISC",
25+
"bugs": {
26+
"url": "https://github.com/tompascall/js-to-styles-var-loader/issues"
27+
},
28+
"homepage": "https://github.com/tompascall/js-to-styles-var-loader#readme",
29+
"devDependencies": {
30+
"babel-jest": "^19.0.0",
31+
"babel-preset-es2015": "^6.24.0",
32+
"coveralls": "^2.12.0",
33+
"jest": "^19.0.2"
34+
}
35+
}

0 commit comments

Comments
 (0)
Please sign in to comment.