UNPKG

6.84 kBJavaScriptView Raw
1// Some css-modules-loader-code dependencies use Promise so we'll provide it for older node versions
2if (!global.Promise) { global.Promise = require('promise-polyfill'); }
3
4var fs = require('fs');
5var path = require('path');
6var Cmify = require('./cmify');
7var Core = require('css-modules-loader-core');
8var FileSystemLoader = require('./file-system-loader');
9var assign = require('object-assign');
10var stringHash = require('string-hash');
11var ReadableStream = require('stream').Readable;
12
13/*
14 Custom `generateScopedName` function for `postcss-modules-scope`.
15 Short names consisting of source hash and line number.
16*/
17function generateShortName (name, filename, css) {
18 // first occurrence of the name
19 // TODO: better match with regex
20 var i = css.indexOf('.' + name);
21 var numLines = css.substr(0, i).split(/[\r\n]/).length;
22
23 var hash = stringHash(css).toString(36).substr(0, 5);
24 return '_' + name + '_' + hash + '_' + numLines;
25}
26
27/*
28 Custom `generateScopedName` function for `postcss-modules-scope`.
29 Appends a hash of the css source.
30*/
31function generateLongName (name, filename) {
32 var sanitisedPath = filename.replace(/\.[^\.\/\\]+$/, '')
33 .replace(/[\W_]+/g, '_')
34 .replace(/^_|_$/g, '');
35
36 return '_' + sanitisedPath + '__' + name;
37}
38
39/*
40 Get the default plugins and apply options.
41*/
42function getDefaultPlugins (options) {
43 var scope = Core.scope;
44 var customNameFunc = options.generateScopedName;
45 var defaultNameFunc = process.env.NODE_ENV === 'production' ?
46 generateShortName :
47 generateLongName;
48
49 scope.generateScopedName = customNameFunc || defaultNameFunc;
50
51 return [
52 Core.values
53 , Core.localByDefault
54 , Core.extractImports
55 , scope
56 ];
57}
58
59/*
60
61 Normalize the manifest paths so that they are always relative
62 to the project root directory.
63
64*/
65function normalizeManifestPaths (tokensByFile, rootDir) {
66 var output = {};
67 var rootDirLength = rootDir.length + 1;
68
69 Object.keys(tokensByFile).forEach(function (filename) {
70 var normalizedFilename = filename.substr(rootDirLength);
71 output[normalizedFilename] = tokensByFile[filename];
72 });
73
74 return output;
75}
76
77// caches
78//
79// persist these for as long as the process is running. #32
80
81// keep track of all tokens so we can avoid duplicates
82var tokensByFile = {};
83
84// we need a separate loader for each entry point
85var loadersByFile = {};
86
87module.exports = function (browserify, options) {
88 options = options || {};
89
90 // if no root directory is specified, assume the cwd
91 var rootDir = options.rootDir || options.d;
92 if (rootDir) { rootDir = path.resolve(rootDir); }
93 if (!rootDir) { rootDir = process.cwd(); }
94
95 var transformOpts = {};
96 if (options.global) {
97 transformOpts.global = true;
98 }
99
100 var cssOutFilename = options.output || options.o;
101 var jsonOutFilename = options.json || options.jsonOutput;
102 transformOpts.cssOutFilename = cssOutFilename;
103
104 // PostCSS plugins passed to FileSystemLoader
105 var plugins = options.use || options.u;
106 if (!plugins) {
107 plugins = getDefaultPlugins(options);
108 }
109 else {
110 if (typeof plugins === 'string') {
111 plugins = [plugins];
112 }
113 }
114
115 var postcssAfter = options.postcssAfter || options.after || [];
116 plugins = plugins.concat(postcssAfter);
117
118 // load plugins by name (if a string is used)
119 plugins = plugins.map(function requirePlugin (name) {
120 // assume functions are already required plugins
121 if (typeof name === 'function') {
122 return name;
123 }
124
125 var plugin = require(require.resolve(name));
126
127 // custom scoped name generation
128 if (name === 'postcss-modules-scope') {
129 options[name] = options[name] || {};
130 if (!options[name].generateScopedName) {
131 options[name].generateScopedName = generateLongName;
132 }
133 }
134
135 if (name in options) {
136 plugin = plugin(options[name]);
137 }
138 else {
139 plugin = plugin.postcss || plugin();
140 }
141
142 return plugin;
143 });
144
145 // create a loader for this entry file
146 if (!loadersByFile[cssOutFilename]) {
147 loadersByFile[cssOutFilename] = new FileSystemLoader(rootDir, plugins);
148 }
149
150 // TODO: clean this up so there's less scope crossing
151 Cmify.prototype._flush = function (callback) {
152 var self = this;
153 var filename = this._filename;
154
155 // only handle .css files
156 if (!this.isCssFile(filename)) { return callback(); }
157
158 // grab the correct loader
159 var loader = loadersByFile[this._cssOutFilename];
160
161 // convert css to js before pushing
162 // reset the `tokensByFile` cache
163 var relFilename = path.relative(rootDir, filename);
164 tokensByFile[filename] = loader.tokensByFile[filename] = null;
165
166 loader.fetch(relFilename, '/').then(function (tokens) {
167 var deps = loader.deps.dependenciesOf(filename);
168 var output = deps.map(function (f) {
169 return 'require("' + f + '")';
170 });
171 output.push('module.exports = ' + JSON.stringify(tokens));
172
173 var isValid = true;
174 var isUndefined = /\bundefined\b/;
175 Object.keys(tokens).forEach(function (k) {
176 if (isUndefined.test(tokens[k])) {
177 isValid = false;
178 }
179 });
180
181 if (!isValid) {
182 var err = 'Composition in ' + filename + ' contains an undefined reference';
183 console.error(err);
184 output.push('console.error("' + err + '");');
185 }
186
187 assign(tokensByFile, loader.tokensByFile);
188
189 self.push(output.join('\n'));
190 return callback();
191 }).catch(function (err) {
192 self.push('console.error("' + err + '");');
193 self.emit('error', err);
194 return callback();
195 });
196 };
197
198 browserify.transform(Cmify, transformOpts);
199
200 // ----
201
202 browserify.on('bundle', function (bundle) {
203 // on each bundle, create a new stream b/c the old one might have ended
204 var compiledCssStream = new ReadableStream();
205 compiledCssStream._read = function () {};
206
207 bundle.emit('css stream', compiledCssStream);
208
209 bundle.on('end', function () {
210 // Combine the collected sources for a single bundle into a single CSS file
211 var loader = loadersByFile[cssOutFilename];
212 var css = loader.finalSource;
213
214 // end the output stream
215 compiledCssStream.push(css);
216 compiledCssStream.push(null);
217
218 // write the css file
219 if (cssOutFilename) {
220 fs.writeFile(cssOutFilename, css, function (err) {
221 if (err) {
222 browserify.emit('error', err);
223 }
224 });
225 }
226
227 // write the classname manifest
228 if (jsonOutFilename) {
229 fs.writeFile(jsonOutFilename, JSON.stringify(normalizeManifestPaths(tokensByFile, rootDir)), function (err) {
230 if (err) {
231 browserify.emit('error', err);
232 }
233 });
234 }
235 });
236 });
237
238 return browserify;
239};
240
241module.exports.generateShortName = generateShortName;
242module.exports.generateLongName = generateLongName;