UNPKG

7.94 kBJavaScriptView Raw
1var path = require("path")
2var assign = require("object-assign")
3var postcss = require("postcss")
4var joinMedia = require("./lib/join-media")
5var resolveId = require("./lib/resolve-id")
6var loadContent = require("./lib/load-content")
7var parseStatements = require("./lib/parse-statements")
8
9function AtImport(options) {
10 options = assign({
11 root: process.cwd(),
12 path: [],
13 skipDuplicates: true,
14 resolve: resolveId,
15 load: loadContent,
16 plugins: [],
17 }, options)
18
19 options.root = path.resolve(options.root)
20
21 // convert string to an array of a single element
22 if (typeof options.path === "string") {
23 options.path = [ options.path ]
24 }
25
26 if (!Array.isArray(options.path)) {
27 options.path = []
28 }
29
30 options.path = options.path.map(function(p) {
31 return path.resolve(options.root, p)
32 })
33
34 return function(styles, result) {
35 var state = {
36 importedFiles: {},
37 hashFiles: {},
38 }
39
40 if (styles.source && styles.source.input && styles.source.input.file) {
41 state.importedFiles[styles.source.input.file] = {}
42 }
43
44 if (options.plugins && !Array.isArray(options.plugins)) {
45 throw new Error("plugins option must be an array")
46 }
47
48 return parseStyles(
49 result,
50 styles,
51 options,
52 state,
53 []
54 ).then(function(bundle) {
55
56 applyRaws(bundle)
57 applyMedia(bundle)
58 applyStyles(bundle, styles)
59
60 if (
61 typeof options.addDependencyTo === "object" &&
62 typeof options.addDependencyTo.addDependency === "function"
63 ) {
64 Object.keys(state.importedFiles)
65 .forEach(options.addDependencyTo.addDependency)
66 }
67
68 if (typeof options.onImport === "function") {
69 options.onImport(Object.keys(state.importedFiles))
70 }
71 })
72 }
73}
74
75function applyRaws(bundle) {
76 bundle.forEach(function(stmt, index) {
77 if (index === 0) {
78 return
79 }
80
81 if (stmt.parent) {
82 var before = stmt.parent.node.raws.before
83 if (stmt.type === "nodes") {
84 stmt.nodes[0].raws.before = before
85 }
86 else {
87 stmt.node.raws.before = before
88 }
89 }
90 else if (stmt.type === "nodes") {
91 stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n"
92 }
93 })
94}
95
96function applyMedia(bundle) {
97 bundle.forEach(function(stmt) {
98 if (!stmt.media.length) {
99 return
100 }
101 if (stmt.type === "import") {
102 stmt.node.params = stmt.fullUri + " " + stmt.media.join(", ")
103 }
104 else if (stmt.type ==="media") {
105 stmt.node.params = stmt.media.join(", ")
106 }
107 else {
108 var nodes = stmt.nodes
109 var parent = nodes[0].parent
110 var mediaNode = postcss.atRule({
111 name: "media",
112 params: stmt.media.join(", "),
113 source: parent.source,
114 })
115
116 parent.insertBefore(nodes[0], mediaNode)
117
118 // remove nodes
119 nodes.forEach(function(node) {
120 node.parent = undefined
121 })
122
123 // better output
124 nodes[0].raws.before = nodes[0].raws.before || "\n"
125
126 // wrap new rules with media query
127 mediaNode.append(nodes)
128
129 stmt.type = "media"
130 stmt.node = mediaNode
131 delete stmt.nodes
132 }
133 })
134}
135
136function applyStyles(bundle, styles) {
137 styles.nodes = []
138
139 bundle.forEach(function(stmt) {
140 if (stmt.type === "import") {
141 stmt.node.parent = undefined
142 styles.append(stmt.node)
143 }
144 else if (stmt.type === "media") {
145 stmt.node.parent = undefined
146 styles.append(stmt.node)
147 }
148 else if (stmt.type === "nodes") {
149 stmt.nodes.forEach(function(node) {
150 node.parent = undefined
151 styles.append(node)
152 })
153 }
154 })
155}
156
157function parseStyles(
158 result,
159 styles,
160 options,
161 state,
162 media
163) {
164 var statements = parseStatements(result, styles)
165
166 return Promise.all(statements.map(function(stmt) {
167 stmt.media = joinMedia(media, stmt.media)
168
169 // skip protocol base uri (protocol://url) or protocol-relative
170 if (stmt.type !== "import" || /^(?:[a-z]+:)?\/\//i.test(stmt.uri)) {
171 return
172 }
173 return resolveImportId(
174 result,
175 stmt,
176 options,
177 state
178 )
179 })).then(function() {
180 var imports = []
181 var bundle = []
182
183 // squash statements and their children
184 statements.forEach(function(stmt) {
185 if (stmt.type === "import") {
186 if (stmt.children) {
187 stmt.children.forEach(function(child, index) {
188 if (child.type === "import") {
189 imports.push(child)
190 }
191 else {
192 bundle.push(child)
193 }
194 // For better output
195 if (index === 0) {
196 child.parent = stmt
197 }
198 })
199 }
200 else {
201 imports.push(stmt)
202 }
203 }
204 else if (stmt.type === "media" || stmt.type === "nodes") {
205 bundle.push(stmt)
206 }
207 })
208
209 return imports.concat(bundle)
210 })
211}
212
213function resolveImportId(
214 result,
215 stmt,
216 options,
217 state
218) {
219 var atRule = stmt.node
220 var base = atRule.source && atRule.source.input && atRule.source.input.file
221 ? path.dirname(atRule.source.input.file)
222 : options.root
223
224 return Promise.resolve(options.resolve(stmt.uri, base, options))
225 .then(function(resolved) {
226 if (!Array.isArray(resolved)) {
227 resolved = [ resolved ]
228 }
229 return Promise.all(resolved.map(function(file) {
230 return loadImportContent(
231 result,
232 stmt,
233 file,
234 options,
235 state
236 )
237 }))
238 })
239 .then(function(result) {
240 // Merge loaded statements
241 stmt.children = result.reduce(function(result, statements) {
242 if (statements) {
243 result = result.concat(statements)
244 }
245 return result
246 }, [])
247 })
248 .catch(function(err) {
249 result.warn(err.message, { node: atRule })
250 })
251}
252
253function loadImportContent(
254 result,
255 stmt,
256 filename,
257 options,
258 state
259) {
260 var atRule = stmt.node
261 var media = stmt.media
262 if (options.skipDuplicates) {
263 // skip files already imported at the same scope
264 if (
265 state.importedFiles[filename] &&
266 state.importedFiles[filename][media]
267 ) {
268 return
269 }
270
271 // save imported files to skip them next time
272 if (!state.importedFiles[filename]) {
273 state.importedFiles[filename] = {}
274 }
275 state.importedFiles[filename][media] = true
276 }
277
278 return Promise.resolve(options.load(filename, options))
279 .then(function(content) {
280 if (typeof options.transform !== "function") {
281 return content
282 }
283 return Promise.resolve(options.transform(content, filename, options))
284 .then(function(transformed) {
285 return typeof transformed === "string" ? transformed : content
286 })
287 })
288 .then(function(content) {
289 if (content.trim() === "") {
290 result.warn(filename + " is empty", { node: atRule })
291 return
292 }
293
294 // skip previous imported files not containing @import rules
295 if (
296 state.hashFiles[content] &&
297 state.hashFiles[content][media]
298 ) {
299 return
300 }
301
302 return postcss(options.plugins).process(content, {
303 from: filename,
304 syntax: result.opts.syntax,
305 parser: result.opts.parser,
306 })
307 .then(function(importedResult) {
308 var styles = importedResult.root
309 result.messages = result.messages.concat(importedResult.messages)
310
311 if (options.skipDuplicates) {
312 var hasImport = styles.some(function(child) {
313 return child.type === "atrule" && child.name === "import"
314 })
315 if (!hasImport) {
316 // save hash files to skip them next time
317 if (!state.hashFiles[content]) {
318 state.hashFiles[content] = {}
319 }
320 state.hashFiles[content][media] = true
321 }
322 }
323
324 // recursion: import @import from imported file
325 return parseStyles(
326 result,
327 styles,
328 options,
329 state,
330 media
331 )
332 })
333 })
334}
335
336module.exports = postcss.plugin(
337 "postcss-import",
338 AtImport
339)