UNPKG

6.79 kBJavaScriptView Raw
1const Asset = require('../Asset');
2const postcss = require('postcss');
3const valueParser = require('postcss-value-parser');
4const postcssTransform = require('../transforms/postcss');
5const CssSyntaxError = require('postcss/lib/css-syntax-error');
6const SourceMap = require('../SourceMap');
7const loadSourceMap = require('../utils/loadSourceMap');
8const path = require('path');
9const urlJoin = require('../utils/urlJoin');
10const isURL = require('../utils/is-url');
11
12const URL_RE = /url\s*\("?(?![a-z]+:)/;
13const IMPORT_RE = /@import/;
14const COMPOSES_RE = /composes:.+from\s*("|').*("|')\s*;?/;
15const FROM_IMPORT_RE = /.+from\s*(?:"|')(.*)(?:"|')\s*;?/;
16const PROTOCOL_RE = /^[a-z]+:/;
17
18class CSSAsset extends Asset {
19 constructor(name, options) {
20 super(name, options);
21 this.type = 'css';
22 this.previousSourceMap = this.options.rendition
23 ? this.options.rendition.map
24 : null;
25 }
26
27 mightHaveDependencies() {
28 return (
29 !/\.css$/.test(this.name) ||
30 IMPORT_RE.test(this.contents) ||
31 COMPOSES_RE.test(this.contents) ||
32 URL_RE.test(this.contents)
33 );
34 }
35
36 parse(code) {
37 let root = postcss.parse(code, {
38 from: this.name
39 });
40 return new CSSAst(code, root);
41 }
42
43 collectDependencies() {
44 this.ast.root.walkAtRules('import', rule => {
45 let params = valueParser(rule.params);
46 let [name, ...media] = params.nodes;
47 let dep;
48 if (
49 name.type === 'function' &&
50 name.value === 'url' &&
51 name.nodes.length
52 ) {
53 name = name.nodes[0];
54 }
55
56 dep = name.value;
57
58 if (!dep) {
59 throw new Error('Could not find import name for ' + rule);
60 }
61
62 if (PROTOCOL_RE.test(dep)) {
63 return;
64 }
65
66 // If this came from an inline <style> tag, don't inline the imported file. Replace with the correct URL instead.
67 // TODO: run CSSPackager on inline style tags.
68 let inlineHTML =
69 this.options.rendition && this.options.rendition.inlineHTML;
70 if (inlineHTML) {
71 name.value = this.addURLDependency(dep, {loc: rule.source.start});
72 rule.params = params.toString();
73 } else {
74 media = valueParser.stringify(media).trim();
75 this.addDependency(dep, {media, loc: rule.source.start});
76 rule.remove();
77 }
78
79 this.ast.dirty = true;
80 });
81
82 this.ast.root.walkDecls(decl => {
83 if (URL_RE.test(decl.value)) {
84 let parsed = valueParser(decl.value);
85 let dirty = false;
86
87 parsed.walk(node => {
88 if (
89 node.type === 'function' &&
90 node.value === 'url' &&
91 node.nodes.length
92 ) {
93 let url = this.addURLDependency(node.nodes[0].value, {
94 loc: decl.source.start
95 });
96 if (!isURL(url)) {
97 url = urlJoin(this.options.publicURL, url);
98 }
99 dirty = node.nodes[0].value !== url;
100 node.nodes[0].value = url;
101 }
102 });
103
104 if (dirty) {
105 decl.value = parsed.toString();
106 this.ast.dirty = true;
107 }
108 }
109
110 if (decl.prop === 'composes' && FROM_IMPORT_RE.test(decl.value)) {
111 let parsed = valueParser(decl.value);
112
113 parsed.walk(node => {
114 if (node.type === 'string') {
115 const [, importPath] = FROM_IMPORT_RE.exec(decl.value);
116 this.addURLDependency(importPath, {
117 dynamic: false,
118 loc: decl.source.start
119 });
120 }
121 });
122 }
123 });
124 }
125
126 async pretransform() {
127 if (this.options.sourceMaps && !this.previousSourceMap) {
128 this.previousSourceMap = await loadSourceMap(this);
129 }
130 }
131
132 async transform() {
133 await postcssTransform(this);
134 }
135
136 getCSSAst() {
137 // Converts the ast to a CSS ast if needed, so we can apply postcss transforms.
138 if (!(this.ast instanceof CSSAst)) {
139 this.ast = CSSAsset.prototype.parse.call(
140 this,
141 this.ast.render(this.name)
142 );
143 }
144
145 return this.ast.root;
146 }
147
148 async generate() {
149 let css;
150 if (this.ast) {
151 let result = this.ast.render(this.name);
152 css = result.css;
153 if (result.map) this.sourceMap = result.map;
154 } else {
155 css = this.contents;
156 }
157
158 let js = '';
159 if (this.options.hmr) {
160 this.addDependency('_css_loader');
161
162 js = `
163 var reloadCSS = require('_css_loader');
164 module.hot.dispose(reloadCSS);
165 module.hot.accept(reloadCSS);
166 `;
167 }
168
169 if (this.cssModules) {
170 js +=
171 'module.exports = ' + JSON.stringify(this.cssModules, null, 2) + ';';
172 }
173
174 if (this.options.sourceMaps) {
175 if (this.sourceMap) {
176 this.sourceMap = await new SourceMap().addMap(this.sourceMap);
177 }
178
179 if (this.previousSourceMap) {
180 this.previousSourceMap.sources = this.previousSourceMap.sources.map(v =>
181 path.join(
182 path.dirname(this.relativeName),
183 this.previousSourceMap.sourceRoot || '',
184 v
185 )
186 );
187 if (this.sourceMap) {
188 this.sourceMap = await new SourceMap().extendSourceMap(
189 this.previousSourceMap,
190 this.sourceMap
191 );
192 } else {
193 this.sourceMap = await new SourceMap().addMap(this.previousSourceMap);
194 }
195 } else if (!this.sourceMap) {
196 this.sourceMap = new SourceMap().generateEmptyMap(
197 this.relativeName,
198 css
199 );
200 }
201 }
202
203 return [
204 {
205 type: 'css',
206 value: css,
207 cssModules: this.cssModules,
208 map: this.sourceMap
209 },
210 {
211 type: 'js',
212 value: js,
213 hasDependencies: false
214 }
215 ];
216 }
217
218 generateErrorMessage(err) {
219 // Wrap the error in a CssSyntaxError if needed so we can generate a code frame
220 if (err.loc && !err.showSourceCode) {
221 err = new CssSyntaxError(
222 err.message,
223 err.loc.line,
224 err.loc.column,
225 this.contents
226 );
227 }
228
229 err.message = err.reason || err.message;
230 err.loc = {
231 line: err.line,
232 column: err.column
233 };
234
235 if (err.showSourceCode) {
236 err.codeFrame = err.showSourceCode();
237 err.highlightedCodeFrame = err.showSourceCode(true);
238 }
239
240 return err;
241 }
242}
243
244class CSSAst {
245 constructor(css, root) {
246 this.css = css;
247 this.root = root;
248 this.dirty = false;
249 }
250
251 render(name) {
252 if (this.dirty) {
253 let {css, map} = this.root.toResult({
254 to: name,
255 map: {inline: false, annotation: false, sourcesContent: true}
256 });
257
258 this.css = css;
259
260 return {
261 css: this.css,
262 map: map ? map.toJSON() : null
263 };
264 }
265
266 return {
267 css: this.css
268 };
269 }
270}
271
272module.exports = CSSAsset;