UNPKG

11 kBJavaScriptView Raw
1'use strict';
2
3var path = require('path');
4var rollupPluginutils = require('rollup-pluginutils');
5
6function scss(options = {}) {
7 const filter = rollupPluginutils.createFilter(options.include || ['/**/*.css', '/**/*.scss', '/**/*.sass'], options.exclude);
8 const insertStyleFnName = '___$insertStylesToHeader';
9 const styles = {};
10 const fileName = options.fileName ||
11 (options.output === 'string' ? options.output : undefined);
12 const name = options.name || 'output.css';
13 const prefix = options.prefix ? options.prefix + '\n' : '';
14 let includePaths = options.includePaths || ['node_modules/'];
15 includePaths.push(process.cwd());
16 const compileToCSS = async function (scss) {
17 // Compile SASS to CSS
18 if (scss.length) {
19 includePaths = includePaths.filter((v, i, a) => a.indexOf(v) === i);
20 try {
21 const sass = options.sass || loadSassLibrary();
22 const render = sass.renderSync(Object.assign({
23 data: prefix + scss,
24 outFile: fileName || name,
25 includePaths,
26 importer: (url, prev, done) => {
27 /* If a path begins with `.`, then it's a local import and this
28 * importer cannot handle it. This check covers both `.` and
29 * `..`.
30 *
31 * Additionally, if an import path begins with `url` or `http`,
32 * then it's a remote import, this importer also cannot handle
33 * that. */
34 if (url.startsWith('.') ||
35 url.startsWith('url') ||
36 url.startsWith('http')) {
37 /* The importer returns `null` to defer processing the import
38 * back to the sass compiler. */
39 return null;
40 }
41 /* If the requested path begins with a `~`, we remove it. This
42 * character is used by webpack-contrib's sass-loader to
43 * indicate the import is from the node_modules folder. Since
44 * this is so standard in the JS world, the importer supports
45 * it, by removing it and ignoring it. */
46 const cleanUrl = url.startsWith('~')
47 ? url.replace('~', '')
48 : url;
49 /* Now, the importer uses `require.resolve()` to attempt
50 * to resolve the path to the requested file. In the case
51 * of a standard node_modules project, this will use Node's
52 * `require.resolve()`. In the case of a Plug 'n Play project,
53 * this will use the `require.resolve()` provided by the
54 * package manager.
55 *
56 * This statement is surrounded by a try/catch block because
57 * if Node or the package manager cannot resolve the requested
58 * file, they will throw an error, so the importer needs to
59 * defer to sass, by returning `null`.
60 *
61 * The paths property tells `require.resolve()` where to begin
62 * resolution (i.e. who is requesting the file). */
63 try {
64 const resolved = require.resolve(cleanUrl, {
65 paths: [prefix + scss]
66 });
67 /* Since `require.resolve()` will throw an error if a file
68 * doesn't exist. It's safe to assume the file exists and
69 * pass it off to the sass compiler. */
70 return { file: resolved };
71 }
72 catch (e) {
73 /* Just because `require.resolve()` couldn't find the file
74 * doesn't mean it doesn't exist. It may still be a local
75 * import that just doesn't list a relative path, so defer
76 * processing back to sass by returning `null` */
77 return null;
78 }
79 }
80 }, options));
81 const css = render.css.toString();
82 const map = render.map ? render.map.toString() : '';
83 // Possibly process CSS (e.g. by PostCSS)
84 if (typeof options.processor === 'function') {
85 const result = await options.processor(css, map, styles);
86 // TODO: figure out how to check for
87 // @ts-ignore
88 const postcss = result;
89 // PostCSS support
90 if (typeof postcss.process === 'function') {
91 return Promise.resolve(postcss.process(css, {
92 from: undefined,
93 to: fileName || name,
94 map: map ? { prev: map, inline: false } : null
95 }));
96 }
97 // @ts-ignore
98 const output = result;
99 return stringToCSS(output);
100 }
101 return { css, map };
102 }
103 catch (e) {
104 if (options.failOnError) {
105 throw e;
106 }
107 console.log();
108 console.log(red('Error:\n\t' + e.message));
109 if (e.message.includes('Invalid CSS')) {
110 console.log(green('Solution:\n\t' + 'fix your Sass code'));
111 console.log('Line: ' + e.line);
112 console.log('Column: ' + e.column);
113 }
114 if (e.message.includes('sass') && e.message.includes('find module')) {
115 console.log(green('Solution:\n\t' + 'npm install --save-dev sass'));
116 }
117 if (e.message.includes('node-sass') && e.message.includes('bindings')) {
118 console.log(green('Solution:\n\t' + 'npm rebuild node-sass --force'));
119 }
120 console.log();
121 }
122 }
123 return { css: '', map: '' };
124 };
125 return {
126 name: 'scss',
127 intro() {
128 return options.insert === true
129 ? insertStyleFn.replace(/insertStyleFn/, insertStyleFnName)
130 : '';
131 },
132 async transform(code, id) {
133 if (!filter(id)) {
134 return;
135 }
136 // Add the include path before doing any processing
137 includePaths.push(path.dirname(id));
138 // Rebuild all scss files if anything happens to this folder
139 // TODO: check if it's possible to get a list of all dependent scss files
140 // and only watch those
141 if (options.watch) {
142 const files = Array.isArray(options.watch)
143 ? options.watch
144 : [options.watch];
145 files.forEach(file => this.addWatchFile(file));
146 }
147 if (options.insert === true) {
148 // When the 'insert' is enabled, the stylesheet will be inserted into <head/> tag.
149 const { css, map } = await compileToCSS(code);
150 return {
151 code: 'export default ' +
152 insertStyleFnName +
153 '(' +
154 JSON.stringify(css) +
155 ')',
156 map: { mappings: '' }
157 };
158 }
159 else if (options.output === false) {
160 // When output is disabled, the stylesheet is exported as a string
161 const { css, map } = await compileToCSS(code);
162 return {
163 code: 'export default ' + JSON.stringify(css),
164 map: { mappings: '' }
165 };
166 }
167 // Map of every stylesheet
168 styles[id] = code;
169 return '';
170 },
171 async generateBundle(opts) {
172 // No stylesheet needed
173 if (options.output === false || options.insert === true) {
174 return;
175 }
176 // Combine all stylesheets
177 let scss = '';
178 for (const id in styles) {
179 scss += styles[id] || '';
180 }
181 const compiled = await compileToCSS(scss);
182 if (typeof compiled !== 'object' || typeof compiled.css !== 'string') {
183 return;
184 }
185 // Emit styles through callback
186 if (typeof options.output === 'function') {
187 options.output(compiled.css, styles);
188 return;
189 }
190 // Don't create unwanted empty stylesheets
191 if (!compiled.css.length) {
192 return;
193 }
194 // Emit styles to file
195 this.emitFile({
196 type: 'asset',
197 source: compiled.css,
198 name,
199 fileName
200 });
201 if (options.sourceMap && compiled.map) {
202 let sourcemap = compiled.map;
203 if (typeof compiled.map.toString === 'function') {
204 sourcemap = compiled.map.toString();
205 }
206 this.emitFile({
207 type: 'asset',
208 source: sourcemap,
209 name: name && name + '.map',
210 fileName: fileName && fileName + '.map'
211 });
212 }
213 }
214 };
215}
216/**
217 * Create a style tag and append to head tag
218 *
219 * @param {String} css style
220 * @return {String} css style
221 */
222const insertStyleFn = `function insertStyleFn(css) {
223 if (!css) {
224 return
225 }
226 if (typeof window === 'undefined') {
227 return
228 }
229
230 const style = document.createElement('style');
231
232 style.setAttribute('type', 'text/css');
233 style.innerHTML = css;
234 document.head.appendChild(style);
235 return css
236}`;
237function loadSassLibrary() {
238 try {
239 return require('sass');
240 }
241 catch (e) {
242 return require('node-sass');
243 }
244}
245function stringToCSS(input) {
246 if (typeof input === 'string') {
247 return { css: input, map: '' };
248 }
249 return input;
250}
251function red(text) {
252 return '\x1b[1m\x1b[31m' + text + '\x1b[0m';
253}
254function green(text) {
255 return '\x1b[1m\x1b[32m' + text + '\x1b[0m';
256}
257
258module.exports = scss;