UNPKG

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