|
| 1 | +// The MIT License (MIT) |
| 2 | + |
| 3 | +// Copyright (c) Maxime Thirouin, Jason Campbell & Kevin Mårtensson |
| 4 | + |
| 5 | +// Permission is hereby granted, free of charge, to any person obtaining a copy of |
| 6 | +// this software and associated documentation files (the "Software"), to deal in |
| 7 | +// the Software without restriction, including without limitation the rights to |
| 8 | +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
| 9 | +// the Software, and to permit persons to whom the Software is furnished to do so, |
| 10 | +// subject to the following conditions: |
| 11 | + |
| 12 | +// The above copyright notice and this permission notice shall be included in all |
| 13 | +// copies or substantial portions of the Software. |
| 14 | + |
| 15 | +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 16 | +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
| 17 | +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
| 18 | +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
| 19 | +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
| 20 | +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 21 | + |
| 22 | +import * as path from 'path' |
| 23 | +import * as postcss from 'postcss' |
| 24 | +import * as postcssImport from 'postcss-import' |
| 25 | +import * as isGlob from 'is-glob' |
| 26 | + |
| 27 | +import joinMedia from './lib/join-media' |
| 28 | +import resolveId from './lib/resolve-id' |
| 29 | +import loadContent from './lib/load-content' |
| 30 | +import processContent from './lib/process-content' |
| 31 | +import parseStatements from './lib/parse-statements' |
| 32 | +import resolveGlob from './lib/resolve-glob' |
| 33 | +import resolveModule from './lib/resolve-module' |
| 34 | + |
| 35 | +function resolve(id) { |
| 36 | + const resolver = isGlob(id) ? resolveGlob : resolveModule |
| 37 | + return resolver.apply(null, arguments) |
| 38 | +} |
| 39 | + |
| 40 | +function AtImport(options) { |
| 41 | + options = Object.assign( |
| 42 | + { |
| 43 | + root: process.cwd(), |
| 44 | + path: [], |
| 45 | + skipDuplicates: true, |
| 46 | + resolve: resolve, |
| 47 | + load: loadContent, |
| 48 | + plugins: [], |
| 49 | + addModulesDirectories: [], |
| 50 | + prefix: '_', |
| 51 | + extensions: ['.ccss'] |
| 52 | + }, |
| 53 | + options |
| 54 | + ) |
| 55 | + |
| 56 | + options.root = path.resolve(options.root) |
| 57 | + |
| 58 | + // convert string to an array of a single element |
| 59 | + if (typeof options.path === "string") options.path = [options.path] |
| 60 | + |
| 61 | + if (!Array.isArray(options.path)) options.path = [] |
| 62 | + |
| 63 | + options.path = options.path.map(p => path.resolve(options.root, p)) |
| 64 | + |
| 65 | + return function(styles, result) { |
| 66 | + const state = { |
| 67 | + importedFiles: {}, |
| 68 | + hashFiles: {}, |
| 69 | + } |
| 70 | + |
| 71 | + if (styles.source && styles.source.input && styles.source.input.file) { |
| 72 | + state.importedFiles[styles.source.input.file] = {} |
| 73 | + } |
| 74 | + |
| 75 | + if (options.plugins && !Array.isArray(options.plugins)) { |
| 76 | + throw new Error("plugins option must be an array") |
| 77 | + } |
| 78 | + |
| 79 | + return parseStyles(result, styles, options, state, []).then(bundle => { |
| 80 | + applyRaws(bundle) |
| 81 | + applyMedia(bundle) |
| 82 | + applyStyles(bundle, styles) |
| 83 | + }) |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +function applyRaws(bundle) { |
| 88 | + bundle.forEach((stmt, index) => { |
| 89 | + if (index === 0) return |
| 90 | + |
| 91 | + if (stmt.parent) { |
| 92 | + const before = stmt.parent.node.raws.before |
| 93 | + if (stmt.type === "nodes") stmt.nodes[0].raws.before = before |
| 94 | + else stmt.node.raws.before = before |
| 95 | + } else if (stmt.type === "nodes") { |
| 96 | + stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n" |
| 97 | + } |
| 98 | + }) |
| 99 | +} |
| 100 | + |
| 101 | +function applyMedia(bundle) { |
| 102 | + bundle.forEach(stmt => { |
| 103 | + if (!stmt.media.length) return |
| 104 | + if (stmt.type === "import") { |
| 105 | + stmt.node.params = `${stmt.fullUri} ${stmt.media.join(", ")}` |
| 106 | + } else if (stmt.type === "media") stmt.node.params = stmt.media.join(", ") |
| 107 | + else { |
| 108 | + |
| 109 | + const nodes = stmt.nodes |
| 110 | + const parent = nodes[0].parent |
| 111 | + const mediaNode = postcss.atRule({ |
| 112 | + name: "media", |
| 113 | + params: stmt.media.join(", "), |
| 114 | + // I don't know what this does but isn't included in the type definition |
| 115 | + //source: parent.source |
| 116 | + }) |
| 117 | + |
| 118 | + parent.insertBefore(nodes[0], mediaNode) |
| 119 | + |
| 120 | + // remove nodes |
| 121 | + nodes.forEach(node => { |
| 122 | + node.parent = undefined |
| 123 | + }) |
| 124 | + |
| 125 | + // better output |
| 126 | + nodes[0].raws.before = nodes[0].raws.before || "\n" |
| 127 | + |
| 128 | + // wrap new rules with media query |
| 129 | + mediaNode.append(nodes) |
| 130 | + |
| 131 | + stmt.type = "media" |
| 132 | + stmt.node = mediaNode |
| 133 | + delete stmt.nodes |
| 134 | + } |
| 135 | + }) |
| 136 | +} |
| 137 | + |
| 138 | +function applyStyles(bundle, styles) { |
| 139 | + styles.nodes = [] |
| 140 | + |
| 141 | + bundle.forEach(stmt => { |
| 142 | + if (stmt.type === "import") { |
| 143 | + stmt.node.parent = undefined |
| 144 | + styles.append(stmt.node) |
| 145 | + } else if (stmt.type === "media") { |
| 146 | + stmt.node.parent = undefined |
| 147 | + styles.append(stmt.node) |
| 148 | + } else if (stmt.type === "nodes") { |
| 149 | + stmt.nodes.forEach(node => { |
| 150 | + node.parent = undefined |
| 151 | + styles.append(node) |
| 152 | + }) |
| 153 | + } |
| 154 | + }) |
| 155 | +} |
| 156 | + |
| 157 | +function parseStyles(result, styles, options, state, media) { |
| 158 | + const statements = parseStatements(result, styles) |
| 159 | + |
| 160 | + return Promise.resolve(statements) |
| 161 | + .then(stmts => { |
| 162 | + // process each statement in series |
| 163 | + return stmts.reduce((promise, stmt) => { |
| 164 | + return promise.then(() => { |
| 165 | + stmt.media = joinMedia(media, stmt.media || []) |
| 166 | + |
| 167 | + // skip protocol base uri (protocol://url) or protocol-relative |
| 168 | + if (stmt.type !== "import" || /^(?:[a-z]+:)?\/\//i.test(stmt.uri)) { |
| 169 | + return |
| 170 | + } |
| 171 | + |
| 172 | + return resolveImportId(result, stmt, options, state) |
| 173 | + }) |
| 174 | + }, Promise.resolve()) |
| 175 | + }) |
| 176 | + .then(() => { |
| 177 | + const imports = [] |
| 178 | + const bundle = [] |
| 179 | + |
| 180 | + // squash statements and their children |
| 181 | + statements.forEach(stmt => { |
| 182 | + if (stmt.type === "import") { |
| 183 | + if (stmt.children) { |
| 184 | + stmt.children.forEach((child, index) => { |
| 185 | + if (child.type === "import") imports.push(child) |
| 186 | + else bundle.push(child) |
| 187 | + // For better output |
| 188 | + if (index === 0) child.parent = stmt |
| 189 | + }) |
| 190 | + } else imports.push(stmt) |
| 191 | + } else if (stmt.type === "media" || stmt.type === "nodes") { |
| 192 | + bundle.push(stmt) |
| 193 | + } |
| 194 | + }) |
| 195 | + |
| 196 | + return imports.concat(bundle) |
| 197 | + }) |
| 198 | +} |
| 199 | + |
| 200 | +function resolveImportId(result, stmt, options, state) { |
| 201 | + const atRule = stmt.node |
| 202 | + let sourceFile |
| 203 | + if (atRule.source && atRule.source.input && atRule.source.input.file) { |
| 204 | + sourceFile = atRule.source.input.file |
| 205 | + } |
| 206 | + const base = sourceFile |
| 207 | + ? path.dirname(atRule.source.input.file) |
| 208 | + : options.root |
| 209 | + |
| 210 | + return Promise.resolve(options.resolve(stmt.uri, base, options)) |
| 211 | + .then(paths => { |
| 212 | + if (!Array.isArray(paths)) paths = [paths] |
| 213 | + // Ensure that each path is absolute: |
| 214 | + return Promise.all( |
| 215 | + paths.map(file => { |
| 216 | + return !path.isAbsolute(file) ? resolveId(file, base, options) : file |
| 217 | + }) |
| 218 | + ) |
| 219 | + }) |
| 220 | + .then(resolved => { |
| 221 | + // Add dependency messages: |
| 222 | + resolved.forEach(file => { |
| 223 | + result.messages.push({ |
| 224 | + type: "dependency", |
| 225 | + file: file, |
| 226 | + parent: sourceFile, |
| 227 | + }) |
| 228 | + }) |
| 229 | + |
| 230 | + return Promise.all( |
| 231 | + resolved.map(file => { |
| 232 | + return loadImportContent(result, stmt, file, options, state) |
| 233 | + }) |
| 234 | + ) |
| 235 | + }) |
| 236 | + .then(result => { |
| 237 | + // Merge loaded statements |
| 238 | + stmt.children = result.reduce((result, statements) => { |
| 239 | + return statements ? result.concat(statements) : result |
| 240 | + }, []) |
| 241 | + }) |
| 242 | +} |
| 243 | + |
| 244 | +function loadImportContent(result, stmt, filename, options, state) { |
| 245 | + const atRule = stmt.node |
| 246 | + const media = stmt.media |
| 247 | + if (options.skipDuplicates) { |
| 248 | + // skip files already imported at the same scope |
| 249 | + if (state.importedFiles[filename] && state.importedFiles[filename][media]) { |
| 250 | + return |
| 251 | + } |
| 252 | + |
| 253 | + // save imported files to skip them next time |
| 254 | + if (!state.importedFiles[filename]) state.importedFiles[filename] = {} |
| 255 | + state.importedFiles[filename][media] = true |
| 256 | + } |
| 257 | + |
| 258 | + return Promise.resolve(options.load(filename, options)).then(content => { |
| 259 | + if (content.trim() === "") { |
| 260 | + result.warn(`${filename} is empty`, { node: atRule }) |
| 261 | + return |
| 262 | + } |
| 263 | + |
| 264 | + // skip previous imported files not containing @import rules |
| 265 | + if (state.hashFiles[content] && state.hashFiles[content][media]) return |
| 266 | + |
| 267 | + return processContent(result, content, filename, options).then( |
| 268 | + importedResult => { |
| 269 | + const styles = importedResult.root |
| 270 | + result.messages = result.messages.concat(importedResult.messages) |
| 271 | + |
| 272 | + if (options.skipDuplicates) { |
| 273 | + const hasImport = styles.some(child => { |
| 274 | + return child.type === "atrule" && child.name === "import" |
| 275 | + }) |
| 276 | + if (!hasImport) { |
| 277 | + // save hash files to skip them next time |
| 278 | + if (!state.hashFiles[content]) state.hashFiles[content] = {} |
| 279 | + state.hashFiles[content][media] = true |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + // recursion: import @import from imported file |
| 284 | + return parseStyles(result, styles, options, state, media) |
| 285 | + } |
| 286 | + ) |
| 287 | + }) |
| 288 | +} |
| 289 | + |
| 290 | +export default postcss.plugin("postcss-import", AtImport) |
0 commit comments