UNPKG

13.2 kBJavaScriptView Raw
1"use strict"
2// builtin tooling
3const path = require("path")
4
5// internal tooling
6const joinMedia = require("./lib/join-media")
7const joinLayer = require("./lib/join-layer")
8const resolveId = require("./lib/resolve-id")
9const loadContent = require("./lib/load-content")
10const processContent = require("./lib/process-content")
11const parseStatements = require("./lib/parse-statements")
12const assignLayerNames = require("./lib/assign-layer-names")
13const dataURL = require("./lib/data-url")
14
15function 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 // convert string to an array of a single element
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 // remove nodes
186 nodes.forEach(node => {
187 node.parent = undefined
188 })
189
190 // better output
191 nodes[0].raws.before = nodes[0].raws.before || "\n"
192
193 // wrap new rules with media query and/or layer at rule
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 // Strip additional statements.
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 // process each statement in series
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 // skip protocol base uri (protocol://url) or protocol-relative
233 if (
234 stmt.type !== "import" ||
235 /^(?:[a-z]+:)?\/\//i.test(stmt.uri)
236 ) {
237 return
238 }
239
240 if (options.filter && !options.filter(stmt.uri)) {
241 // rejected by filter
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 // charsets aren't case-sensitive, so convert to lower case to compare
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 // squash statements and their children
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 // For better output
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 // Ensure that each path is absolute:
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 // Add dependency messages:
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 // Merge loaded statements
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 // skip files already imported at the same scope
356 if (state.importedFiles[filename]?.[media]?.[layer]) {
357 return
358 }
359
360 // save imported files to skip them next time
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 // skip previous imported files not containing @import rules
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 // save hash files to skip them next time
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 // recursion: import @import from imported file
409 return parseStyles(result, styles, options, state, media, layer)
410 })
411 }
412 )
413 }
414 },
415 }
416}
417
418AtImport.postcss = true
419
420module.exports = AtImport