1 | import { dirname } from 'path';
|
2 | import { createFilter } from 'rollup-pluginutils';
|
3 |
|
4 | function 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 | */
|
220 | const 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 | }`;
|
235 | function loadSassLibrary() {
|
236 | try {
|
237 | return require('sass');
|
238 | }
|
239 | catch (e) {
|
240 | return require('node-sass');
|
241 | }
|
242 | }
|
243 | function stringToCSS(input) {
|
244 | if (typeof input === 'string') {
|
245 | return { css: input, map: '' };
|
246 | }
|
247 | return input;
|
248 | }
|
249 | function red(text) {
|
250 | return '\x1b[1m\x1b[31m' + text + '\x1b[0m';
|
251 | }
|
252 | function green(text) {
|
253 | return '\x1b[1m\x1b[32m' + text + '\x1b[0m';
|
254 | }
|
255 |
|
256 | export { scss as default };
|