index.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. "use strict"
  2. // builtin tooling
  3. const path = require("path")
  4. // external tooling
  5. const postcss = require("postcss")
  6. // internal tooling
  7. const joinMedia = require("./lib/join-media")
  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. function AtImport(options) {
  13. options = Object.assign(
  14. {
  15. root: process.cwd(),
  16. path: [],
  17. skipDuplicates: true,
  18. resolve: resolveId,
  19. load: loadContent,
  20. plugins: [],
  21. addModulesDirectories: [],
  22. },
  23. options
  24. )
  25. options.root = path.resolve(options.root)
  26. // convert string to an array of a single element
  27. if (typeof options.path === "string") options.path = [options.path]
  28. if (!Array.isArray(options.path)) options.path = []
  29. options.path = options.path.map(p => path.resolve(options.root, p))
  30. return function(styles, result) {
  31. const state = {
  32. importedFiles: {},
  33. hashFiles: {},
  34. }
  35. if (styles.source && styles.source.input && styles.source.input.file) {
  36. state.importedFiles[styles.source.input.file] = {}
  37. }
  38. if (options.plugins && !Array.isArray(options.plugins)) {
  39. throw new Error("plugins option must be an array")
  40. }
  41. return parseStyles(result, styles, options, state, []).then(bundle => {
  42. applyRaws(bundle)
  43. applyMedia(bundle)
  44. applyStyles(bundle, styles)
  45. })
  46. }
  47. }
  48. function applyRaws(bundle) {
  49. bundle.forEach((stmt, index) => {
  50. if (index === 0) return
  51. if (stmt.parent) {
  52. const before = stmt.parent.node.raws.before
  53. if (stmt.type === "nodes") stmt.nodes[0].raws.before = before
  54. else stmt.node.raws.before = before
  55. } else if (stmt.type === "nodes") {
  56. stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n"
  57. }
  58. })
  59. }
  60. function applyMedia(bundle) {
  61. bundle.forEach(stmt => {
  62. if (!stmt.media.length) return
  63. if (stmt.type === "import") {
  64. stmt.node.params = `${stmt.fullUri} ${stmt.media.join(", ")}`
  65. } else if (stmt.type === "media") stmt.node.params = stmt.media.join(", ")
  66. else {
  67. const nodes = stmt.nodes
  68. const parent = nodes[0].parent
  69. const mediaNode = postcss.atRule({
  70. name: "media",
  71. params: stmt.media.join(", "),
  72. source: parent.source,
  73. })
  74. parent.insertBefore(nodes[0], mediaNode)
  75. // remove nodes
  76. nodes.forEach(node => {
  77. node.parent = undefined
  78. })
  79. // better output
  80. nodes[0].raws.before = nodes[0].raws.before || "\n"
  81. // wrap new rules with media query
  82. mediaNode.append(nodes)
  83. stmt.type = "media"
  84. stmt.node = mediaNode
  85. delete stmt.nodes
  86. }
  87. })
  88. }
  89. function applyStyles(bundle, styles) {
  90. styles.nodes = []
  91. // Strip additional statements.
  92. bundle.forEach(stmt => {
  93. if (stmt.type === "import") {
  94. stmt.node.parent = undefined
  95. styles.append(stmt.node)
  96. } else if (stmt.type === "media") {
  97. stmt.node.parent = undefined
  98. styles.append(stmt.node)
  99. } else if (stmt.type === "nodes") {
  100. stmt.nodes.forEach(node => {
  101. node.parent = undefined
  102. styles.append(node)
  103. })
  104. }
  105. })
  106. }
  107. function parseStyles(result, styles, options, state, media) {
  108. const statements = parseStatements(result, styles)
  109. return Promise.resolve(statements)
  110. .then(stmts => {
  111. // process each statement in series
  112. return stmts.reduce((promise, stmt) => {
  113. return promise.then(() => {
  114. stmt.media = joinMedia(media, stmt.media || [])
  115. // skip protocol base uri (protocol://url) or protocol-relative
  116. if (stmt.type !== "import" || /^(?:[a-z]+:)?\/\//i.test(stmt.uri)) {
  117. return
  118. }
  119. if (options.filter && !options.filter(stmt.uri)) {
  120. // rejected by filter
  121. return
  122. }
  123. return resolveImportId(result, stmt, options, state)
  124. })
  125. }, Promise.resolve())
  126. })
  127. .then(() => {
  128. const imports = []
  129. const bundle = []
  130. // squash statements and their children
  131. statements.forEach(stmt => {
  132. if (stmt.type === "import") {
  133. if (stmt.children) {
  134. stmt.children.forEach((child, index) => {
  135. if (child.type === "import") imports.push(child)
  136. else bundle.push(child)
  137. // For better output
  138. if (index === 0) child.parent = stmt
  139. })
  140. } else imports.push(stmt)
  141. } else if (stmt.type === "media" || stmt.type === "nodes") {
  142. bundle.push(stmt)
  143. }
  144. })
  145. return imports.concat(bundle)
  146. })
  147. }
  148. function resolveImportId(result, stmt, options, state) {
  149. const atRule = stmt.node
  150. let sourceFile
  151. if (atRule.source && atRule.source.input && atRule.source.input.file) {
  152. sourceFile = atRule.source.input.file
  153. }
  154. const base = sourceFile
  155. ? path.dirname(atRule.source.input.file)
  156. : options.root
  157. return Promise.resolve(options.resolve(stmt.uri, base, options))
  158. .then(paths => {
  159. if (!Array.isArray(paths)) paths = [paths]
  160. // Ensure that each path is absolute:
  161. return Promise.all(
  162. paths.map(file => {
  163. return !path.isAbsolute(file) ? resolveId(file, base, options) : file
  164. })
  165. )
  166. })
  167. .then(resolved => {
  168. // Add dependency messages:
  169. resolved.forEach(file => {
  170. result.messages.push({
  171. type: "dependency",
  172. plugin: "postcss-import",
  173. file: file,
  174. parent: sourceFile,
  175. })
  176. })
  177. return Promise.all(
  178. resolved.map(file => {
  179. return loadImportContent(result, stmt, file, options, state)
  180. })
  181. )
  182. })
  183. .then(result => {
  184. // Merge loaded statements
  185. stmt.children = result.reduce((result, statements) => {
  186. return statements ? result.concat(statements) : result
  187. }, [])
  188. })
  189. }
  190. function loadImportContent(result, stmt, filename, options, state) {
  191. const atRule = stmt.node
  192. const media = stmt.media
  193. if (options.skipDuplicates) {
  194. // skip files already imported at the same scope
  195. if (state.importedFiles[filename] && state.importedFiles[filename][media]) {
  196. return
  197. }
  198. // save imported files to skip them next time
  199. if (!state.importedFiles[filename]) state.importedFiles[filename] = {}
  200. state.importedFiles[filename][media] = true
  201. }
  202. return Promise.resolve(options.load(filename, options)).then(content => {
  203. if (content.trim() === "") {
  204. result.warn(`${filename} is empty`, { node: atRule })
  205. return
  206. }
  207. // skip previous imported files not containing @import rules
  208. if (state.hashFiles[content] && state.hashFiles[content][media]) return
  209. return processContent(result, content, filename, options).then(
  210. importedResult => {
  211. const styles = importedResult.root
  212. result.messages = result.messages.concat(importedResult.messages)
  213. if (options.skipDuplicates) {
  214. const hasImport = styles.some(child => {
  215. return child.type === "atrule" && child.name === "import"
  216. })
  217. if (!hasImport) {
  218. // save hash files to skip them next time
  219. if (!state.hashFiles[content]) state.hashFiles[content] = {}
  220. state.hashFiles[content][media] = true
  221. }
  222. }
  223. // recursion: import @import from imported file
  224. return parseStyles(result, styles, options, state, media)
  225. }
  226. )
  227. })
  228. }
  229. module.exports = postcss.plugin("postcss-import", AtImport)