1 | ;
|
2 |
|
3 | var path = require('path');
|
4 | var rollupPluginutils = require('rollup-pluginutils');
|
5 |
|
6 | function 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 | */
|
222 | const 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 | }`;
|
237 | function loadSassLibrary() {
|
238 | try {
|
239 | return require('sass');
|
240 | }
|
241 | catch (e) {
|
242 | return require('node-sass');
|
243 | }
|
244 | }
|
245 | function stringToCSS(input) {
|
246 | if (typeof input === 'string') {
|
247 | return { css: input, map: '' };
|
248 | }
|
249 | return input;
|
250 | }
|
251 | function red(text) {
|
252 | return '\x1b[1m\x1b[31m' + text + '\x1b[0m';
|
253 | }
|
254 | function green(text) {
|
255 | return '\x1b[1m\x1b[32m' + text + '\x1b[0m';
|
256 | }
|
257 |
|
258 | module.exports = scss;
|