1 | "use strict"
|
2 |
|
3 | const path = require("path")
|
4 |
|
5 |
|
6 | const joinMedia = require("./lib/join-media")
|
7 | const joinLayer = require("./lib/join-layer")
|
8 | const resolveId = require("./lib/resolve-id")
|
9 | const loadContent = require("./lib/load-content")
|
10 | const processContent = require("./lib/process-content")
|
11 | const parseStatements = require("./lib/parse-statements")
|
12 | const assignLayerNames = require("./lib/assign-layer-names")
|
13 | const dataURL = require("./lib/data-url")
|
14 |
|
15 | function AtImport(options) {
|
16 | options = {
|
17 | root: process.cwd(),
|
18 | path: [],
|
19 | skipDuplicates: true,
|
20 | resolve: resolveId,
|
21 | load: loadContent,
|
22 | plugins: [],
|
23 | addModulesDirectories: [],
|
24 | nameLayer: null,
|
25 | ...options,
|
26 | }
|
27 |
|
28 | options.root = path.resolve(options.root)
|
29 |
|
30 |
|
31 | if (typeof options.path === "string") options.path = [options.path]
|
32 |
|
33 | if (!Array.isArray(options.path)) options.path = []
|
34 |
|
35 | options.path = options.path.map(p => path.resolve(options.root, p))
|
36 |
|
37 | return {
|
38 | postcssPlugin: "postcss-import",
|
39 | Once(styles, { result, atRule, postcss }) {
|
40 | const state = {
|
41 | importedFiles: {},
|
42 | hashFiles: {},
|
43 | rootFilename: null,
|
44 | anonymousLayerCounter: 0,
|
45 | }
|
46 |
|
47 | if (styles.source?.input?.file) {
|
48 | state.rootFilename = styles.source.input.file
|
49 | state.importedFiles[styles.source.input.file] = {}
|
50 | }
|
51 |
|
52 | if (options.plugins && !Array.isArray(options.plugins)) {
|
53 | throw new Error("plugins option must be an array")
|
54 | }
|
55 |
|
56 | if (options.nameLayer && typeof options.nameLayer !== "function") {
|
57 | throw new Error("nameLayer option must be a function")
|
58 | }
|
59 |
|
60 | return parseStyles(result, styles, options, state, [], []).then(
|
61 | bundle => {
|
62 | applyRaws(bundle)
|
63 | applyMedia(bundle)
|
64 | applyStyles(bundle, styles)
|
65 | }
|
66 | )
|
67 |
|
68 | function applyRaws(bundle) {
|
69 | bundle.forEach((stmt, index) => {
|
70 | if (index === 0) return
|
71 |
|
72 | if (stmt.parent) {
|
73 | const { before } = stmt.parent.node.raws
|
74 | if (stmt.type === "nodes") stmt.nodes[0].raws.before = before
|
75 | else stmt.node.raws.before = before
|
76 | } else if (stmt.type === "nodes") {
|
77 | stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n"
|
78 | }
|
79 | })
|
80 | }
|
81 |
|
82 | function applyMedia(bundle) {
|
83 | bundle.forEach(stmt => {
|
84 | if (
|
85 | (!stmt.media.length && !stmt.layer.length) ||
|
86 | stmt.type === "charset"
|
87 | ) {
|
88 | return
|
89 | }
|
90 |
|
91 | if (stmt.layer.length > 1) {
|
92 | assignLayerNames(stmt.layer, stmt.node, state, options)
|
93 | }
|
94 |
|
95 | if (stmt.type === "import") {
|
96 | const parts = [stmt.fullUri]
|
97 |
|
98 | const media = stmt.media.join(", ")
|
99 |
|
100 | if (stmt.layer.length) {
|
101 | const layerName = stmt.layer.join(".")
|
102 |
|
103 | let layerParams = "layer"
|
104 | if (layerName) {
|
105 | layerParams = `layer(${layerName})`
|
106 | }
|
107 |
|
108 | parts.push(layerParams)
|
109 | }
|
110 |
|
111 | if (media) {
|
112 | parts.push(media)
|
113 | }
|
114 |
|
115 | stmt.node.params = parts.join(" ")
|
116 | } else if (stmt.type === "media") {
|
117 | if (stmt.layer.length) {
|
118 | const layerNode = atRule({
|
119 | name: "layer",
|
120 | params: stmt.layer.join("."),
|
121 | source: stmt.node.source,
|
122 | })
|
123 |
|
124 | if (stmt.parentMedia?.length) {
|
125 | const mediaNode = atRule({
|
126 | name: "media",
|
127 | params: stmt.parentMedia.join(", "),
|
128 | source: stmt.node.source,
|
129 | })
|
130 |
|
131 | mediaNode.append(layerNode)
|
132 | layerNode.append(stmt.node)
|
133 | stmt.node = mediaNode
|
134 | } else {
|
135 | layerNode.append(stmt.node)
|
136 | stmt.node = layerNode
|
137 | }
|
138 | } else {
|
139 | stmt.node.params = stmt.media.join(", ")
|
140 | }
|
141 | } else {
|
142 | const { nodes } = stmt
|
143 | const { parent } = nodes[0]
|
144 |
|
145 | let outerAtRule
|
146 | let innerAtRule
|
147 | if (stmt.media.length && stmt.layer.length) {
|
148 | const mediaNode = atRule({
|
149 | name: "media",
|
150 | params: stmt.media.join(", "),
|
151 | source: parent.source,
|
152 | })
|
153 |
|
154 | const layerNode = atRule({
|
155 | name: "layer",
|
156 | params: stmt.layer.join("."),
|
157 | source: parent.source,
|
158 | })
|
159 |
|
160 | mediaNode.append(layerNode)
|
161 | innerAtRule = layerNode
|
162 | outerAtRule = mediaNode
|
163 | } else if (stmt.media.length) {
|
164 | const mediaNode = atRule({
|
165 | name: "media",
|
166 | params: stmt.media.join(", "),
|
167 | source: parent.source,
|
168 | })
|
169 |
|
170 | innerAtRule = mediaNode
|
171 | outerAtRule = mediaNode
|
172 | } else if (stmt.layer.length) {
|
173 | const layerNode = atRule({
|
174 | name: "layer",
|
175 | params: stmt.layer.join("."),
|
176 | source: parent.source,
|
177 | })
|
178 |
|
179 | innerAtRule = layerNode
|
180 | outerAtRule = layerNode
|
181 | }
|
182 |
|
183 | parent.insertBefore(nodes[0], outerAtRule)
|
184 |
|
185 |
|
186 | nodes.forEach(node => {
|
187 | node.parent = undefined
|
188 | })
|
189 |
|
190 |
|
191 | nodes[0].raws.before = nodes[0].raws.before || "\n"
|
192 |
|
193 |
|
194 | innerAtRule.append(nodes)
|
195 |
|
196 | stmt.type = "media"
|
197 | stmt.node = outerAtRule
|
198 | delete stmt.nodes
|
199 | }
|
200 | })
|
201 | }
|
202 |
|
203 | function applyStyles(bundle, styles) {
|
204 | styles.nodes = []
|
205 |
|
206 |
|
207 | bundle.forEach(stmt => {
|
208 | if (["charset", "import", "media"].includes(stmt.type)) {
|
209 | stmt.node.parent = undefined
|
210 | styles.append(stmt.node)
|
211 | } else if (stmt.type === "nodes") {
|
212 | stmt.nodes.forEach(node => {
|
213 | node.parent = undefined
|
214 | styles.append(node)
|
215 | })
|
216 | }
|
217 | })
|
218 | }
|
219 |
|
220 | function parseStyles(result, styles, options, state, media, layer) {
|
221 | const statements = parseStatements(result, styles)
|
222 |
|
223 | return Promise.resolve(statements)
|
224 | .then(stmts => {
|
225 |
|
226 | return stmts.reduce((promise, stmt) => {
|
227 | return promise.then(() => {
|
228 | stmt.media = joinMedia(media, stmt.media || [])
|
229 | stmt.parentMedia = media
|
230 | stmt.layer = joinLayer(layer, stmt.layer || [])
|
231 |
|
232 |
|
233 | if (
|
234 | stmt.type !== "import" ||
|
235 | /^(?:[a-z]+:)?\/\
|
236 | ) {
|
237 | return
|
238 | }
|
239 |
|
240 | if (options.filter && !options.filter(stmt.uri)) {
|
241 |
|
242 | return
|
243 | }
|
244 |
|
245 | return resolveImportId(result, stmt, options, state)
|
246 | })
|
247 | }, Promise.resolve())
|
248 | })
|
249 | .then(() => {
|
250 | let charset
|
251 | const imports = []
|
252 | const bundle = []
|
253 |
|
254 | function handleCharset(stmt) {
|
255 | if (!charset) charset = stmt
|
256 |
|
257 | else if (
|
258 | stmt.node.params.toLowerCase() !==
|
259 | charset.node.params.toLowerCase()
|
260 | ) {
|
261 | throw new Error(
|
262 | `Incompatable @charset statements:
|
263 | ${stmt.node.params} specified in ${stmt.node.source.input.file}
|
264 | ${charset.node.params} specified in ${charset.node.source.input.file}`
|
265 | )
|
266 | }
|
267 | }
|
268 |
|
269 |
|
270 | statements.forEach(stmt => {
|
271 | if (stmt.type === "charset") handleCharset(stmt)
|
272 | else if (stmt.type === "import") {
|
273 | if (stmt.children) {
|
274 | stmt.children.forEach((child, index) => {
|
275 | if (child.type === "import") imports.push(child)
|
276 | else if (child.type === "charset") handleCharset(child)
|
277 | else bundle.push(child)
|
278 |
|
279 | if (index === 0) child.parent = stmt
|
280 | })
|
281 | } else imports.push(stmt)
|
282 | } else if (stmt.type === "media" || stmt.type === "nodes") {
|
283 | bundle.push(stmt)
|
284 | }
|
285 | })
|
286 |
|
287 | return charset
|
288 | ? [charset, ...imports.concat(bundle)]
|
289 | : imports.concat(bundle)
|
290 | })
|
291 | }
|
292 |
|
293 | function resolveImportId(result, stmt, options, state) {
|
294 | if (dataURL.isValid(stmt.uri)) {
|
295 | return loadImportContent(result, stmt, stmt.uri, options, state).then(
|
296 | result => {
|
297 | stmt.children = result
|
298 | }
|
299 | )
|
300 | }
|
301 |
|
302 | const atRule = stmt.node
|
303 | let sourceFile
|
304 | if (atRule.source?.input?.file) {
|
305 | sourceFile = atRule.source.input.file
|
306 | }
|
307 | const base = sourceFile
|
308 | ? path.dirname(atRule.source.input.file)
|
309 | : options.root
|
310 |
|
311 | return Promise.resolve(options.resolve(stmt.uri, base, options))
|
312 | .then(paths => {
|
313 | if (!Array.isArray(paths)) paths = [paths]
|
314 |
|
315 | return Promise.all(
|
316 | paths.map(file => {
|
317 | return !path.isAbsolute(file)
|
318 | ? resolveId(file, base, options)
|
319 | : file
|
320 | })
|
321 | )
|
322 | })
|
323 | .then(resolved => {
|
324 |
|
325 | resolved.forEach(file => {
|
326 | result.messages.push({
|
327 | type: "dependency",
|
328 | plugin: "postcss-import",
|
329 | file,
|
330 | parent: sourceFile,
|
331 | })
|
332 | })
|
333 |
|
334 | return Promise.all(
|
335 | resolved.map(file => {
|
336 | return loadImportContent(result, stmt, file, options, state)
|
337 | })
|
338 | )
|
339 | })
|
340 | .then(result => {
|
341 |
|
342 | stmt.children = result.reduce((result, statements) => {
|
343 | return statements ? result.concat(statements) : result
|
344 | }, [])
|
345 | })
|
346 | }
|
347 |
|
348 | function loadImportContent(result, stmt, filename, options, state) {
|
349 | const atRule = stmt.node
|
350 | const { media, layer } = stmt
|
351 |
|
352 | assignLayerNames(layer, atRule, state, options)
|
353 |
|
354 | if (options.skipDuplicates) {
|
355 |
|
356 | if (state.importedFiles[filename]?.[media]?.[layer]) {
|
357 | return
|
358 | }
|
359 |
|
360 |
|
361 | if (!state.importedFiles[filename]) {
|
362 | state.importedFiles[filename] = {}
|
363 | }
|
364 | if (!state.importedFiles[filename][media]) {
|
365 | state.importedFiles[filename][media] = {}
|
366 | }
|
367 | state.importedFiles[filename][media][layer] = true
|
368 | }
|
369 |
|
370 | return Promise.resolve(options.load(filename, options)).then(
|
371 | content => {
|
372 | if (content.trim() === "") {
|
373 | result.warn(`${filename} is empty`, { node: atRule })
|
374 | return
|
375 | }
|
376 |
|
377 |
|
378 | if (state.hashFiles[content]?.[media]?.[layer]) {
|
379 | return
|
380 | }
|
381 |
|
382 | return processContent(
|
383 | result,
|
384 | content,
|
385 | filename,
|
386 | options,
|
387 | postcss
|
388 | ).then(importedResult => {
|
389 | const styles = importedResult.root
|
390 | result.messages = result.messages.concat(importedResult.messages)
|
391 |
|
392 | if (options.skipDuplicates) {
|
393 | const hasImport = styles.some(child => {
|
394 | return child.type === "atrule" && child.name === "import"
|
395 | })
|
396 | if (!hasImport) {
|
397 |
|
398 | if (!state.hashFiles[content]) {
|
399 | state.hashFiles[content] = {}
|
400 | }
|
401 | if (!state.hashFiles[content][media]) {
|
402 | state.hashFiles[content][media] = {}
|
403 | }
|
404 | state.hashFiles[content][media][layer] = true
|
405 | }
|
406 | }
|
407 |
|
408 |
|
409 | return parseStyles(result, styles, options, state, media, layer)
|
410 | })
|
411 | }
|
412 | )
|
413 | }
|
414 | },
|
415 | }
|
416 | }
|
417 |
|
418 | AtImport.postcss = true
|
419 |
|
420 | module.exports = AtImport
|