From 7af006e8206f44a181c80a7276bbcc1828c2caab Mon Sep 17 00:00:00 2001 From: Jed Mao Date: Wed, 17 Jun 2015 13:48:19 -0500 Subject: [PATCH] Introduce plugins option --- README.md | 7 + index.js | 88 +++++---- package.json | 1 + test/fixtures/imports/bar-decl.css | 4 + test/fixtures/imports/foo-decl.css | 4 + test/fixtures/plugins.css | 2 + test/fixtures/plugins.expected.css | 6 + test/index.js | 292 ++++++++++++++++++----------- 8 files changed, 261 insertions(+), 143 deletions(-) create mode 100644 test/fixtures/imports/bar-decl.css create mode 100644 test/fixtures/imports/foo-decl.css create mode 100644 test/fixtures/plugins.css create mode 100644 test/fixtures/plugins.expected.css diff --git a/README.md b/README.md index 5c3fc042..48ee80bb 100755 --- a/README.md +++ b/README.md @@ -100,6 +100,13 @@ Default: `null` A function to transform the content of imported files. Take one argument (file content) & should return the modified content. +#### `plugins` + +Type: `Array` +Default: `undefined` + +An array of plugins to be applied on each imported file. + #### `encoding` Type: `String` diff --git a/index.js b/index.js index b27b4a8e..af3fe455 100755 --- a/index.js +++ b/index.js @@ -12,6 +12,11 @@ var helpers = require("postcss-message-helpers") var hash = require("string-hash") var glob = require("glob") +var Promise = global.Promise || require("es6-promise").Promise +var resolvedPromise = new Promise(function(resolvePromise) { + resolvePromise() +}) + /** * Constants */ @@ -72,22 +77,33 @@ function AtImport(options) { var hashFiles = {} - parseStyles( + return parseStyles( result, styles, opts, - insertRules, importedFiles, ignoredAtRules, null, - hashFiles - ) - addIgnoredAtRulesOnTop(styles, ignoredAtRules) + hashFiles, + createProcessor(result, options.plugins) + ).then(function() { + addIgnoredAtRulesOnTop(styles, ignoredAtRules) - if (typeof opts.onImport === "function") { - opts.onImport(Object.keys(importedFiles)) + if (typeof opts.onImport === "function") { + opts.onImport(Object.keys(importedFiles)) + } + }) + } +} + +function createProcessor(result, plugins) { + if (plugins) { + if (!Array.isArray(plugins)) { + throw new Error("plugins option must be an array") } + return postcss(plugins) } + return postcss() } /** @@ -100,11 +116,11 @@ function parseStyles( result, styles, options, - cb, importedFiles, ignoredAtRules, media, - hashFiles + hashFiles, + processor ) { var imports = [] styles.eachAtRule("import", function checkAtRule(atRule) { @@ -118,20 +134,20 @@ function parseStyles( imports.push(atRule) } }) - imports.forEach(function(atRule) { - helpers.try(function transformAtImport() { - readAtImport( + return Promise.all(imports.map(function(atRule) { + return helpers.try(function transformAtImport() { + return readAtImport( result, atRule, options, - cb, importedFiles, ignoredAtRules, media, - hashFiles + hashFiles, + processor ) }, atRule.source) - }) + })) } /** @@ -220,11 +236,11 @@ function readAtImport( result, atRule, options, - cb, importedFiles, ignoredAtRules, media, - hashFiles + hashFiles, + processor ) { // parse-import module parse entire line // @todo extract what can be interesting from this one @@ -246,7 +262,7 @@ function readAtImport( // detach detach(atRule) - return + return resolvedPromise } addInputToPath(options) @@ -264,7 +280,7 @@ function readAtImport( importedFiles[resolvedFilename][media] ) { detach(atRule) - return + return resolvedPromise } // save imported files to skip them next time @@ -273,17 +289,17 @@ function readAtImport( } importedFiles[resolvedFilename][media] = true - readImportedContent( + return readImportedContent( result, atRule, parsedAtImport, clone(options), resolvedFilename, - cb, importedFiles, ignoredAtRules, media, - hashFiles + hashFiles, + processor ) } @@ -294,7 +310,6 @@ function readAtImport( * @param {Object} parsedAtImport * @param {Object} options * @param {String} resolvedFilename - * @param {Function} cb */ function readImportedContent( result, @@ -302,11 +317,11 @@ function readImportedContent( parsedAtImport, options, resolvedFilename, - cb, importedFiles, ignoredAtRules, media, - hashFiles + hashFiles, + processor ) { // add directory containing the @imported file in the paths // to allow local import from this file @@ -328,7 +343,7 @@ function readImportedContent( if (fileContent.trim() === "") { result.warn(resolvedFilename + " is empty", {node: atRule}) detach(atRule) - return + return resolvedPromise } // skip files wich only contain @import rules @@ -339,7 +354,7 @@ function readImportedContent( // skip files already imported at the same scope and same hash if (hashFiles[fileContentHash] && hashFiles[fileContentHash][media]) { detach(atRule) - return + return resolvedPromise } // save hash files to skip them next time @@ -352,18 +367,27 @@ function readImportedContent( var newStyles = postcss.parse(fileContent, options) // recursion: import @import from imported file - parseStyles( + return parseStyles( result, newStyles, options, - cb, importedFiles, ignoredAtRules, parsedAtImport.media, - hashFiles + hashFiles, + processor ) - - cb(atRule, parsedAtImport, newStyles, resolvedFilename) + .then(function() { + return processor.process(newStyles) + .then(function(newResult) { + newResult.warnings().forEach(function(message) { + result.warn(message) + }) + }) + }) + .then(function() { + insertRules(atRule, parsedAtImport, newStyles) + }) } /** diff --git a/package.json b/package.json index 81aebe4b..0e7e16fd 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ ], "dependencies": { "clone": "^0.1.17", + "es6-promise": "^2.3.0", "glob": "^5.0.1", "object-assign": "^3.0.0", "postcss": "^4.1.4", diff --git a/test/fixtures/imports/bar-decl.css b/test/fixtures/imports/bar-decl.css new file mode 100644 index 00000000..f00d6f94 --- /dev/null +++ b/test/fixtures/imports/bar-decl.css @@ -0,0 +1,4 @@ +body { + bar: bar; + qux: qux; +} diff --git a/test/fixtures/imports/foo-decl.css b/test/fixtures/imports/foo-decl.css new file mode 100644 index 00000000..0a8b6aa8 --- /dev/null +++ b/test/fixtures/imports/foo-decl.css @@ -0,0 +1,4 @@ +body { + foo: foo; + baz: baz; +} diff --git a/test/fixtures/plugins.css b/test/fixtures/plugins.css new file mode 100644 index 00000000..c356bd6e --- /dev/null +++ b/test/fixtures/plugins.css @@ -0,0 +1,2 @@ +@import foo-decl; +@import bar-decl; diff --git a/test/fixtures/plugins.expected.css b/test/fixtures/plugins.expected.css new file mode 100644 index 00000000..dc1c10dd --- /dev/null +++ b/test/fixtures/plugins.expected.css @@ -0,0 +1,6 @@ +body { + baz: baz; +} +body { + qux: qux; +} diff --git a/test/index.js b/test/index.js index f59ec60c..2df946a5 100755 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,6 @@ var test = require("tape") +var assign = require("object-assign") var path = require("path") var fs = require("fs") @@ -14,20 +15,25 @@ function read(name) { } function compareFixtures(t, name, msg, opts, postcssOpts) { - opts = opts || {path: importsDir} - var actual = postcss() - .use(atImport(opts)) + opts = assign({path: importsDir}, opts || {}) + postcss(atImport(opts)) .process(read("fixtures/" + name), postcssOpts) - .css.trim() - var expected = read("fixtures/" + name + ".expected") - - // handy thing: checkout actual in the *.actual.css file - fs.writeFile("test/fixtures/" + name + ".actual.css", actual) + .then(trimResultCss) + .then(function(actual) { + var expected = read("fixtures/" + name + ".expected") + // handy thing: checkout actual in the *.actual.css file + fs.writeFile("test/fixtures/" + name + ".actual.css", actual) + t.equal(actual, expected, msg) + }) +} - t.equal(actual, expected, msg) +function trimResultCss(result) { + return result.css.trim() } test("@import", function(t) { + t.plan(15) + compareFixtures(t, "simple", "should import stylsheets") compareFixtures(t, "no-duplicate", "should not import a stylsheet twice") @@ -36,7 +42,6 @@ test("@import", function(t) { compareFixtures(t, "glob", "should handle a glob pattern", { root: __dirname, - path: importsDir, glob: true, }) @@ -44,9 +49,9 @@ test("@import", function(t) { t, "glob-alt", "should handle a glob pattern with single quote and/or url(...)", { - path: importsDir, - glob: true, - }) + glob: true, + }) + compareFixtures(t, "recursive", "should import stylsheets recursively") compareFixtures(t, "relative", "should import stylsheets relatively") @@ -54,10 +59,28 @@ test("@import", function(t) { compareFixtures(t, "empty-and-useless", "should work with empty files") compareFixtures(t, "transform", "should support transform", { - path: importsDir, transform: require("css-whitespace"), }) + compareFixtures(t, "plugins", "should apply plugins", { + plugins: [ + postcss.plugin("postcss-no-foo", function() { + return function(css) { + css.eachDecl("foo", function(decl) { + decl.removeSelf() + }) + } + }), + postcss.plugin("postcss-no-bar", function() { + return function(css) { + css.eachDecl("bar", function(decl) { + decl.removeSelf() + }) + } + }), + ], + }) + compareFixtures(t, "cwd", "should work without a specified path", {}) compareFixtures( @@ -72,83 +95,95 @@ test("@import", function(t) { t, "modules", "should be able to consume npm package or local modules", - {root: __dirname, path: importsDir} + {root: __dirname} ) var base = "@import url(http://)" - t.equal( - postcss() - .use(atImport()) - .process(base) - .css.trim(), - base, - "should not fail with only one absolute import" - ) - - t.equal( - postcss() - .use(atImport()) - .process( - "@import url('http://');\n@import 'test/fixtures/imports/foo.css';" + postcss() + .use(atImport()) + .process(base) + .then(trimResultCss) + .then(function(css) { + t.equal( + css, + base, + "should not fail with only one absolute import" ) - .css.trim(), - "@import url('http://');\nfoo{}", - "should not fail with absolute and local import" - ) + }) - t.end() + postcss() + .use(atImport()) + .process( + "@import url('http://');\n@import 'test/fixtures/imports/foo.css';" + ) + .then(trimResultCss) + .then(function(css) { + t.equal( + css, + "@import url('http://');\nfoo{}", + "should not fail with absolute and local import" + ) + }) }) test("@import error output", function(t) { var file = importsDir + "/import-missing.css" - t.throws( - function() { - postcss() - .use(atImport()) - .process(fs.readFileSync(file), {from: file}) - .css.trim() - }, - /* eslint-disable max-len */ - /import-missing.css:2:5: Failed to find 'missing-file.css' from .*\n\s+in \[/gm, - /* eslint-enabme max-len */ - "should output readable trace" - ) + postcss() + .use(atImport()) + .process(fs.readFileSync(file), {from: file}) + .catch(function(error) { + t.throws( + function() { + throw error + }, + /* eslint-disable max-len */ + /import-missing.css:2:5: Failed to find 'missing-file.css' from .*\n\s+in \[/gm, + /* eslint-enabme max-len */ + "should output readable trace" + ) - t.end() + t.end() + }) }) test("@import glob pattern matches no files", function(t) { var file = importsDir + "/glob-missing.css" - t.equal( - postcss() + postcss() .use(atImport({glob: true})) .process(fs.readFileSync(file), {from: file}) - .css.trim(), - "foobar{}", - "should fail silently, skipping the globbed import, if no files found" - ) + .then(trimResultCss) + .then(function(css) { + t.equal( + css, + "foobar{}", + "should fail silently, skipping the globbed import, if no files found" + ) - t.end() + t.end() + }) }) test("@import sourcemap", function(t) { - t.equal( - postcss() - .use(atImport()) - .process(read("sourcemap/in"), { - from: "./test/sourcemap/in.css", - to: null, - map: { - inline: true, - sourcesContent: true, - }, - }) - .css.trim(), - read("sourcemap/out"), - "should contain a correct sourcemap" - ) + postcss() + .use(atImport()) + .process(read("sourcemap/in"), { + from: "./test/sourcemap/in.css", + to: null, + map: { + inline: true, + sourcesContent: true, + }, + }) + .then(trimResultCss) + .then(function(css) { + t.equal( + css, + read("sourcemap/out"), + "should contain a correct sourcemap" + ) - t.end() + t.end() + }) }) test("@import callback", function(t) { @@ -173,41 +208,46 @@ test("@import callback", function(t) { .process(read("fixtures/recursive"), { from: "./test/fixtures/recursive.css", }) - .css.trim() + .then(trimResultCss) }) test("import relative files using path option only", function(t) { - t.equal( - postcss() - .use(atImport({path: "test/fixtures/imports/relative"})) - .process(read("fixtures/imports/relative/import")) - .css.trim(), - read("fixtures/imports/bar") - ) - t.end() + postcss() + .use(atImport({path: "test/fixtures/imports/relative"})) + .process(read("fixtures/imports/relative/import")) + .then(trimResultCss) + .then(function(css) { + t.equal( + css, + read("fixtures/imports/bar") + ) + + t.end() + }) }) test("inlined @import should keep PostCSS AST references clean", function(t) { - var root = postcss() + postcss() .use(atImport()) .process("@import 'test/fixtures/imports/foo.css';\nbar{}") - .root - root.nodes.forEach(function(node) { - t.equal(root, node.parent) - }) - - t.end() + .then(function(result) { + result.root.nodes.forEach(function(node) { + t.equal(result.root, node.parent) + }) + }) + .then(function() { + t.end() + }) }) test("works with no styles at all", function(t) { - t.doesNotThrow(function() { - postcss() - .use(atImport()) - .process("") - .css.trim() - }, "should works with nothing without throwing an error") - - t.end() + postcss() + .use(atImport()) + .process("") + .then(function() { + t.pass("should work with no styles without throwing an error") + t.end() + }) }) test("@import custom resolve", function(t) { @@ -232,23 +272,53 @@ test("@import custom resolve", function(t) { }) test("warn when a import doesn't have ;", function(t) { - t.equal( - postcss() - .use(atImport()) - .process("@import url('http://') :root{}") - .warnings()[0].text, - atImport.warnNodesMessage, - "should warn when a user didn't close an import with ;" - ) + t.plan(2) - t.equal( - postcss() - .use(atImport()) - .process("@import url('http://');") - .warnings().length, - 0, - "should not warn when a user closed an import with ;" - ) + postcss() + .use(atImport()) + .process("@import url('http://') :root{}") + .then(function(result) { + t.equal( + result.warnings()[0].text, + atImport.warnNodesMessage, + "should warn when a user didn't close an import with ;" + ) + }) - t.end() + postcss() + .use(atImport()) + .process("@import url('http://');") + .then(function(result) { + t.equal( + result.warnings().length, + 0, + "should not warn when a user closed an import with ;" + ) + }) +}) + +test("plugins option", function(t) { + t.plan(2) + + postcss() + .use(atImport({ + plugins: "foo", + })) + .process("") + .catch(function(error) { + t.equal( + error.message, + "plugins option must be an array", + "should error when value is not an array" + ) + }) + + postcss() + .use(atImport({ + plugins: [], + })) + .process("") + .then(function() { + t.pass("should remain silent when value is an empty array") + }) })