UNPKG

5.85 kBJavaScriptView Raw
1"use strict"
2
3const path = require("path")
4
5const dataURL = require("./data-url")
6const parseStatements = require("./parse-statements")
7const processContent = require("./process-content")
8const resolveId = require("./resolve-id")
9const formatImportPrelude = require("./format-import-prelude")
10
11async function parseStyles(
12 result,
13 styles,
14 options,
15 state,
16 conditions,
17 from,
18 postcss,
19) {
20 const statements = parseStatements(result, styles, conditions, from)
21
22 for (const stmt of statements) {
23 if (stmt.type !== "import" || !isProcessableURL(stmt.uri)) {
24 continue
25 }
26
27 if (options.filter && !options.filter(stmt.uri)) {
28 // rejected by filter
29 continue
30 }
31
32 await resolveImportId(result, stmt, options, state, postcss)
33 }
34
35 let charset
36 const imports = []
37 const bundle = []
38
39 function handleCharset(stmt) {
40 if (!charset) charset = stmt
41 // charsets aren't case-sensitive, so convert to lower case to compare
42 else if (
43 stmt.node.params.toLowerCase() !== charset.node.params.toLowerCase()
44 ) {
45 throw stmt.node.error(
46 `Incompatible @charset statements:
47 ${stmt.node.params} specified in ${stmt.node.source.input.file}
48 ${charset.node.params} specified in ${charset.node.source.input.file}`,
49 )
50 }
51 }
52
53 // squash statements and their children
54 statements.forEach(stmt => {
55 if (stmt.type === "charset") handleCharset(stmt)
56 else if (stmt.type === "import") {
57 if (stmt.children) {
58 stmt.children.forEach((child, index) => {
59 if (child.type === "import") imports.push(child)
60 else if (child.type === "charset") handleCharset(child)
61 else bundle.push(child)
62 // For better output
63 if (index === 0) child.parent = stmt
64 })
65 } else imports.push(stmt)
66 } else if (stmt.type === "nodes") {
67 bundle.push(stmt)
68 }
69 })
70
71 return charset ? [charset, ...imports.concat(bundle)] : imports.concat(bundle)
72}
73
74async function resolveImportId(result, stmt, options, state, postcss) {
75 if (dataURL.isValid(stmt.uri)) {
76 // eslint-disable-next-line require-atomic-updates
77 stmt.children = await loadImportContent(
78 result,
79 stmt,
80 stmt.uri,
81 options,
82 state,
83 postcss,
84 )
85
86 return
87 } else if (dataURL.isValid(stmt.from.slice(-1))) {
88 // Data urls can't be used as a base url to resolve imports.
89 throw stmt.node.error(
90 `Unable to import '${stmt.uri}' from a stylesheet that is embedded in a data url`,
91 )
92 }
93
94 const atRule = stmt.node
95 let sourceFile
96 if (atRule.source?.input?.file) {
97 sourceFile = atRule.source.input.file
98 }
99 const base = sourceFile
100 ? path.dirname(atRule.source.input.file)
101 : options.root
102
103 const paths = [await options.resolve(stmt.uri, base, options, atRule)].flat()
104
105 // Ensure that each path is absolute:
106 const resolved = await Promise.all(
107 paths.map(file => {
108 return !path.isAbsolute(file)
109 ? resolveId(file, base, options, atRule)
110 : file
111 }),
112 )
113
114 // Add dependency messages:
115 resolved.forEach(file => {
116 result.messages.push({
117 type: "dependency",
118 plugin: "postcss-import",
119 file,
120 parent: sourceFile,
121 })
122 })
123
124 const importedContent = await Promise.all(
125 resolved.map(file => {
126 return loadImportContent(result, stmt, file, options, state, postcss)
127 }),
128 )
129
130 // Merge loaded statements
131 // eslint-disable-next-line require-atomic-updates
132 stmt.children = importedContent.flat().filter(x => !!x)
133}
134
135async function loadImportContent(
136 result,
137 stmt,
138 filename,
139 options,
140 state,
141 postcss,
142) {
143 const atRule = stmt.node
144 const { conditions, from } = stmt
145 const stmtDuplicateCheckKey = conditions
146 .map(condition =>
147 formatImportPrelude(condition.layer, condition.media, condition.supports),
148 )
149 .join(":")
150
151 if (options.skipDuplicates) {
152 // skip files already imported at the same scope
153 if (state.importedFiles[filename]?.[stmtDuplicateCheckKey]) {
154 return
155 }
156
157 // save imported files to skip them next time
158 if (!state.importedFiles[filename]) {
159 state.importedFiles[filename] = {}
160 }
161 state.importedFiles[filename][stmtDuplicateCheckKey] = true
162 }
163
164 if (from.includes(filename)) {
165 return
166 }
167
168 const content = await options.load(filename, options)
169
170 if (content.trim() === "" && options.warnOnEmpty) {
171 result.warn(`${filename} is empty`, { node: atRule })
172 return
173 }
174
175 // skip previous imported files not containing @import rules
176 if (
177 options.skipDuplicates &&
178 state.hashFiles[content]?.[stmtDuplicateCheckKey]
179 ) {
180 return
181 }
182
183 const importedResult = await processContent(
184 result,
185 content,
186 filename,
187 options,
188 postcss,
189 )
190
191 const styles = importedResult.root
192 result.messages = result.messages.concat(importedResult.messages)
193
194 if (options.skipDuplicates) {
195 const hasImport = styles.some(child => {
196 return child.type === "atrule" && child.name === "import"
197 })
198 if (!hasImport) {
199 // save hash files to skip them next time
200 if (!state.hashFiles[content]) {
201 state.hashFiles[content] = {}
202 }
203
204 state.hashFiles[content][stmtDuplicateCheckKey] = true
205 }
206 }
207
208 // recursion: import @import from imported file
209 return parseStyles(
210 result,
211 styles,
212 options,
213 state,
214 conditions,
215 [...from, filename],
216 postcss,
217 )
218}
219
220function isProcessableURL(uri) {
221 // skip protocol base uri (protocol://url) or protocol-relative
222 if (/^(?:[a-z]+:)?\/\//i.test(uri)) {
223 return false
224 }
225
226 // check for fragment or query
227 try {
228 // needs a base to parse properly
229 const url = new URL(uri, "https://example.com")
230 if (url.search) {
231 return false
232 }
233 } catch {} // Ignore
234
235 return true
236}
237
238module.exports = parseStyles