Skip to content

Commit b3b595b

Browse files
committed
Add custom-media queries support
1 parent e26d6e5 commit b3b595b

File tree

13 files changed

+336
-70
lines changed

13 files changed

+336
-70
lines changed

lib/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ exports.default = function (source) {
2525
return _done(null, {}, map);
2626
}
2727

28-
return _done(null, transformToConfig(result.root, path), map);
28+
var obj = transformToConfig(result, path);
29+
emitWarnings(result);
30+
return _done(null, obj, map);
2931
};
3032

3133
var emitWarnings = function emitWarnings(result) {
@@ -44,7 +46,7 @@ exports.default = function (source) {
4446
}
4547
};
4648

47-
utils.getPostcss(true).process(source, { from: this.resourcePath }).then(emitWarnings).then(function (result) {
49+
utils.getPostcss(true).process(source, { from: this.resourcePath }).then(function (result) {
4850
return end(null, result, _this.resourcePath, result.map);
4951
}).catch(onError);
5052
};

lib/utils.js

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ Object.defineProperty(exports, "__esModule", {
55
});
66
exports.getPostcss = exports.toES5Config = exports.toConfig = undefined;
77

8+
var _keys = require('babel-runtime/core-js/object/keys');
9+
10+
var _keys2 = _interopRequireDefault(_keys);
11+
12+
var _typeof2 = require('babel-runtime/helpers/typeof');
13+
14+
var _typeof3 = _interopRequireDefault(_typeof2);
15+
816
var _stringify = require('babel-runtime/core-js/json/stringify');
917

1018
var _stringify2 = _interopRequireDefault(_stringify);
@@ -60,33 +68,106 @@ var dashesCamelCase = function dashesCamelCase(str) {
6068
});
6169
};
6270

63-
var objectify = function objectify(root, filepath) {
64-
var result = {};
71+
var EXTENSION_RE = /\(\s*--([\w-]+)\s*\)/g;
72+
73+
var resolveCustomMediaValue = function resolveCustomMediaValue(query, depChain, map) {
74+
if (!EXTENSION_RE.test(query.value)) {
75+
return query.value;
76+
}
77+
var val = query.value.replace(EXTENSION_RE, function (orig, name) {
78+
name = dashesCamelCase(name.replace(/^-+/, ''));
79+
if (!map[name]) {
80+
return orig;
81+
}
82+
var mq = map[name];
83+
if (mq.resolved) {
84+
return mq.value;
85+
}
86+
87+
if (depChain.indexOf(name) !== -1) {
88+
mq.circular = true;
89+
return orig;
90+
}
91+
depChain.push(name);
92+
mq.value = resolveCustomMediaValue(mq, depChain, map);
93+
94+
return mq.value;
95+
});
96+
if (val === query.value) {
97+
query.circular = true;
98+
}
99+
return val;
100+
};
101+
102+
var objectify = function objectify(result, filepath) {
103+
var obj = {};
104+
var root = result.root;
65105

106+
var customMediaMap = {};
66107
if (!root) {
67-
return result;
108+
return obj;
68109
}
69110

70-
root.walkDecls(function (rule) {
111+
root.walk(function (rule) {
71112
if (rule.source.input.file !== filepath) {
72113
return;
73114
}
74-
if (rule.parent && rule.parent.selectors.find(function (sel) {
75-
return sel === ':root';
76-
})) {
77-
var value = rule.value;
78-
79-
var key = dashesCamelCase(rule.prop.replace(/^-+/, '') // replace "--"
80-
);
81-
82-
result[key] = value.match(/^[+-]?\d*.?(\d*)?(px)$/i) ? parseFloat(value) : value;
115+
if (rule.type === 'atrule' && rule.name === 'custom-media') {
116+
var params = rule.params.split(' ');
117+
var originalKey = params.shift();
118+
var key = dashesCamelCase(originalKey.replace(/^-+/, ''));
119+
if (typeof obj[key] === 'string') {
120+
rule.warn(result, 'Existing exported CSS variable was replaced by @custom-media variable [' + originalKey + ']', {
121+
plugin: 'postcss-variables-loader',
122+
word: originalKey
123+
});
124+
}
125+
customMediaMap[key] = obj[key] = {
126+
value: params.join(' '),
127+
resolved: false,
128+
circular: false,
129+
rule: rule,
130+
originalKey: originalKey
131+
};
132+
} else if (rule.type === 'decl') {
133+
if (rule.parent && rule.parent.selectors.find(function (sel) {
134+
return sel === ':root';
135+
})) {
136+
var value = rule.value;
137+
138+
var _key = dashesCamelCase(rule.prop.replace(/^-+/, ''));
139+
140+
if ((0, _typeof3.default)(obj[_key]) === 'object') {
141+
rule.warn(result, 'Existing exported @custom-media variable was replaced by CSS variable [' + rule.prop + ']', { plugin: 'postcss-variables-loader', word: rule.prop });
142+
}
143+
144+
obj[_key] = value.match(/^[+-]?\d*.?(\d*)?(px)$/i) ? parseFloat(value) : value;
145+
}
83146
}
84147
});
85-
return result;
148+
(0, _keys2.default)(obj).forEach(function (key) {
149+
var val = obj[key];
150+
if ((typeof val === 'undefined' ? 'undefined' : (0, _typeof3.default)(val)) === 'object') {
151+
var mq = customMediaMap[key];
152+
var value = resolveCustomMediaValue(mq, [key], customMediaMap);
153+
mq.value = value;
154+
mq.resolved = true;
155+
if (!mq.circular) {
156+
obj[key] = value;
157+
} else {
158+
mq.rule.warn(result, 'Circular @custom-media definition for [' + mq.originalKey + ']. The entire rule has been removed from the output.', { node: mq.rule });
159+
delete obj[key];
160+
}
161+
}
162+
});
163+
164+
return obj;
86165
};
87166

88167
var toConfig = exports.toConfig = (0, _compose2.default)(toExport, toString, objectify);
89168
var toES5Config = exports.toES5Config = (0, _compose2.default)(toES5Export, toString, objectify);
90169
var getPostcss = exports.getPostcss = function getPostcss(async) {
91-
return (0, _postcss2.default)().use((0, _postcssImport2.default)({ async: async })).use((0, _postcssCssnext2.default)({ features: { customProperties: { preserve: 'computed' } } }));
170+
return (0, _postcss2.default)().use((0, _postcssImport2.default)({ async: async })).use((0, _postcssCssnext2.default)({
171+
features: { customProperties: { preserve: 'computed' }, customMedia: { preserve: true } }
172+
}));
92173
};

src/lib/index.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@ export default function (source) {
2020
return _done(null, {}, map)
2121
}
2222

23-
return _done(null, transformToConfig(result.root, path), map)
23+
const obj = transformToConfig(result, path)
24+
emitWarnings(result)
25+
return _done(null, obj, map)
2426
}
2527

26-
const emitWarnings = (result) => {
27-
result.warnings().forEach((msg) => this.emitWarning(msg.toString()))
28+
const emitWarnings = result => {
29+
result.warnings().forEach(msg => this.emitWarning(msg.toString()))
2830
return result
2931
}
3032

31-
const onError = (error) => {
33+
const onError = error => {
3234
if (error.name === 'CssSyntaxError') {
3335
this.emitError(error.message + error.showSourceCode())
3436
end()
@@ -37,9 +39,9 @@ export default function (source) {
3739
}
3840
}
3941

40-
utils.getPostcss(true)
42+
utils
43+
.getPostcss(true)
4144
.process(source, { from: this.resourcePath })
42-
.then(emitWarnings)
43-
.then((result) => end(null, result, this.resourcePath, result.map))
45+
.then(result => end(null, result, this.resourcePath, result.map))
4446
.catch(onError)
4547
}

src/lib/utils.js

Lines changed: 107 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import atImport from 'postcss-import'
77
const isDev = () => process.env.NODE_ENV === 'development'
88
const isProd = () => !isDev()
99

10-
const toProdExport = (code) => `export default ${code}`
11-
const toDevExport = (code) => `let config = ${code};
10+
const toProdExport = code => `export default ${code}`
11+
const toDevExport = code => `let config = ${code};
1212
if (typeof Proxy !== 'undefined') {
1313
config = new Proxy(config, {
1414
get(target, key) {
@@ -25,43 +25,128 @@ if (typeof Proxy !== 'undefined') {
2525
});
2626
}
2727
export default config`
28-
const toES5Export = (code) => `module.exports = ${code}`
28+
const toES5Export = code => `module.exports = ${code}`
2929

30-
const toExport = cond([
31-
[isDev, toDevExport],
32-
[isProd, toProdExport]
33-
])
30+
const toExport = cond([[isDev, toDevExport], [isProd, toProdExport]])
3431

35-
const toString = (data) => `${JSON.stringify(data, null, '\t')}`
32+
const toString = data => `${JSON.stringify(data, null, '\t')}`
3633

3734
const dashesCamelCase = str =>
3835
str.replace(/-+(\w)/g, (match, firstLetter) => firstLetter.toUpperCase())
3936

40-
const objectify = (root, filepath) => {
41-
const result = {}
37+
const EXTENSION_RE = /\(\s*--([\w-]+)\s*\)/g
4238

39+
const resolveCustomMediaValue = (query, depChain, map) => {
40+
if (!EXTENSION_RE.test(query.value)) {
41+
return query.value
42+
}
43+
const val = query.value.replace(EXTENSION_RE, function (orig, name) {
44+
name = dashesCamelCase(name.replace(/^-+/, ''))
45+
if (!map[name]) {
46+
return orig
47+
}
48+
const mq = map[name]
49+
if (mq.resolved) {
50+
return mq.value
51+
}
52+
53+
if (depChain.indexOf(name) !== -1) {
54+
mq.circular = true
55+
return orig
56+
}
57+
depChain.push(name)
58+
mq.value = resolveCustomMediaValue(mq, depChain, map)
59+
60+
return mq.value
61+
})
62+
if (val === query.value) {
63+
query.circular = true
64+
}
65+
return val
66+
}
67+
68+
const objectify = (result, filepath) => {
69+
const obj = {}
70+
const { root } = result
71+
const customMediaMap = {}
4372
if (!root) {
44-
return result
73+
return obj
4574
}
4675

47-
root.walkDecls((rule) => {
76+
root.walk(rule => {
4877
if (rule.source.input.file !== filepath) {
4978
return
5079
}
51-
if (rule.parent && rule.parent.selectors.find((sel) => sel === ':root')) {
52-
const { value } = rule
53-
const key = dashesCamelCase(
54-
rule.prop.replace(/^-+/, '') // replace "--"
55-
)
80+
if (rule.type === 'atrule' && rule.name === 'custom-media') {
81+
const params = rule.params.split(' ')
82+
const originalKey = params.shift()
83+
const key = dashesCamelCase(originalKey.replace(/^-+/, ''))
84+
if (typeof obj[key] === 'string') {
85+
rule.warn(
86+
result,
87+
`Existing exported CSS variable was replaced by @custom-media variable [${originalKey}]`,
88+
{
89+
plugin: 'postcss-variables-loader',
90+
word: originalKey
91+
}
92+
)
93+
}
94+
customMediaMap[key] = obj[key] = {
95+
value: params.join(' '),
96+
resolved: false,
97+
circular: false,
98+
rule,
99+
originalKey
100+
}
101+
} else if (rule.type === 'decl') {
102+
if (rule.parent && rule.parent.selectors.find(sel => sel === ':root')) {
103+
const { value } = rule
104+
const key = dashesCamelCase(rule.prop.replace(/^-+/, ''))
105+
106+
if (typeof obj[key] === 'object') {
107+
rule.warn(
108+
result,
109+
`Existing exported @custom-media variable was replaced by CSS variable [${rule.prop}]`,
110+
{ plugin: 'postcss-variables-loader', word: rule.prop }
111+
)
112+
}
56113

57-
result[key] = value.match(/^[+-]?\d*.?(\d*)?(px)$/i) ? parseFloat(value) : value
114+
obj[key] = value.match(/^[+-]?\d*.?(\d*)?(px)$/i) ? parseFloat(value) : value
115+
}
58116
}
59117
})
60-
return result
118+
Object.keys(obj).forEach(function (key) {
119+
const val = obj[key]
120+
if (typeof val === 'object') {
121+
const mq = customMediaMap[key]
122+
const value = resolveCustomMediaValue(mq, [key], customMediaMap)
123+
mq.value = value
124+
mq.resolved = true
125+
if (!mq.circular) {
126+
obj[key] = value
127+
} else {
128+
mq.rule.warn(
129+
result,
130+
`Circular @custom-media definition for [${
131+
mq.originalKey
132+
}]. The entire rule has been removed from the output.`,
133+
{ node: mq.rule }
134+
)
135+
delete obj[key]
136+
}
137+
}
138+
})
139+
140+
return obj
61141
}
62142

63143
export const toConfig = compose(toExport, toString, objectify)
64144
export const toES5Config = compose(toES5Export, toString, objectify)
65-
export const getPostcss = (async) => postcss()
66-
.use(atImport({ async }))
67-
.use(cssnext({ features: { customProperties: { preserve: 'computed' } } }))
145+
export const getPostcss = async =>
146+
postcss()
147+
.use(atImport({ async }))
148+
.use(
149+
cssnext({
150+
features: { customProperties: { preserve: 'computed' }, customMedia: { preserve: true } }
151+
})
152+
)

test/fixtures/customMedia.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@custom-media --small-viewport (max-width: 30em);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@custom-media --a (--b);
2+
@custom-media --b (--a);

test/fixtures/customMediaOrder.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
:root {
2+
--small-viewport: i am here;
3+
}
4+
5+
@custom-media --small-viewport (max-width: 30em);
6+
@custom-media --viewport (max-width: 30em);
7+
8+
:root {
9+
--viewport: i am here;
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@import './basic.css';
2+
:root {
3+
--size: 10em;
4+
--custom: 1;
5+
}
6+
7+
@custom-media --custom (max-width: 30em);
8+
@custom-media --small (max-width: 30em);
9+
10+
:root {
11+
--small: 10em;
12+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@import './customMedia.css';
2+
@custom-media --viewport (max-width: 31em);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@custom-media --a-a-a (i am a-a-a);
2+
@custom-media --b (--a-a-a);

0 commit comments

Comments
 (0)