Skip to content

Commit 1c5262d

Browse files
committed
Initial commit
0 parents  commit 1c5262d

8 files changed

Lines changed: 474 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.DS_Store
2+
node_modules
3+
npm-debug.log

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./lib/plugin');

lib/loader.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const path = require('path');
2+
const loaderUtils = require('loader-utils');
3+
const postcss = require('postcss');
4+
5+
const localByDefault = require('postcss-modules-local-by-default');
6+
const modulesScope = require('postcss-modules-scope');
7+
8+
const postCssPlugins = require('./postcss-plugins');
9+
const extractThemeRulesPlugin = postCssPlugins.extractThemeRulesPlugin;
10+
const removeCssModuleExports = postCssPlugins.removeCssModuleExports;
11+
12+
const stringifyRuleMap = (ruleMap) => {
13+
return Object.keys(ruleMap).reduce((acc, selector) => {
14+
const declsMap = ruleMap[selector];
15+
const decls = Object.keys(declsMap).map(prop => `${prop}: ${declsMap[prop]};`);
16+
17+
return `${selector} {\n\t${decls.join('\n\t')}\n}`;
18+
}, '');
19+
};
20+
21+
// copied from css-loader/lib/getLocalIdent.js
22+
function getLocalIdent(loaderContext, localIdentName, localName, options) {
23+
if(!options.context)
24+
options.context = loaderContext.options && typeof loaderContext.options.context === "string" ? loaderContext.options.context : loaderContext.context;
25+
var request = path.relative(options.context, loaderContext.resourcePath);
26+
options.content = options.hashPrefix + request + "+" + localName;
27+
localIdentName = localIdentName.replace(/\[local\]/gi, localName);
28+
var hash = loaderUtils.interpolateName(loaderContext, localIdentName, options);
29+
return hash.replace(new RegExp("[^a-zA-Z0-9\\-_\u00A0-\uFFFF]", "g"), "-").replace(/^([^a-zA-Z_])/, "_$1");
30+
};
31+
32+
33+
module.exports = function (source, map) {
34+
if ( this.cacheable ) this.cacheable();
35+
36+
var loader = this;
37+
var file = loader.resourcePath;
38+
39+
var callback = loader.async();
40+
41+
var plugins = [extractThemeRulesPlugin];
42+
43+
Promise.resolve().then(function (config) {
44+
return postcss(plugins).process(source).then(function (result) {
45+
result.warnings().forEach(function (msg) {
46+
loader.emitWarning(msg.toString());
47+
});
48+
49+
if (result.ruleMap) {
50+
const content = stringifyRuleMap(result.ruleMap);
51+
52+
return {
53+
css: result.css,
54+
map: result.map ? result.map.toJSON() : null,
55+
themeCss: content
56+
};
57+
}
58+
59+
// pass through unaffected css
60+
callback(null, source, map);
61+
return null;
62+
});
63+
}).then(function (prevResult) {
64+
const content = prevResult.themeCss;
65+
66+
const options = loaderUtils.getOptions(loader) || {};
67+
const localIdentName = options.localIdentName || '[hash:base64]';
68+
const customGetLocalIdent = options.getLocalIdent || getLocalIdent;
69+
70+
// convert local classnames for css modules
71+
return postcss([
72+
localByDefault({
73+
mode: 'local'
74+
}),
75+
modulesScope({
76+
generateScopedName: function generateScopedName (exportName) {
77+
return customGetLocalIdent(loader, localIdentName, exportName, {
78+
regExp: options.localIdentRegExp,
79+
hashPrefix: options.hashPrefix || '',
80+
context: loader.options.context
81+
});
82+
}
83+
}),
84+
removeCssModuleExports
85+
]).process(content).then(function (result) {
86+
result.warnings().forEach(function (msg) {
87+
loader.emitWarning(msg.toString());
88+
});
89+
90+
if ('_emitThemePartial' in loader) {
91+
loader._emitThemePartial(result.css);
92+
} else {
93+
throw new Error('Theme loader missing _emitThemePartial function. Did you forget to include the ThemePlugin?');
94+
}
95+
96+
callback(null, prevResult.css, prevResult.map);
97+
return null;
98+
});
99+
}).catch(function (error) {
100+
callback(error);
101+
});
102+
};

lib/plugin.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
const fs = require('fs');
2+
const postcss = require('postcss');
3+
const loaderUtils = require('loader-utils');
4+
5+
const postCssPlugins = require('./postcss-plugins');
6+
const extractVariablesPlugin = postCssPlugins.extractVariablesPlugin;
7+
const replaceThemeVars = postCssPlugins.replaceThemeVars;
8+
9+
const parseThemeFile = (source) => {
10+
return postcss([extractVariablesPlugin]).process(source).then(result => {
11+
result.warnings().forEach(function (msg) {
12+
console.warn(msg.toString());
13+
});
14+
15+
return result.variables;
16+
});
17+
};
18+
19+
const readThemeFile = (filename) => {
20+
return new Promise((resolve, reject) => {
21+
fs.readFile(filename, (err, data) => {
22+
if (err) {
23+
reject(err);
24+
return;
25+
}
26+
27+
parseThemeFile(data)
28+
.then(resolve)
29+
.catch(reject);
30+
});
31+
});
32+
};
33+
34+
const generateTheme = (name, source, variables) => {
35+
return postcss([
36+
replaceThemeVars({variables})
37+
]).process(source).then(result => {
38+
result.warnings().forEach(function (msg) {
39+
console.warn(msg.toString());
40+
});
41+
42+
return {
43+
name: name,
44+
source: result.css
45+
};
46+
});
47+
};
48+
49+
const generateThemes = (themes, source) => {
50+
let promises = [];
51+
52+
for (const themeName in themes) {
53+
if (!themes.hasOwnProperty(themeName)) continue;
54+
55+
const themeFile = themes[themeName];
56+
const promise = readThemeFile(themeFile)
57+
.then(themeVars => generateTheme(themeName, source, themeVars));
58+
59+
promises.push(promise);
60+
}
61+
62+
return Promise.all(promises);
63+
};
64+
65+
function ThemePlugin(options) {
66+
this.options = options || {};
67+
this.cache = null;
68+
}
69+
70+
ThemePlugin.prototype.apply = function apply(compiler) {
71+
const themes = this.options.themes;
72+
const themePartials = [];
73+
74+
compiler.plugin('compilation', compilation => {
75+
compilation.plugin('normal-module-loader', (context, module) => {
76+
context._emitThemePartial = (themeSource) => {
77+
themePartials.push(themeSource);
78+
};
79+
});
80+
});
81+
82+
compiler.plugin('emit', (compilation, done) => {
83+
if (themePartials.length === 0) {
84+
done();
85+
return;
86+
}
87+
88+
// Concat theme partials
89+
const source = themePartials.join('\n');
90+
91+
generateThemes(themes, source).then(function (results) {
92+
const themeAssets = results.reduce((acc, theme) => {
93+
// loaderUtils.interpolateName(loader, 'theme.[name].css', { content: loader.options.context });
94+
const filename = `${theme.name}.theme.css`;
95+
96+
acc[filename] = {
97+
source: () => theme.source,
98+
size: () => theme.source.length
99+
};
100+
101+
return acc;
102+
}, {});
103+
104+
Object.assign(compilation.assets, themeAssets);
105+
106+
done();
107+
}).catch(function (error) {
108+
console.error(error);
109+
done();
110+
});
111+
});
112+
};
113+
114+
module.exports = ThemePlugin;

lib/postcss-plugins.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const postcss = require('postcss');
2+
3+
const THEME_VAR_PREFIX = 'theme-var';
4+
5+
module.exports.extractThemeRulesPlugin = postcss.plugin('postcss-extract-theme-rules', (options = {}) => {
6+
const prefix = options.prefix || THEME_VAR_PREFIX;
7+
8+
return (root, results) => {
9+
let ruleMap;
10+
11+
root.walkRules(rule => {
12+
rule.walkDecls(decl => {
13+
if (decl.value.indexOf(prefix) > -1) {
14+
ruleMap = ruleMap || (ruleMap = {});
15+
let declMap = ruleMap[rule.selector] || (ruleMap[rule.selector] = {});
16+
17+
declMap[decl.prop] = decl.value;
18+
decl.remove();
19+
}
20+
});
21+
});
22+
23+
results.ruleMap = ruleMap;
24+
};
25+
});
26+
27+
module.exports.extractVariablesPlugin = postcss.plugin('postcss-extract-variables', () => {
28+
return (root, results) => {
29+
let vars = {};
30+
31+
root.walkRules(rule => {
32+
if (rule.selector !== ':root') return;
33+
34+
rule.walkDecls(decl => {
35+
if (decl.prop.indexOf('--') === 0) {
36+
vars[decl.prop] = decl.value;
37+
}
38+
});
39+
});
40+
41+
results.variables = vars;
42+
};
43+
});
44+
45+
module.exports.replaceThemeVars = postcss.plugin('postcss-replace-theme-vars', (options = {}) => {
46+
const regex = new RegExp(THEME_VAR_PREFIX + '\\((.+?)\\)');
47+
const variables = options.variables;
48+
49+
return (root) => {
50+
root.walkDecls(decl => {
51+
const match = regex.exec(decl.value);
52+
if (!match) return;
53+
54+
const varName = match[1];
55+
56+
if (!variables.hasOwnProperty(varName)) {
57+
console.error(`Missing theme-var '${varName}'`);
58+
}
59+
60+
decl.value = variables[varName];
61+
});
62+
};
63+
});
64+
65+
module.exports.removeCssModuleExports = postcss.plugin('postcss-remove-exports', () => {
66+
return (root) => {
67+
root.walkRules(rule => {
68+
if (rule.selector === ':export') {
69+
rule.remove();
70+
}
71+
});
72+
};
73+
});

loader.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./lib/loader');

package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "css-variable-theme-webpack-plugin",
3+
"version": "1.0.0",
4+
"description": "Generate theme stylesheets by extracting theme css variables",
5+
"main": "index.js",
6+
"engines": {
7+
"node": ">= 6.0.0"
8+
},
9+
"scripts": {
10+
"test": "echo \"Error: no test specified\" && exit 1"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "git+https://github.com/willowtreeapps/css-variable-theme-webpack-plugin.git"
15+
},
16+
"keywords": [
17+
"webpack",
18+
"plugin",
19+
"loader",
20+
"css",
21+
"variables",
22+
"theme"
23+
],
24+
"author": "Samuel Maddock",
25+
"license": "MIT",
26+
"bugs": {
27+
"url": "https://github.com/willowtreeapps/css-variable-theme-webpack-plugin/issues"
28+
},
29+
"homepage": "https://github.com/willowtreeapps/css-variable-theme-webpack-plugin",
30+
"dependencies": {
31+
"loader-utils": "^1.0.2",
32+
"postcss": "^5.2.15",
33+
"postcss-modules-local-by-default": "^1.1.1",
34+
"postcss-modules-scope": "^1.0.2"
35+
}
36+
}

0 commit comments

Comments
 (0)