-
Notifications
You must be signed in to change notification settings - Fork 14
/
build-sass.js
186 lines (175 loc) · 6.6 KB
/
build-sass.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
import {$} from 'zx'
import fs from 'fs-extra';
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
import { files, log } from 'origami-tools-helpers';
import path from 'node:path';
import { promisify } from 'node:util';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const readFile = promisify(fs.readFile);
const outputFile = promisify(fs.outputFile);
const writeFile = promisify(fs.writeFile);
const unlink = promisify(fs.unlink);
const sassBinary = require.resolve('sass-bin/src/sass');
/**
*
* @param {String} sassFile - The file to return Sass from.
* @param {Object} config - Configuration to augment Sass.
* @param {String|Undefined} config.brand [undefined] - The brand the Sass is for .e.g. "core", "internal", or "whitelabel".
* @param {String} config.sassPrefix [''] - Sass to prefix the Sass from file with.
* @return {Promise<String>} - The sass from the file, with extra Sass variables and prefixes according to configuration.
*/
function getSassData(sassFile, config = {
brand: undefined,
sassPrefix: ''
}) {
// Set Sass system code variable `$system-code`.
const sassSystemCodeVariable = '$system-code: "origami-build-tools";';
// Set Sass brand variable `$o-brand`, given as an obt argument.
const sassBrandVariable = config.brand ? `$o-brand: ${config.brand};` : '';
const sassPrefix = config.sassPrefix ? config.sassPrefix : '';
return readFile(sassFile, 'utf-8').then(code =>
sassSystemCodeVariable + sassBrandVariable + sassPrefix + code
);
}
/**
* Build Sass and return CSS.
*
* @param {Object} [config] - Configuration to build sass.
* @param {String} [config.cwd] - The working directory to resolve assets paths.
* @param {String} [config.sass] ['main.scss'] - The Sass file to build.
* @param {String} [config.buildFolder] ['/build'] - The destination for build assets. Either a directory or "disabled", to never write build css to file.
* @param {String} [config.buildCss] ['main.css'] - The destination filename.
* @param {Boolean} [config.sourcemaps] [true] - Whether to include inline sourcemaps.
* @param {String|Undefined} [config.brand] [undefined] - The brand the Sass build is for .e.g. "core", "internal", or "whitelabel".
* @param {String|Undefined} [config.sassPrefix] [undefined] - Sass to prefix the Sass from file with.
* @param {String} [config.outputStyle] ['expanded'] - The Sass output style. One of `compressed` (removes as many extra characters as possible) or `expanded` (writes each selector and declaration on its own line).
* @param {String[]} [config.sassIncludePaths] - Extra Sass paths to includes.
* @return {Promise<String>} - The built css.
*/
function buildSass(config) {
config = config || {};
const cwd = config.cwd || process.cwd();
const src = config.sass ? Promise.resolve(config.sass) : files.getMainSassPath(cwd);
return src.then(sassFile => {
if (sassFile) {
const destFolder = config.buildFolder || files.getBuildFolderPath(cwd);
const dest = config.buildCss || 'main.css';
const useSourceMaps = typeof config.sourcemaps === 'boolean' ?
config.sourcemaps :
true;
const sassData = getSassData(sassFile, {
brand: config.brand,
sassPrefix: config.sassPrefix,
});
return Promise.resolve(sassData)
.then(async (sassData) => {
const sassArguments = [];
// Set Sass include paths (i.e. npm paths)
sassArguments.push(
...files.getSassIncludePaths(cwd, config).map(p => `--load-path=${p}`)
);
// Set CSS output style. Expanded by default
sassArguments.push(`--style=${config.outputStyle || 'expanded'}`);
// Configure sourcemaps
sassArguments.push(...useSourceMaps ?
['--embed-source-map', '--source-map-urls=absolute'] :
['--no-source-map'],
);
// Build Sass
let result = '';
const prevd = process.cwd();
try {
process.chdir(path.resolve(sassFile, ".."));
$.verbose = false;
const p = $`${sassBinary} --stdin ${sassArguments}`;
p.stdin.write(sassData);
p.stdin.end();
result = await p;
// Output Sass debug logs and warnings
if (result.stderr) {
log.secondary(result.stderr);
}
} catch (error) {
const stderr = error.message || error.stderr || '';
let errorMessage = `Failed building Sass:\n' ${stderr}\n`;
// Find where the Sass error occurred from stderr.
const errorLineMatch = stderr.match(/(?:[\s]+)?(.+.scss)(?:[\s]+)([0-9]+):([0-9]+)/);
// If we know where the Sass error occurred, provide an absolute uri.
if (errorLineMatch) {
const [
,
file,
line,
column
] = errorLineMatch;
errorMessage = errorMessage +
`\n${path.join(cwd, file)}:${line}:${column}\n`;
}
// Forward Sass error.
throw new Error(errorMessage);
} finally {
process.chdir(prevd);
}
return result.stdout;
}).then(css => {
// postcss does not parse the charset unless it is also base64.
// TODO: Remove the charset as a workaround, and remove this code
// when postcss release a fix.
// https://github.com/postcss/postcss/issues/1281#issuecomment-599626666
css = css.replace(
`application/json;charset=utf-8,`,
`application/json,`
);
const postCssTransforms = [];
// Configure postcss autoprefixer transform
postCssTransforms.push(autoprefixer({
overrideBrowserslist: [
'> 1%',
'last 2 versions',
'ie >= 11',
'ff ESR',
'safari >= 9'
],
cascade: false,
flexbox: 'no-2009',
grid: true
}));
// Set postcss options
const postCssOptions = useSourceMaps ? {
from: sassFile,
to: dest,
map: { inline: true }
} : {from: undefined};
// Run postcss
try {
return postcss(postCssTransforms).process(css, postCssOptions);
} catch(error) {
throw new Error(
`Failed building Sass: postcss threw an error.\n` +
error.message
);
}
}).then(postcssResult => {
function cssOnlyHasComments(css) {
const cssComments = /\/\*[^*]*\*+([^\/*][^*]*\*+)*\//g;
const cssWithoutComments = css.replace(cssComments, '');
return cssWithoutComments.trim().length === 0;
}
const css = cssOnlyHasComments(postcssResult.css) ? '' : postcssResult.css;
// Return css after writing to file if a destination
// directory is given.
if (destFolder !== 'disabled') {
return outputFile(path.join(destFolder, dest), css).then(() => css);
}
// Otherwise just return the css.
return css;
});
}
});
};
export {
getSassData,
buildSass
}