| 1 | "use strict"
|
|---|
| 2 | // builtin tooling
|
|---|
| 3 | const path = require("path")
|
|---|
| 4 |
|
|---|
| 5 | // internal tooling
|
|---|
| 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 | // 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 |
|
|---|
| 418 | AtImport.postcss = true
|
|---|
| 419 |
|
|---|
| 420 | module.exports = AtImport
|
|---|