1 | "use strict"
|
2 |
|
3 | const path = require("path")
|
4 |
|
5 | const dataURL = require("./data-url")
|
6 | const parseStatements = require("./parse-statements")
|
7 | const processContent = require("./process-content")
|
8 | const resolveId = require("./resolve-id")
|
9 | const formatImportPrelude = require("./format-import-prelude")
|
10 |
|
11 | async 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
74 | async function resolveImportId(result, stmt, options, state, postcss) {
|
75 | if (dataURL.isValid(stmt.uri)) {
|
76 |
|
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 |
|
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 |
|
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 |
|
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 |
|
131 |
|
132 | stmt.children = importedContent.flat().filter(x => !!x)
|
133 | }
|
134 |
|
135 | async 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 |
|
153 | if (state.importedFiles[filename]?.[stmtDuplicateCheckKey]) {
|
154 | return
|
155 | }
|
156 |
|
157 |
|
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 |
|
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 |
|
200 | if (!state.hashFiles[content]) {
|
201 | state.hashFiles[content] = {}
|
202 | }
|
203 |
|
204 | state.hashFiles[content][stmtDuplicateCheckKey] = true
|
205 | }
|
206 | }
|
207 |
|
208 |
|
209 | return parseStyles(
|
210 | result,
|
211 | styles,
|
212 | options,
|
213 | state,
|
214 | conditions,
|
215 | [...from, filename],
|
216 | postcss,
|
217 | )
|
218 | }
|
219 |
|
220 | function isProcessableURL(uri) {
|
221 |
|
222 | if (/^(?:[a-z]+:)?\/\//i.test(uri)) {
|
223 | return false
|
224 | }
|
225 |
|
226 |
|
227 | try {
|
228 |
|
229 | const url = new URL(uri, "https://example.com")
|
230 | if (url.search) {
|
231 | return false
|
232 | }
|
233 | } catch {}
|
234 |
|
235 | return true
|
236 | }
|
237 |
|
238 | module.exports = parseStyles
|