|
2 | 2 | // builtin tooling
|
3 | 3 | const path = require("path")
|
4 | 4 |
|
5 |
| -// external tooling |
6 |
| -const postcss = require("postcss") |
7 |
| - |
8 | 5 | // internal tooling
|
9 | 6 | const joinMedia = require("./lib/join-media")
|
10 | 7 | const resolveId = require("./lib/resolve-id")
|
@@ -35,234 +32,253 @@ function AtImport(options) {
|
35 | 32 |
|
36 | 33 | options.path = options.path.map(p => path.resolve(options.root, p))
|
37 | 34 |
|
38 |
| - return function(styles, result) { |
39 |
| - const state = { |
40 |
| - importedFiles: {}, |
41 |
| - hashFiles: {}, |
42 |
| - } |
43 |
| - |
44 |
| - if (styles.source && styles.source.input && styles.source.input.file) { |
45 |
| - state.importedFiles[styles.source.input.file] = {} |
46 |
| - } |
47 |
| - |
48 |
| - if (options.plugins && !Array.isArray(options.plugins)) { |
49 |
| - throw new Error("plugins option must be an array") |
50 |
| - } |
51 |
| - |
52 |
| - return parseStyles(result, styles, options, state, []).then(bundle => { |
53 |
| - applyRaws(bundle) |
54 |
| - applyMedia(bundle) |
55 |
| - applyStyles(bundle, styles) |
56 |
| - }) |
57 |
| - } |
58 |
| -} |
59 |
| - |
60 |
| -function applyRaws(bundle) { |
61 |
| - bundle.forEach((stmt, index) => { |
62 |
| - if (index === 0) return |
63 |
| - |
64 |
| - if (stmt.parent) { |
65 |
| - const before = stmt.parent.node.raws.before |
66 |
| - if (stmt.type === "nodes") stmt.nodes[0].raws.before = before |
67 |
| - else stmt.node.raws.before = before |
68 |
| - } else if (stmt.type === "nodes") { |
69 |
| - stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n" |
70 |
| - } |
71 |
| - }) |
72 |
| -} |
| 35 | + return { |
| 36 | + postcssPlugin: "postcss-import", |
| 37 | + Once(styles, { result, atRule }) { |
| 38 | + const state = { |
| 39 | + importedFiles: {}, |
| 40 | + hashFiles: {}, |
| 41 | + } |
73 | 42 |
|
74 |
| -function applyMedia(bundle) { |
75 |
| - bundle.forEach(stmt => { |
76 |
| - if (!stmt.media.length) return |
77 |
| - if (stmt.type === "import") { |
78 |
| - stmt.node.params = `${stmt.fullUri} ${stmt.media.join(", ")}` |
79 |
| - } else if (stmt.type === "media") stmt.node.params = stmt.media.join(", ") |
80 |
| - else { |
81 |
| - const nodes = stmt.nodes |
82 |
| - const parent = nodes[0].parent |
83 |
| - const mediaNode = postcss.atRule({ |
84 |
| - name: "media", |
85 |
| - params: stmt.media.join(", "), |
86 |
| - source: parent.source, |
87 |
| - }) |
| 43 | + if (styles.source && styles.source.input && styles.source.input.file) { |
| 44 | + state.importedFiles[styles.source.input.file] = {} |
| 45 | + } |
88 | 46 |
|
89 |
| - parent.insertBefore(nodes[0], mediaNode) |
| 47 | + if (options.plugins && !Array.isArray(options.plugins)) { |
| 48 | + throw new Error("plugins option must be an array") |
| 49 | + } |
90 | 50 |
|
91 |
| - // remove nodes |
92 |
| - nodes.forEach(node => { |
93 |
| - node.parent = undefined |
| 51 | + return parseStyles(result, styles, options, state, []).then(bundle => { |
| 52 | + applyRaws(bundle) |
| 53 | + applyMedia(bundle) |
| 54 | + applyStyles(bundle, styles) |
94 | 55 | })
|
95 | 56 |
|
96 |
| - // better output |
97 |
| - nodes[0].raws.before = nodes[0].raws.before || "\n" |
| 57 | + function applyRaws(bundle) { |
| 58 | + bundle.forEach((stmt, index) => { |
| 59 | + if (index === 0) return |
98 | 60 |
|
99 |
| - // wrap new rules with media query |
100 |
| - mediaNode.append(nodes) |
| 61 | + if (stmt.parent) { |
| 62 | + const before = stmt.parent.node.raws.before |
| 63 | + if (stmt.type === "nodes") stmt.nodes[0].raws.before = before |
| 64 | + else stmt.node.raws.before = before |
| 65 | + } else if (stmt.type === "nodes") { |
| 66 | + stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n" |
| 67 | + } |
| 68 | + }) |
| 69 | + } |
101 | 70 |
|
102 |
| - stmt.type = "media" |
103 |
| - stmt.node = mediaNode |
104 |
| - delete stmt.nodes |
105 |
| - } |
106 |
| - }) |
107 |
| -} |
| 71 | + function applyMedia(bundle) { |
| 72 | + bundle.forEach(stmt => { |
| 73 | + if (!stmt.media.length) return |
| 74 | + if (stmt.type === "import") { |
| 75 | + stmt.node.params = `${stmt.fullUri} ${stmt.media.join(", ")}` |
| 76 | + } else if (stmt.type === "media") |
| 77 | + stmt.node.params = stmt.media.join(", ") |
| 78 | + else { |
| 79 | + const nodes = stmt.nodes |
| 80 | + const parent = nodes[0].parent |
| 81 | + const mediaNode = atRule({ |
| 82 | + name: "media", |
| 83 | + params: stmt.media.join(", "), |
| 84 | + source: parent.source, |
| 85 | + }) |
108 | 86 |
|
109 |
| -function applyStyles(bundle, styles) { |
110 |
| - styles.nodes = [] |
111 |
| - |
112 |
| - // Strip additional statements. |
113 |
| - bundle.forEach(stmt => { |
114 |
| - if (stmt.type === "import") { |
115 |
| - stmt.node.parent = undefined |
116 |
| - styles.append(stmt.node) |
117 |
| - } else if (stmt.type === "media") { |
118 |
| - stmt.node.parent = undefined |
119 |
| - styles.append(stmt.node) |
120 |
| - } else if (stmt.type === "nodes") { |
121 |
| - stmt.nodes.forEach(node => { |
122 |
| - node.parent = undefined |
123 |
| - styles.append(node) |
124 |
| - }) |
125 |
| - } |
126 |
| - }) |
127 |
| -} |
| 87 | + parent.insertBefore(nodes[0], mediaNode) |
128 | 88 |
|
129 |
| -function parseStyles(result, styles, options, state, media) { |
130 |
| - const statements = parseStatements(result, styles) |
| 89 | + // remove nodes |
| 90 | + nodes.forEach(node => { |
| 91 | + node.parent = undefined |
| 92 | + }) |
131 | 93 |
|
132 |
| - return Promise.resolve(statements) |
133 |
| - .then(stmts => { |
134 |
| - // process each statement in series |
135 |
| - return stmts.reduce((promise, stmt) => { |
136 |
| - return promise.then(() => { |
137 |
| - stmt.media = joinMedia(media, stmt.media || []) |
| 94 | + // better output |
| 95 | + nodes[0].raws.before = nodes[0].raws.before || "\n" |
138 | 96 |
|
139 |
| - // skip protocol base uri (protocol://url) or protocol-relative |
140 |
| - if (stmt.type !== "import" || /^(?:[a-z]+:)?\/\//i.test(stmt.uri)) { |
141 |
| - return |
142 |
| - } |
| 97 | + // wrap new rules with media query |
| 98 | + mediaNode.append(nodes) |
143 | 99 |
|
144 |
| - if (options.filter && !options.filter(stmt.uri)) { |
145 |
| - // rejected by filter |
146 |
| - return |
| 100 | + stmt.type = "media" |
| 101 | + stmt.node = mediaNode |
| 102 | + delete stmt.nodes |
147 | 103 | }
|
148 |
| - |
149 |
| - return resolveImportId(result, stmt, options, state) |
150 |
| - }) |
151 |
| - }, Promise.resolve()) |
152 |
| - }) |
153 |
| - .then(() => { |
154 |
| - const imports = [] |
155 |
| - const bundle = [] |
156 |
| - |
157 |
| - // squash statements and their children |
158 |
| - statements.forEach(stmt => { |
159 |
| - if (stmt.type === "import") { |
160 |
| - if (stmt.children) { |
161 |
| - stmt.children.forEach((child, index) => { |
162 |
| - if (child.type === "import") imports.push(child) |
163 |
| - else bundle.push(child) |
164 |
| - // For better output |
165 |
| - if (index === 0) child.parent = stmt |
166 |
| - }) |
167 |
| - } else imports.push(stmt) |
168 |
| - } else if (stmt.type === "media" || stmt.type === "nodes") { |
169 |
| - bundle.push(stmt) |
170 |
| - } |
171 |
| - }) |
172 |
| - |
173 |
| - return imports.concat(bundle) |
174 |
| - }) |
175 |
| -} |
176 |
| - |
177 |
| -function resolveImportId(result, stmt, options, state) { |
178 |
| - const atRule = stmt.node |
179 |
| - let sourceFile |
180 |
| - if (atRule.source && atRule.source.input && atRule.source.input.file) { |
181 |
| - sourceFile = atRule.source.input.file |
182 |
| - } |
183 |
| - const base = sourceFile |
184 |
| - ? path.dirname(atRule.source.input.file) |
185 |
| - : options.root |
186 |
| - |
187 |
| - return Promise.resolve(options.resolve(stmt.uri, base, options)) |
188 |
| - .then(paths => { |
189 |
| - if (!Array.isArray(paths)) paths = [paths] |
190 |
| - // Ensure that each path is absolute: |
191 |
| - return Promise.all( |
192 |
| - paths.map(file => { |
193 |
| - return !path.isAbsolute(file) ? resolveId(file, base, options) : file |
194 | 104 | })
|
195 |
| - ) |
196 |
| - }) |
197 |
| - .then(resolved => { |
198 |
| - // Add dependency messages: |
199 |
| - resolved.forEach(file => { |
200 |
| - result.messages.push({ |
201 |
| - type: "dependency", |
202 |
| - plugin: "postcss-import", |
203 |
| - file: file, |
204 |
| - parent: sourceFile, |
205 |
| - }) |
206 |
| - }) |
| 105 | + } |
207 | 106 |
|
208 |
| - return Promise.all( |
209 |
| - resolved.map(file => { |
210 |
| - return loadImportContent(result, stmt, file, options, state) |
| 107 | + function applyStyles(bundle, styles) { |
| 108 | + styles.nodes = [] |
| 109 | + |
| 110 | + // Strip additional statements. |
| 111 | + bundle.forEach(stmt => { |
| 112 | + if (stmt.type === "import") { |
| 113 | + stmt.node.parent = undefined |
| 114 | + styles.append(stmt.node) |
| 115 | + } else if (stmt.type === "media") { |
| 116 | + stmt.node.parent = undefined |
| 117 | + styles.append(stmt.node) |
| 118 | + } else if (stmt.type === "nodes") { |
| 119 | + stmt.nodes.forEach(node => { |
| 120 | + node.parent = undefined |
| 121 | + styles.append(node) |
| 122 | + }) |
| 123 | + } |
211 | 124 | })
|
212 |
| - ) |
213 |
| - }) |
214 |
| - .then(result => { |
215 |
| - // Merge loaded statements |
216 |
| - stmt.children = result.reduce((result, statements) => { |
217 |
| - return statements ? result.concat(statements) : result |
218 |
| - }, []) |
219 |
| - }) |
220 |
| -} |
| 125 | + } |
221 | 126 |
|
222 |
| -function loadImportContent(result, stmt, filename, options, state) { |
223 |
| - const atRule = stmt.node |
224 |
| - const media = stmt.media |
225 |
| - if (options.skipDuplicates) { |
226 |
| - // skip files already imported at the same scope |
227 |
| - if (state.importedFiles[filename] && state.importedFiles[filename][media]) { |
228 |
| - return |
229 |
| - } |
230 |
| - |
231 |
| - // save imported files to skip them next time |
232 |
| - if (!state.importedFiles[filename]) state.importedFiles[filename] = {} |
233 |
| - state.importedFiles[filename][media] = true |
234 |
| - } |
| 127 | + function parseStyles(result, styles, options, state, media) { |
| 128 | + const statements = parseStatements(result, styles) |
| 129 | + |
| 130 | + return Promise.resolve(statements) |
| 131 | + .then(stmts => { |
| 132 | + // process each statement in series |
| 133 | + return stmts.reduce((promise, stmt) => { |
| 134 | + return promise.then(() => { |
| 135 | + stmt.media = joinMedia(media, stmt.media || []) |
| 136 | + |
| 137 | + // skip protocol base uri (protocol://url) or protocol-relative |
| 138 | + if ( |
| 139 | + stmt.type !== "import" || |
| 140 | + /^(?:[a-z]+:)?\/\//i.test(stmt.uri) |
| 141 | + ) { |
| 142 | + return |
| 143 | + } |
| 144 | + |
| 145 | + if (options.filter && !options.filter(stmt.uri)) { |
| 146 | + // rejected by filter |
| 147 | + return |
| 148 | + } |
| 149 | + |
| 150 | + return resolveImportId(result, stmt, options, state) |
| 151 | + }) |
| 152 | + }, Promise.resolve()) |
| 153 | + }) |
| 154 | + .then(() => { |
| 155 | + const imports = [] |
| 156 | + const bundle = [] |
| 157 | + |
| 158 | + // squash statements and their children |
| 159 | + statements.forEach(stmt => { |
| 160 | + if (stmt.type === "import") { |
| 161 | + if (stmt.children) { |
| 162 | + stmt.children.forEach((child, index) => { |
| 163 | + if (child.type === "import") imports.push(child) |
| 164 | + else bundle.push(child) |
| 165 | + // For better output |
| 166 | + if (index === 0) child.parent = stmt |
| 167 | + }) |
| 168 | + } else imports.push(stmt) |
| 169 | + } else if (stmt.type === "media" || stmt.type === "nodes") { |
| 170 | + bundle.push(stmt) |
| 171 | + } |
| 172 | + }) |
235 | 173 |
|
236 |
| - return Promise.resolve(options.load(filename, options)).then(content => { |
237 |
| - if (content.trim() === "") { |
238 |
| - result.warn(`${filename} is empty`, { node: atRule }) |
239 |
| - return |
240 |
| - } |
| 174 | + return imports.concat(bundle) |
| 175 | + }) |
| 176 | + } |
241 | 177 |
|
242 |
| - // skip previous imported files not containing @import rules |
243 |
| - if (state.hashFiles[content] && state.hashFiles[content][media]) return |
| 178 | + function resolveImportId(result, stmt, options, state) { |
| 179 | + const atRule = stmt.node |
| 180 | + let sourceFile |
| 181 | + if (atRule.source && atRule.source.input && atRule.source.input.file) { |
| 182 | + sourceFile = atRule.source.input.file |
| 183 | + } |
| 184 | + const base = sourceFile |
| 185 | + ? path.dirname(atRule.source.input.file) |
| 186 | + : options.root |
| 187 | + |
| 188 | + return Promise.resolve(options.resolve(stmt.uri, base, options)) |
| 189 | + .then(paths => { |
| 190 | + if (!Array.isArray(paths)) paths = [paths] |
| 191 | + // Ensure that each path is absolute: |
| 192 | + return Promise.all( |
| 193 | + paths.map(file => { |
| 194 | + return !path.isAbsolute(file) |
| 195 | + ? resolveId(file, base, options) |
| 196 | + : file |
| 197 | + }) |
| 198 | + ) |
| 199 | + }) |
| 200 | + .then(resolved => { |
| 201 | + // Add dependency messages: |
| 202 | + resolved.forEach(file => { |
| 203 | + result.messages.push({ |
| 204 | + type: "dependency", |
| 205 | + plugin: "postcss-import", |
| 206 | + file: file, |
| 207 | + parent: sourceFile, |
| 208 | + }) |
| 209 | + }) |
244 | 210 |
|
245 |
| - return processContent(result, content, filename, options).then( |
246 |
| - importedResult => { |
247 |
| - const styles = importedResult.root |
248 |
| - result.messages = result.messages.concat(importedResult.messages) |
| 211 | + return Promise.all( |
| 212 | + resolved.map(file => { |
| 213 | + return loadImportContent(result, stmt, file, options, state) |
| 214 | + }) |
| 215 | + ) |
| 216 | + }) |
| 217 | + .then(result => { |
| 218 | + // Merge loaded statements |
| 219 | + stmt.children = result.reduce((result, statements) => { |
| 220 | + return statements ? result.concat(statements) : result |
| 221 | + }, []) |
| 222 | + }) |
| 223 | + } |
249 | 224 |
|
| 225 | + function loadImportContent(result, stmt, filename, options, state) { |
| 226 | + const atRule = stmt.node |
| 227 | + const media = stmt.media |
250 | 228 | if (options.skipDuplicates) {
|
251 |
| - const hasImport = styles.some(child => { |
252 |
| - return child.type === "atrule" && child.name === "import" |
253 |
| - }) |
254 |
| - if (!hasImport) { |
255 |
| - // save hash files to skip them next time |
256 |
| - if (!state.hashFiles[content]) state.hashFiles[content] = {} |
257 |
| - state.hashFiles[content][media] = true |
| 229 | + // skip files already imported at the same scope |
| 230 | + if ( |
| 231 | + state.importedFiles[filename] && |
| 232 | + state.importedFiles[filename][media] |
| 233 | + ) { |
| 234 | + return |
258 | 235 | }
|
| 236 | + |
| 237 | + // save imported files to skip them next time |
| 238 | + if (!state.importedFiles[filename]) state.importedFiles[filename] = {} |
| 239 | + state.importedFiles[filename][media] = true |
259 | 240 | }
|
260 | 241 |
|
261 |
| - // recursion: import @import from imported file |
262 |
| - return parseStyles(result, styles, options, state, media) |
| 242 | + return Promise.resolve(options.load(filename, options)).then( |
| 243 | + content => { |
| 244 | + if (content.trim() === "") { |
| 245 | + result.warn(`${filename} is empty`, { node: atRule }) |
| 246 | + return |
| 247 | + } |
| 248 | + |
| 249 | + // skip previous imported files not containing @import rules |
| 250 | + if (state.hashFiles[content] && state.hashFiles[content][media]) |
| 251 | + return |
| 252 | + |
| 253 | + return processContent(result, content, filename, options).then( |
| 254 | + importedResult => { |
| 255 | + const styles = importedResult.root |
| 256 | + result.messages = result.messages.concat( |
| 257 | + importedResult.messages |
| 258 | + ) |
| 259 | + |
| 260 | + if (options.skipDuplicates) { |
| 261 | + const hasImport = styles.some(child => { |
| 262 | + return child.type === "atrule" && child.name === "import" |
| 263 | + }) |
| 264 | + if (!hasImport) { |
| 265 | + // save hash files to skip them next time |
| 266 | + if (!state.hashFiles[content]) state.hashFiles[content] = {} |
| 267 | + state.hashFiles[content][media] = true |
| 268 | + } |
| 269 | + } |
| 270 | + |
| 271 | + // recursion: import @import from imported file |
| 272 | + return parseStyles(result, styles, options, state, media) |
| 273 | + } |
| 274 | + ) |
| 275 | + } |
| 276 | + ) |
263 | 277 | }
|
264 |
| - ) |
265 |
| - }) |
| 278 | + }, |
| 279 | + } |
266 | 280 | }
|
267 | 281 |
|
268 |
| -module.exports = postcss.plugin("postcss-import", AtImport) |
| 282 | +AtImport.postcss = true |
| 283 | + |
| 284 | +module.exports = AtImport |
0 commit comments