Skip to content

Commit 7444028

Browse files
authored
Merge pull request tompascall#6 from dhharker/master
Some fixes and add support for importing from nested properties and block scoped imports
2 parents 88e4332 + 1829fd7 commit 7444028

17 files changed

+344
-110
lines changed

README.md

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This loader is for that special case when you would like to import data from a j
1818

1919
Probably you use [sass-loader](https://github.com/webpack-contrib/sass-loader) or [less-loader](https://github.com/webpack-contrib/less-loader) with Webpack. The usage in this case is pretty simple: just put the js-to-styles-var-loader before the sass-loader / less-loader in your webpack config:
2020

21-
For sass-loader:
21+
For sass-loader:
2222
```js
2323
{
2424
rules: [
@@ -41,7 +41,7 @@ For sass-loader:
4141
}
4242
```
4343

44-
For less-loader:
44+
For less-loader:
4545

4646
```js
4747
{
@@ -80,7 +80,7 @@ module.exports = colors;
8080
```
8181

8282
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:
83-
```js
83+
```sass
8484
require('relative/path/to/colors.js');
8585
8686
// ...
@@ -92,7 +92,7 @@ require('relative/path/to/colors.js');
9292

9393
**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 variable.
9494

95-
The js-to-styles-var-loader transforms this sass file and provides it in the following form for the sass-loader:
95+
The js-to-styles-var-loader transforms this sass file and provides it in the following form for the sass-loader:
9696

9797
```js
9898
$fancy-white: #FFFFFE;
@@ -105,11 +105,50 @@ $fancy-black: #000001;
105105

106106
#### Misc
107107

108-
You can use other require forms (`require('relative/path/to/module').someProperty`), too.
108+
You can import from named exports and properties of those, although the value of these must still be a flat list:
109+
110+
```js
111+
// themes.js
112+
113+
module.exports = {
114+
themes: {
115+
blue_theme: {
116+
some_color: "#00f"
117+
},
118+
red_theme: {
119+
some_color: "#f00"
120+
}
121+
},
122+
default: {
123+
some_color: "#aaa"
124+
}
125+
};
126+
```
127+
128+
Variables definitions are inserted into your sass/less file in place of the `require()` statement, so you can override variables inside blocks:
129+
130+
```less
131+
132+
require("themes.js").default;
133+
134+
.someThing {
135+
color: @some_color;
136+
}
137+
138+
.theme-blue {
139+
require("themes.js").themes.blue_theme;
140+
141+
.someThing {
142+
color: @some_color;
143+
}
144+
}
145+
146+
147+
```
109148

110149
#### Demo
111150

112-
You can try the loader via a small fake app in the `demo` folder:
151+
You can try the loader via a small fake app in the `demo` folder:
113152
```sh
114153
cd demo
115154
npm i
@@ -119,6 +158,6 @@ The webpack dev server serves the app on `localhost:8030`. In the app we share d
119158

120159
#### Development
121160

122-
Run tests with `npm test` or `npm run test:watch`.
161+
Run tests with `npm test` or `npm run test:watch`.
123162

124163
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

Lines changed: 141 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from 'path';
2+
import fs from 'fs';
23
const loader = require('../index').default;
34
jest.mock('decache');
45
const { operator } = require('../index');
@@ -22,7 +23,6 @@ describe('js-to-styles-vars-loader', () => {
2223
spyOn(operator, 'getResource').and.callThrough();
2324
loader.call(context, 'asdf');
2425
expect(operator.getResource).toHaveBeenCalledWith(context);
25-
2626
});
2727

2828
it('calls getPreprocessorType with resource', () => {
@@ -53,42 +53,65 @@ describe('js-to-styles-vars-loader', () => {
5353
});
5454
});
5555

56-
describe('divideContent', () => {
57-
it('divides the require (if it exists) from the content', () => {
58-
const content = "require('colors.js');\n" +
59-
".someClass { color: #fff;}";
60-
expect(operator.divideContent(content)[0]).toEqual("require('colors.js');");
61-
expect(operator.divideContent(content)[1]).toEqual("\n.someClass { color: #fff;}");
62-
});
63-
64-
it('gives back content if there is no require in content', () => {
65-
const content = ".someClass { color: #fff;}";
66-
expect(operator.divideContent(content)[0]).toEqual("");
67-
expect(operator.divideContent(content)[1]).toEqual(content);
68-
});
69-
70-
it('handles more requires when divide', () => {
71-
const content = "require('colors.js');\n" +
72-
"require('sizes.js');\n" +
73-
".someClass { color: #fff;}";
74-
expect(operator.divideContent(content)[0]).toEqual("require('colors.js');\n" + "require('sizes.js');");
75-
expect(operator.divideContent(content)[1]).toEqual("\n.someClass { color: #fff;}");
76-
});
77-
78-
it('handles the form of request("asdf").someProp', () => {
79-
const content = "require('corners.js').typeOne;\n" + ".someClass { color: #fff;}";
80-
expect(operator.divideContent(content)[0]).toEqual("require('corners.js').typeOne;");
81-
});
82-
});
83-
84-
describe('getModulePath', () => {
85-
it('extracts module paths and methodName into an array', () => {
86-
expect(operator.getModulePath('require("./mocks/colors.js");\n')).toEqual([{path: "./mocks/colors.js"}]);
87-
88-
expect(operator.getModulePath('require("./mocks/colors.js");\n' + 'require("./mocks/sizes.js");')).toEqual([{path: "./mocks/colors.js"}, {path:"./mocks/sizes.js"}]);
89-
90-
expect(operator.getModulePath('require("./mocks/corners.js").typeTwo;\n')).toEqual([{path: "./mocks/corners.js", methodName: 'typeTwo'}]);
91-
});
56+
describe('validation', () => {
57+
it ("validateExportType() throws on anything except an object, does not throw otherwise", () => {
58+
const areOk = [{}, {a: "foo"}];
59+
const areNotOk = [[], ["a"], "", "123", 123, false, true, null, undefined, NaN];
60+
expect(() => {
61+
for (const okThing of areOk) {
62+
operator.validateExportType(okThing, "");
63+
}
64+
}).not.toThrow();
65+
for (const notOkThing of areNotOk) {
66+
expect(() => {
67+
operator.validateExportType(notOkThing, "");
68+
console.error(`Should have thrown on ${typeof notOkThing} '${notOkThing}'`);
69+
},).toThrow();
70+
71+
}
72+
})
73+
74+
it ("validateVariablesValue() throws on nested objects or invalid object property values", () => {
75+
const areOk = [
76+
{a: "foo"},
77+
{a: 100.1},
78+
{a: 100},
79+
{a: ""},
80+
{a: 0},
81+
{a: 0},
82+
{},
83+
];
84+
const areNotOk = [
85+
"",
86+
100.1,
87+
[],
88+
null,
89+
undefined,
90+
{a: 1/"bad"},
91+
{b: []},
92+
{c: ["bad"]},
93+
{d: [100.1]},
94+
{e: () => "bad"},
95+
{f: {}},
96+
{g: { b: "bad"} },
97+
{h: "foo", b: {}},
98+
{i: "foo", b: { c: "bad" }},
99+
{j: "foo", b: { c: 100}}
100+
];
101+
expect(() => {
102+
for (const okThing of areOk) {
103+
operator.validateVariablesValue(okThing, "");
104+
}
105+
}).not.toThrow();
106+
for (const notOkThing of areNotOk) {
107+
expect(() => {
108+
operator.validateVariablesValue(notOkThing, "", "nofile.js");
109+
operator.validateVariablesValue(notOkThing, "some.thing", "nofile.js");
110+
console.error(`Should have thrown on ${typeof notOkThing} '${JSON.stringify(notOkThing)}'`);
111+
},).toThrow();
112+
113+
}
114+
})
92115
});
93116

94117
describe('getVarData', () => {
@@ -98,26 +121,63 @@ describe('js-to-styles-vars-loader', () => {
98121
};
99122

100123
it('gets variable data by modulePath with context', () => {
101-
const varData = operator.getVarData([{path: './mocks/colors.js' }], context);
124+
const varData = operator.getVarData(path.join(context.context, './mocks/colors.js'));
102125
expect(varData).toEqual({ white: '#fff', black: '#000'});
103126
});
104127

105-
it('merges module data if there are more requests', () => {
106-
const varData = operator.getVarData([{path:'./mocks/colors.js'}, {path:'./mocks/sizes.js'}], context);
107-
expect(varData).toEqual({ white: '#fff', black: '#000', small: '10px', large: '50px'});
108-
});
109-
110-
it('handles methodName if it is given', () => {
111-
const varData = operator.getVarData([{ path:'./mocks/corners.js', methodName: 'typeOne'}], context);
128+
it('uses value from property', () => {
129+
const varData = operator.getVarData(path.join(context.context, './mocks/corners.js'), 'typeOne');
112130
expect(varData).toEqual({ tiny: '1%', medium: '3%'});
113131
});
114-
115-
it('call context.addDependecy with modulePath', () => {
116-
spyOn(context, 'addDependency');
117-
const relativePath = './mocks/corners.js';
118-
operator.getVarData([{ path: relativePath, methodName: 'typeOne'}], context);
119-
expect(context.addDependency).toHaveBeenCalledWith(path.resolve(relativePath));
132+
it('uses value from nested property', () => {
133+
const varData = operator.getVarData(path.join(context.context, './mocks/corners.js'), 'deep.nested');
134+
expect(varData).toEqual({ color: '#f00'});
120135
});
136+
137+
it('throws on an missing module', () => {
138+
expect(() => {
139+
operator.getVarData(path.join(context.context, './mocks/this_is_not_an_existing_file.js'));
140+
}).toThrow();
141+
})
142+
it('throws on a non-object export', () => {
143+
expect(() => {
144+
operator.getVarData(path.join(context.context, './mocks/null_export.js'));
145+
}).toThrow();
146+
})
147+
148+
149+
150+
it('throws on an empty property', () => {
151+
expect(() => {
152+
operator.getVarData(path.join(context.context, './mocks/bad_exports.js'), 'empty');
153+
}).toThrow();
154+
expect(() => {
155+
operator.getVarData(path.join(context.context, './mocks/bad_exports.js'), 'notEmptyObject');
156+
}).not.toThrow();
157+
})
158+
159+
it('does not throw on an empty object', () => {
160+
expect(() => {
161+
operator.getVarData(path.join(context.context, './mocks/bad_exports.js'), 'emptyObject');
162+
}).not.toThrow();
163+
})
164+
165+
it('throws on a non-object property', () => {
166+
expect(() => {
167+
operator.getVarData(path.join(context.context, './mocks/bad_exports.js'), 'falsey');
168+
}).toThrow();
169+
expect(() => {
170+
operator.getVarData(path.join(context.context, './mocks/bad_exports.js'), 'truthy');
171+
}).toThrow();
172+
expect(() => {
173+
operator.getVarData(path.join(context.context, './mocks/bad_exports.js'), 'emptyArray');
174+
}).toThrow();
175+
expect(() => {
176+
operator.getVarData(path.join(context.context, './mocks/bad_exports.js'), 'nonEmptyArray');
177+
}).toThrow();
178+
179+
})
180+
121181
});
122182

123183
describe('transformToSassVars', () => {
@@ -139,22 +199,43 @@ describe('js-to-styles-vars-loader', () => {
139199
context: path.resolve(),
140200
addDependency () {}
141201
};
202+
const content = "require('./mocks/colors.js');\n" +
203+
".someClass { color: #fff; }";
142204

143205
it('inserts vars to styles content', () => {
144-
const content = "require('./mocks/colors.js');\n" +
145-
".someClass { color: #fff;}";
146-
const [ moduleData, stylesContent ] = operator.divideContent(content);
147-
const modulePath = operator.getModulePath(moduleData);
148-
const varData = operator.getVarData(modulePath, context);
149-
const vars = operator.transformToStyleVars({ type: 'less', varData });
206+
operator.mergeVarsToContent(content, context, 'less')
150207

151-
expect(operator.mergeVarsToContent(content, context, 'less')).toEqual(vars + stylesContent);
208+
expect(operator.mergeVarsToContent(content, context, 'less'))
209+
.toEqual("@white: #fff;\n@black: #000;\n\n.someClass { color: #fff; }");
152210
});
153211

154-
it('gives back content as is if there is no requre', () => {
212+
it('call context.addDependecy', () => {
213+
spyOn(context, 'addDependency');
214+
const dependencyPath = path.join(context.context, './mocks/colors.js');
215+
operator.mergeVarsToContent(content, context, 'less')
216+
expect(context.addDependency).toHaveBeenCalledWith(path.resolve(dependencyPath));
217+
});
218+
219+
it('gives back content as-is if there is no require', () => {
155220
const content = ".someClass { color: #fff;}";
156221
expect(operator.mergeVarsToContent(content, context)).toEqual(content);
157222
});
223+
224+
it("inserts variables inside style blocks and does not fail if the last 'require' is inside a block", () => {
225+
const content = fs.readFileSync(path.resolve('./mocks/case1.less'), 'utf8');
226+
const expectedContent = fs.readFileSync(path.resolve('./mocks/case1_expected.less'), 'utf8');
227+
const merged = operator.mergeVarsToContent(content, {...context, context: path.resolve('./mocks/')}, 'less');
228+
expect(merged.trim()).toEqual(expectedContent.trim());
229+
})
230+
231+
it("imports nested props", () => {
232+
const content = fs.readFileSync(path.resolve('./mocks/case2.less'), 'utf8');
233+
const expectedContent = fs.readFileSync(path.resolve('./mocks/case2_expected.less'), 'utf8');
234+
const merged = operator.mergeVarsToContent(content, {...context, context: path.resolve('./mocks/')}, 'less');
235+
expect(merged.trim()).toEqual(expectedContent.trim());
236+
})
237+
238+
158239
});
159240

160241
describe('getResource', () => {
@@ -173,6 +254,7 @@ describe('js-to-styles-vars-loader', () => {
173254
describe('getPreprocessorType', () => {
174255
it('should recognise sass resource', () => {
175256
expect(operator.getPreprocessorType({ resource: '/path/to/resource.scss'})).toEqual('sass');
257+
expect(operator.getPreprocessorType({ resource: '/path/to/resource.sass'})).toEqual('sass');
176258
});
177259

178260
it('should recognise less resource', () => {

demo/colors.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ module.exports = {
66
'fancy-white': '#fcfff7',
77
'fancy-black': '#1f2120',
88
'fancy-pink': '#d326c8',
9-
'fancy-lilac': '#941ece'
9+
'fancy-lilac': '#941ece',
10+
'fancy-orange': '#ff6633'
1011
};

demo/style.less

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
require('./dims.js');
2+
require('./colors.js');
3+
24

35
.square {
46
width: @middle;
@@ -9,3 +11,8 @@ require('./dims.js');
911
font-family: monospace;
1012
font-size: small;
1113
}
14+
15+
.fancy-orange {
16+
background-color: @fancy-orange;
17+
}
18+

0 commit comments

Comments
 (0)