/
browserify.js
239 lines (191 loc) · 7.32 KB
/
browserify.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"use strict";
const fs = require("fs");
const path = require("path");
const through = require("through2");
const sink = require("sink-transform");
const mkdirp = require("mkdirp");
const each = require("p-each-series");
const Processor = require("@modular-css/processor");
const relative = require("@modular-css/processor/lib/relative.js");
const output = require("@modular-css/processor/lib/output.js");
const prefixRegex = /^\.\.?\//;
const prefixed = (cwd, file) => {
let out = relative(cwd, file);
if(!prefixRegex.test(out)) {
out = `./${out}`;
}
return out;
};
const outputs = ({ exports }) => `module.exports = ${
JSON.stringify(output.join(exports), null, 4)
};`;
module.exports = (browserify, opts) => {
const options = Object.assign(Object.create(null), {
ext : ".css",
map : browserify._options.debug,
cwd : browserify._options.basedir || process.cwd(),
cache : true,
}, opts);
let processor = options.cache && new Processor(options);
let bundler;
let bundles;
let handled;
if(!options.ext || options.ext.charAt(0) !== ".") {
return browserify.emit("error", `Missing or invalid "ext" option: ${options.ext}`);
}
function depReducer(curr, next) {
curr[prefixed(options.cwd, next)] = next;
return curr;
}
function write(files, to) {
return processor.output({
files,
to,
})
.then(({ css, map }) => {
fs.writeFileSync(to, css, "utf8");
if(map) {
fs.writeFileSync(
`${to}.map`,
map.toString(),
"utf8"
);
}
})
.catch((error) => bundler.emit("error", error));
}
browserify.transform((file) => {
if(path.extname(file) !== options.ext) {
return through();
}
return sink.str(function(buffer, done) {
const push = this.push.bind(this);
const real = fs.realpathSync(file);
processor.string(real, buffer).then(
(result) => {
// Tell watchers about dependencies by emitting "file" events
// AFAIK this is only useful to watchify, to ensure that it watches
// everyone in the dependency graph
processor.dependencies(result.id).forEach((dep) =>
browserify.emit("file", dep, dep)
);
push(outputs(result));
done();
},
(error) => {
// Thrown from the current bundler instance, NOT the main browserify
// instance. This is so that watchify won't explode.
bundler.emit("error", error);
push(buffer);
done();
}
);
});
});
// Splice ourselves as early as possible into the deps pipeline
browserify.pipeline.get("deps").splice(1, 0, through.obj((row, enc, done) => {
if(path.extname(row.file) !== options.ext) {
return done(null, row);
}
handled[row.id] = true;
// Ensure that browserify knows about the CSS dependency tree by updating
// any referenced entries w/ their dependencies
row.deps = processor.dependencies(row.file).reduce(depReducer, {});
return done(null, row);
}, function(done) {
// Ensure that any CSS dependencies not directly referenced are
// injected into the stream of files being managed
const push = this.push.bind(this);
processor.dependencies().forEach((dep) => {
if(dep in handled) {
return;
}
push({
id : path.resolve(options.cwd, dep),
file : path.resolve(options.cwd, dep),
source : outputs(processor.files[dep]),
deps : processor.dependencies(dep).reduce(depReducer, {}),
});
});
done();
}));
// Keep tabs on factor-bundle organization
browserify.on("factor.pipeline", (file, pipeline) => {
bundles[file] = [];
// Track the files in each bundle so we can determine commonalities
// Doesn't actually modify the file, just records it
pipeline.unshift(through.obj((obj, enc, done) => {
if(path.extname(obj.file) === options.ext) {
bundles[file].unshift(obj.file);
}
done(null, obj);
}));
});
// Watchify fires update events when files change, this tells the processor
// to remove the changed files from its cache so they will be re-processed
browserify.on("update", (files) => {
files.forEach((file) => {
processor.dependents(file).forEach((dep) =>
processor.remove(dep)
);
processor.remove(file);
});
});
return browserify.on("bundle", (current) => {
// Calls to .bundle() means we should recreate anything tracking bundling progress
// in case things have changed out from under us, like when using watchify
bundles = {};
handled = {};
// cache set to false means we need to create a new Processor each run-through
if(!options.cache) {
processor = new Processor(options);
}
bundler = current;
// Listen for bundling to finish
bundler.on("end", () => {
const bundling = Object.keys(bundles).length > 0;
if(options.json) {
mkdirp.sync(path.dirname(options.json));
fs.writeFileSync(
options.json,
JSON.stringify(output.compositions(processor), null, 4)
);
}
if(!options.css) {
return;
}
const common = processor.dependencies();
mkdirp.sync(path.dirname(options.css));
// Write out each bundle's CSS files (if they have any)
each(
Object.keys(bundles).map((key) => ({
bundle : key,
files : bundles[key],
})),
(details) => {
const { bundle, files } = details;
if(!files.length && !options.empty) {
return Promise.resolve();
}
// This file was part of a bundle, so remove from the common file
files.forEach((file) =>
common.splice(common.indexOf(file), 1)
);
const dest = path.join(
path.dirname(options.css),
`${path.basename(bundle, path.extname(bundle))}.css`
);
mkdirp.sync(path.dirname(dest));
return write(files, dest);
}
)
.then(() => {
// No common CSS files to write out, so don't (unless they asked nicely)
if(!common.length && !options.empty) {
return Promise.resolve();
}
return write(bundling && common, options.css);
});
});
});
};