-
Notifications
You must be signed in to change notification settings - Fork 0
/
stylus~extend.js
466 lines (382 loc) · 14.6 KB
/
stylus~extend.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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
'use strict';
const path = require('path');
const {Transform} = require('stream');
const utils = require('fepper-utils');
const fs = require('fs-extra');
const gulp = global.gulp || require('gulp');
const sourcemaps = require('gulp-sourcemaps');
const stylus = require('stylus');
const Vinyl = require('vinyl');
const gulpStylus = require('./lib/gulp-stylus');
const conf = global.conf;
const pref = global.pref;
// Set up pref.stylus.
pref.stylus = pref.stylus || {};
// Opt for line comments by default.
if (pref.stylus.linenos !== false) {
pref.stylus.linenos = true;
}
const cssBldDir = conf.ui.paths.source.cssBld; // Do not save .cssSrc because it might be overridden for tests.
const variablesStylPath = conf.ui.paths.source.jsSrc + '/variables.styl';
let vinylPath; // Defined in streamUntouched() to be used in handleError().
const streamUntouched = () => new Transform({
readableObjectMode: true,
writableObjectMode: true,
transform(file, enc, cb) {
vinylPath = file.path;
this.push(file);
cb();
}
});
function getSourcemapDest() {
if (pref.stylus.sourcemap && !pref.stylus.sourcemap.inline) {
return '.';
}
return;
}
function getSourceRoot() {
if (pref.stylus.sourcemap) {
let sourceRoot;
if (pref.stylus.sourcemap.sourceRoot) {
sourceRoot = pref.stylus.sourcemap.sourceRoot;
}
else {
const uiSourceDirRel = conf.ui.pathsRelative.source.root;
const cssSrcDirRel = conf.ui.pathsRelative.source.cssSrc;
if (cssSrcDirRel.indexOf(uiSourceDirRel) === 0) {
const nestedDirs = cssSrcDirRel.slice(uiSourceDirRel.length);
let i = nestedDirs.split('/').length;
sourceRoot = '';
while (i--) {
sourceRoot += '../';
}
sourceRoot += `${cssSrcDirRel}/stylus`;
}
}
return sourceRoot;
}
return;
}
function testForComments() {
const cssFilesBld = fs.readdirSync(cssBldDir);
let hasComments = false;
let i = cssFilesBld.length;
while (i--) {
const cssFileBld = `${cssBldDir}/${cssFilesBld[i]}`;
/* istanbul ignore if */
if (!fs.existsSync(cssFileBld)) {
continue;
}
const stat = fs.statSync(cssFileBld);
if (!stat.isFile()) {
continue;
}
const cssOld = fs.readFileSync(cssFileBld, conf.enc);
hasComments = (/^\/\* line \d+ : /m.test(cssOld) && /\.styl \*\/$/m.test(cssOld)) ||
/^\/\*# sourceMappingURL=/m.test(cssOld);
if (hasComments) {
break;
}
}
return hasComments;
}
function diffThenComment(cb) {
const hasComments = testForComments();
if (hasComments) {
gulp.runSeq(
'stylus:write-tmp',
'stylus',
cb
);
return;
}
const stylDir = `${conf.ui.paths.source.cssSrc}/stylus`;
if (!fs.existsSync(stylDir)) {
cb();
return;
}
const stylFiles = fs.readdirSync(stylDir);
let i = stylFiles.length;
if (i === 0) {
cb();
return;
}
while (i--) {
const stylFile = `${stylDir}/${stylFiles[i]}`;
/* istanbul ignore if */
if (!fs.existsSync(stylFile)) {
if (i === 0) {
cb();
return;
}
continue;
}
const stat = fs.statSync(stylFile);
if (!stat.isFile()) {
if (i === 0) {
cb();
return;
}
continue;
}
const stylFileObj = path.parse(stylFile);
if (stylFileObj.ext !== '.styl') {
/* istanbul ignore if */
if (i === 0) {
cb();
return;
}
continue;
}
const stylFileStr = fs.readFileSync(stylFile, conf.enc);
stylus(stylFileStr)
.set('filename', stylFile)
.set('linenos', false)
.render(
((iteration) => {
return (err, cssNew) => {
if (err) {
/* istanbul ignore next */
utils.error(err);
/* istanbul ignore next */
if (iteration === 0) {
cb();
}
}
else {
// Declare bld file.
const cssFileBld = `${cssBldDir}/${stylFileObj.name}.css`;
const cssFileBldExists = fs.existsSync(cssFileBld);
// Declare tmp file for diffing.
const cssFileTmp = `${conf.ui.paths.source.cssSrc}/.tmp/${stylFileObj.name}.css`;
let cssFileTmpStr;
// If cssFileBld does not exist (quite possibly on a fresh install where it isn't version controlled),
// we need to render and write it. In this case, keep cssFileTmpStr empty even if cssFileTmp exists.
if (cssFileBldExists) {
// For cases where it does exist, set cssFileTmpStr.
// In cases where cssFileTmp exists, set cssFileTmpStr to the contents of that file.
if (fs.existsSync(cssFileTmp)) {
cssFileTmpStr = fs.readFileSync(cssFileTmp, conf.enc);
}
// In cases where cssFileTmp does not exist, output cssFileTmp for future diffing.
else {
// Set cssFileTmpStr == cssNew to skip overwriting cssFileBld.
fs.outputFileSync(cssFileTmp, cssNew);
// Exit this iteration in next block.
cssFileTmpStr = cssNew;
}
}
// Diff newly rendered css against the contents of the tmp file.
// Exit if there has been no change. This is the case for users who only edit bld css and do not modify
// Stylus files.
if (cssFileTmpStr === cssNew) {
/* istanbul ignore if */
if (iteration === 0) {
cb();
}
return;
}
// Output tmp file for future diffing.
fs.outputFileSync(cssFileTmp, cssNew);
// Now, diff tmp css against bld css.
const prefStylusClone = Object.assign({}, pref.stylus, {filename: stylFile});
let stat;
if (cssFileBldExists) {
stat = fs.statSync(cssFileBld);
}
/* istanbul ignore else */
if (!cssFileBldExists || stat.isFile()) {
let cssOld = '';
if (cssFileBldExists) {
cssOld = fs.readFileSync(cssFileBld, conf.enc);
}
// Only overwrite bld css if tmp css and bld css differ.
/* istanbul ignore else */
if (cssNew !== cssOld) {
const style = stylus(stylFileStr, prefStylusClone);
style.render(
((iteration1) => {
return (err1, cssNew1) => {
if (err1) {
/* istanbul ignore next */
utils.error(err1);
}
else {
fs.outputFileSync(cssFileBld, cssNew1);
// Only write sourcemap if not printing line comments and not writing the sourcemap inline.
if (style.sourcemap && !prefStylusClone.linenos && !prefStylusClone.sourcemap.inline) {
fs.outputFileSync(`${cssFileBld}.map`, JSON.stringify(style.sourcemap));
}
}
/* istanbul ignore if */
if (iteration1 === 0) {
cb();
}
};
})(iteration)
);
}
else if (iteration === 0) {
cb();
}
}
else if (iteration === 0) {
cb();
}
}
};
})(i)
);
}
}
function handleError(err) {
utils.error(err);
let errorForBrowserInjection = err.toString();
errorForBrowserInjection = errorForBrowserInjection.slice(errorForBrowserInjection.indexOf('\n') + 1);
errorForBrowserInjection = errorForBrowserInjection.replace(/\\/g, '/'); // Render Windows file paths.
errorForBrowserInjection = errorForBrowserInjection.replace(/\n/g, '\\A '); // Render line feeds.
errorForBrowserInjection = errorForBrowserInjection.replace(/'/g, '\\\''); // Escape internal single-quotes.
errorForBrowserInjection = 'body::before{background-color:white;color:red;content:\'' + errorForBrowserInjection +
'\';white-space:pre;}\n';
const cwd = global.rootDir;
const vPath = (vinylPath.slice(0, vinylPath.lastIndexOf('.')) + '.css').replace(cwd, '');
const base = path.dirname(vPath);
const file = new Vinyl({
cwd,
base,
path: vPath,
contents: Buffer.from(errorForBrowserInjection)
});
this.emit('data', file);
this.emit('end');
}
// Declare gulp tasks.
gulp.task('stylus', function () {
const sourceRoot = getSourceRoot();
let sourcemapsInit = sourcemaps.init;
let sourcemapsWrite = sourcemaps.write;
// Do not write sourcemaps if pref.stylus.sourcemap is falsy.
// Do not write sourcemaps if linenos === true, as the sourcemaps may be inaccurate and the linenos redundant.
if (!pref.stylus.sourcemap || pref.stylus.linenos) {
sourcemapsInit = () => {
return streamUntouched();
};
sourcemapsWrite = () => {
return streamUntouched();
};
}
return gulp.src(conf.ui.paths.source.cssSrc + '/stylus/*.styl')
.pipe(sourcemapsInit())
.pipe(gulpStylus(pref.stylus))
.on('error', handleError)
.pipe(sourcemapsWrite(getSourcemapDest(), {sourceRoot}))
.pipe(gulp.dest(cssBldDir));
});
// This first checks if the old bld CSS has line comments. If so, it runs the 'stylus' task.
// It then renders Stylus into tmp CSS files without line comments for future diffing, and returns.
// If the bld CSS has no line comments, it renders Stylus without line comments to diff the new CSS against the old.
// The first time it runs, it just writes the rendered CSS to a tmp file for future diffing.
// On subsequent runs, it diffs against the previously written tmp CSS.
// If there's no difference, it exits for that file and moves on to the next file if there is one.
// If there is a difference, it writes the new tmp CSS file.
// It then checks for a difference between the new tmp CSS and the bld CSS.
// If there is a difference, it renders Stylus again with line comments and writes that over the bld CSS.
// The intent is for users who use Fepper defaults to never render Stylus if they never edit Stylus files,
// and for users who do edit Stylus files to have Stylus render as expected.
// Power-users should replace this with the 'stylus:once' or 'stylus:no-comment' task for better performance.
gulp.task('stylus:diff-then-comment', diffThenComment);
// 'stylus:frontend-copy' checks if there are line comments in the bld CSS.
// If there are, it renders Stylus without line comments for the full 'frontend-copy' task to copy to the backend.
// If there are not, it does nothing and allows the full 'frontend-copy' task to copy the bld CSS to the backend.
gulp.task('stylus:frontend-copy', function (cb) {
const hasComments = testForComments();
if (hasComments) {
gulp.runSeq(
'stylus:no-comment',
cb
);
}
else {
cb();
}
});
// This renders Stylus without printing line comments. It also never writes sourcemaps.
// You probably want this to preprocess CSS destined for production.
gulp.task('stylus:no-comment', function () {
const prefStylusClone = Object.assign({}, pref.stylus, {linenos: false});
return gulp.src(conf.ui.paths.source.cssSrc + '/stylus/*.styl')
.pipe(streamUntouched())
.pipe(gulpStylus(prefStylusClone))
.on('error', handleError)
.pipe(gulp.dest(cssBldDir));
});
gulp.task('stylus:once', ['stylus']);
gulp.task('stylus:watch', function () {
/* istanbul ignore if */
if (fs.existsSync(variablesStylPath)) {
gulp.watch(variablesStylPath, ['stylus']);
}
// Return the watcher so it can be closed after testing.
return gulp.watch('stylus/**/*', {cwd: conf.ui.paths.source.cssSrc}, ['stylus']);
});
gulp.task('stylus:watch-no-comment', function () {
/* istanbul ignore if */
if (fs.existsSync(variablesStylPath)) {
gulp.watch(variablesStylPath, ['stylus:no-comment']);
}
// Return the watcher so it can be closed after testing.
return gulp.watch('stylus/**/*', {cwd: conf.ui.paths.source.cssSrc}, ['stylus:no-comment']);
});
gulp.task('stylus:watch-write-tmp', function () {
/* istanbul ignore if */
if (fs.existsSync(variablesStylPath)) {
gulp.watch(variablesStylPath, ['stylus']);
}
// Return the watcher so it can be closed after testing.
return gulp.watch('stylus/**/*', {cwd: conf.ui.paths.source.cssSrc}, ['stylus:write-tmp', 'stylus']);
});
// This outputs tmp files without line comments to check for modifications to Stylus code.
gulp.task('stylus:write-tmp', function () {
return gulp.src(conf.ui.paths.source.cssSrc + '/stylus/*.styl')
.pipe(gulpStylus({
linenos: false
}))
.on('error', function () /* istanbul ignore next */ {this.emit('end');})
.pipe(gulp.dest(`${conf.ui.paths.source.cssSrc}/.tmp`));
});
gulp.task('stylus:help', function (cb) {
let out = `
Fepper Stylus Extension
Use:
<task> [<additional args>...]
Tasks:
fp stylus Build Fepper's Stylus files into frontend CSS.
fp stylus:diff-then-comment Only build if there is a diff against tmp file. Line comment CSS by default.
fp stylus:frontend-copy Copy Stylus-built frontend CSS to backend.
fp stylus:no-comment Like 'fp stylus' but without line comments.
fp stylus:once Same as 'fp stylus'.
fp stylus:watch Watch for modifications to Stylus files and build when modified.
fp stylus:watch-no-comment Like 'fp stylus:watch' but without line comments.
fp stylus:watch-write-tmp Like 'fp stylus:watch' but write tmp file for diffing against future builds.
fp stylus:write-tmp Write tmp file for diffing against future builds.
fp stylus:help Print fp-stylus tasks and descriptions.
`;
utils.info(out);
cb();
});
if (fs.existsSync(conf.ui.paths.source.cssSrc + '/broken')) {
gulp.task('stylus:test-broken', function () {
return gulp.src(conf.ui.paths.source.cssSrc + '/broken/broken.styl')
.pipe(streamUntouched())
.pipe(gulpStylus(pref.stylus))
.on('error', handleError)
.pipe(gulp.dest(cssBldDir));
});
gulp.task('stylus:test-broken-partial', function () {
return gulp.src(conf.ui.paths.source.cssSrc + '/broken/broken-partial.styl')
.pipe(streamUntouched())
.pipe(gulpStylus(pref.stylus))
.on('error', handleError)
.pipe(gulp.dest(cssBldDir));
});
}