├── .gitignore ├── LICENCE ├── Makefile ├── README.md ├── bin └── npm-css ├── index.js ├── lib └── prefix.js ├── package.json └── test ├── expected ├── index.css ├── input.css ├── relative.css └── required.css ├── fixtures ├── foo │ └── index.css ├── index.css ├── input.css ├── localwidget │ ├── package.json │ └── style.css ├── relative.css └── required.css ├── mocha.opts └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | *.err -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Raynos. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | node ./bin/index.js \ 3 | ./test/fixtures/input.css \ 4 | -o output.css 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-css 2 | 3 | Require css from node modules. 4 | 5 | ## Syntax 6 | 7 | ``` css 8 | @import "typeahead"; 9 | 10 | @import "./foo.css"; 11 | @import "./bar/baz.css"; 12 | 13 | .foo { 14 | color: red 15 | } 16 | ``` 17 | 18 | npm-css reads css imports in the format of `@import " ... "` and inlines the CSS at that path for you. It also understands node modules. 19 | 20 | If you `@import` a folder it will try to look for a package.json file or read `index.css` by default. See the `package.json` section below. 21 | 22 | ## CLI 23 | 24 | To build a single css file from an entry.css with @import statements. 25 | 26 | `npm-css entry.css -o bundle.css` 27 | 28 | ## API 29 | 30 | If you want to build css files on the fly. 31 | 32 | ```javascript 33 | var npmcss = require('npm-css'); 34 | var css = npmcss('/path/to/file'); 35 | ``` 36 | 37 | ## package.json 38 | 39 | You can specify the stylesheet for your module by adding a `"style"` field to your package.json file. This works similarly to the `"main"` field. 40 | 41 | ## Installation 42 | 43 | `npm install npm-css` 44 | 45 | ## MIT Licenced 46 | -------------------------------------------------------------------------------- /bin/npm-css: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // builtin 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | // local 8 | var npmcss = require('..'); 9 | 10 | var optimist = require('optimist') 11 | .usage('Usage: npm-css [entry file] {OPTIONS}') 12 | .wrap(80) 13 | .option('outfile', { 14 | alias: 'o', 15 | desc: 'Write the bundled css to this file\n' + 16 | 'If unspecified the output will go to stdout' 17 | }) 18 | 19 | var argv = optimist.argv; 20 | 21 | var files = argv._; 22 | 23 | if (!files || files.length === 0) { 24 | console.error('Error: at least one entry file must be specified\n'); 25 | optimist.showHelp(); 26 | return; 27 | } 28 | 29 | var css = ''; 30 | 31 | files.forEach(function(file) { 32 | file = path.resolve(process.cwd(), file); 33 | css += npmcss(file); 34 | }); 35 | 36 | if (argv.outfile) { 37 | fs.writeFileSync(argv.outfile, css, 'utf8'); 38 | return; 39 | } 40 | 41 | process.stdout.write(css); 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // builtin 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | // vendor 6 | var resolve = require('resolve'); 7 | var cssp = require('cssp'); 8 | var traverse = require('traverse'); 9 | 10 | // local 11 | var prefix = require('./lib/prefix'); 12 | 13 | // replace nodes with tree 14 | // nodes is an array representing a node 15 | function replace(nodes, tree) { 16 | 17 | // truncate previous nodes 18 | nodes.splice(0, nodes.length); 19 | 20 | // insert tree as new nodes 21 | tree.forEach(function(node) { 22 | nodes.push(node); 23 | }); 24 | } 25 | 26 | // flatten @imports given a basepath, and a tree 27 | // inline all of the import statements into the tree 28 | // @return a tree with all statements inlined 29 | function flatten(file, modules) { 30 | var src = fs.readFileSync(file, 'utf8'); 31 | 32 | var base = path.dirname(file); 33 | var tree = cssp.parse(src); 34 | 35 | traverse(tree).forEach(function (node) { 36 | var self = this; 37 | 38 | if (node !== 'atrules') { 39 | return; 40 | } 41 | 42 | node = this.parent.node; 43 | 44 | // ignore non import 45 | if (node[1][0] !== 'atkeyword' && nodes[1][1][0] !== 'ident' && 46 | node[1][1][1] !== 'import') { 47 | return; 48 | } 49 | 50 | // ignore non string imports 51 | if (node[3][0] !== 'string') { 52 | return; 53 | } 54 | 55 | // remove quotes from imported name 56 | var name = node[3][1].replace(/["']/g, ''); 57 | 58 | // @import "module" 59 | if (name[0] !== '.') { 60 | 61 | // if user uses direct path to css file in module 62 | // we pluck the module name from the path start 63 | var pkg_name = name.split('/', 1)[0]; 64 | 65 | // lookup the entry css file 66 | var filepath = resolve.sync(name, { 67 | basedir: base, 68 | extensions: ['.css'], 69 | packageFilter: function (pkg) { 70 | pkg_name = pkg.name; 71 | pkg.main = pkg.style; 72 | return pkg; 73 | } 74 | }); 75 | 76 | if (modules.indexOf(filepath) !== -1) { 77 | return; 78 | } 79 | 80 | modules.push(filepath); 81 | 82 | // run css file through tree -> flatten -> prefix 83 | // get required module as a css tree 84 | // replace @import node with new tree 85 | return replace(self.parent.node, 86 | prefix(pkg_name, flatten(filepath, modules))); 87 | } 88 | 89 | var filepath = path.join(base, name); 90 | 91 | if (modules.indexOf(filepath) !== -1) { 92 | return; 93 | } 94 | 95 | modules.push(filepath); 96 | 97 | 98 | // path is a file 99 | if (fs.statSync(filepath).isFile()) { 100 | return replace(self.parent.node, flatten(filepath, modules)); 101 | } 102 | 103 | // path is a dir 104 | var pkginfo = path.join(filepath, 'package.json'); 105 | 106 | // package.json exists for path 107 | // use style info and prefix 108 | if (fs.existsSync(pkginfo)) { 109 | var info = JSON.parse(fs.readFileSync(pkginfo)); 110 | filepath = path.join(base, name, info.style || 'index.css') 111 | return replace(self.parent.node, 112 | prefix(info.name, flatten(filepath, modules))); 113 | } 114 | 115 | // no package.json, try index.css in the dir 116 | filepath = path.join(filepath, 'index.css'); 117 | 118 | // do not prefix if just loading index.css from a dir 119 | // allows for easier organization of multi css files without prefixing 120 | if (fs.existsSync(filepath)) { 121 | return replace(self.parent.node, 122 | flatten(filepath, modules)); 123 | } 124 | }); 125 | 126 | return tree; 127 | } 128 | 129 | // process given file for // @require statements 130 | // file should be /full/path/to/file.css 131 | module.exports = function(file) { 132 | return cssp.translate(flatten(file, [])); 133 | }; 134 | 135 | -------------------------------------------------------------------------------- /lib/prefix.js: -------------------------------------------------------------------------------- 1 | // vendor 2 | var traverse = require('traverse'); 3 | 4 | // prefix selectors of a css tree with a given name 5 | // if the selector has already been properly prefixed, it is not changed 6 | // modifies the tree in place 7 | // @return tree 8 | module.exports = function prefix(name, tree) { 9 | traverse(tree).forEach(function (node) { 10 | if (node !== 'simpleselector') { 11 | return; 12 | } 13 | 14 | var nodes = this.parent.node; 15 | 16 | // don't prefix if any of the following conditions are met: 17 | // .module exists as a parent then ok 18 | // .module exists in a set of classes right after an element 19 | // .module exists in the first set of classes 20 | for (var i=1 ; i", 7 | "repository": "git://github.com/shtylman/npm-css.git", 8 | "main": "index", 9 | "bin": { 10 | "npm-css": "bin/npm-css" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "Jake Verbaten" 15 | }, 16 | { 17 | "name": "Roman Shtylman" 18 | } 19 | ], 20 | "dependencies": { 21 | "resolve": "0.5.1", 22 | "traverse": "0.6.6", 23 | "cssp": "1.0.6", 24 | "optimist": "0.3.5" 25 | }, 26 | "devDependencies": { 27 | "mocha": "~1.13.0", 28 | "foobar-npm-css": "1.0.0" 29 | }, 30 | "licenses": [ 31 | { 32 | "type": "MIT", 33 | "url": "http://github.com/shtylman/npm-css/raw/master/LICENSE" 34 | } 35 | ], 36 | "scripts": { 37 | "test": "mocha" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/expected/index.css: -------------------------------------------------------------------------------- 1 | /* .foo {} */ 2 | .foo {} 3 | 4 | -------------------------------------------------------------------------------- /test/expected/input.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | 5 | /* foobar stuff */ 6 | 7 | /* .foobar */ 8 | .foobar-npm-css .foobar {} 9 | 10 | /* p.foobar */ 11 | .foobar-npm-css p.foobar {} 12 | 13 | /* .foobar p .foobar */ 14 | .foobar-npm-css .foobar p .foobar {} 15 | 16 | /* .foorbar p */ 17 | .foobar-npm-css .foobar p {} 18 | 19 | /* .foobar p */ 20 | .foobar-npm-css .foobar p {} 21 | 22 | /* .foobar.baz */ 23 | .foobar-npm-css .foobar.baz {} 24 | 25 | /* .foobar .baz */ 26 | .foobar-npm-css .foobar .baz {} 27 | 28 | /* .foobar .baz */ 29 | .foobar-npm-css .foobar .baz {} 30 | 31 | /* .baz.foobar */ 32 | .foobar-npm-css .baz.foobar {} 33 | 34 | /* .foobar .baz .foobar */ 35 | .foobar-npm-css .foobar .baz .foobar {} 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/expected/relative.css: -------------------------------------------------------------------------------- 1 | /* test require dir */ 2 | 3 | /* should load localwidget based on localwidget/package.json */ 4 | /* .localwidget .foo */ 5 | .localwidget .foo {} 6 | 7 | -------------------------------------------------------------------------------- /test/expected/required.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/foo/index.css: -------------------------------------------------------------------------------- 1 | /* .foo {} */ 2 | .foo {} 3 | -------------------------------------------------------------------------------- /test/fixtures/index.css: -------------------------------------------------------------------------------- 1 | @import "./foo"; 2 | -------------------------------------------------------------------------------- /test/fixtures/input.css: -------------------------------------------------------------------------------- 1 | @import "./required.css"; 2 | @import "foobar-npm-css/foobar"; 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/localwidget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "localwidget", 3 | "style": "style.css" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/localwidget/style.css: -------------------------------------------------------------------------------- 1 | /* .localwidget .foo */ 2 | .foo {} 3 | -------------------------------------------------------------------------------- /test/fixtures/relative.css: -------------------------------------------------------------------------------- 1 | /* test require dir */ 2 | 3 | /* should load localwidget based on localwidget/package.json */ 4 | @import "./localwidget"; 5 | -------------------------------------------------------------------------------- /test/fixtures/required.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui tdd 2 | --reporter list 3 | --check-leaks 4 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | 5 | var npmcss = require('../'); 6 | 7 | var kGenerate = process.env.TEST_GENERATE; 8 | 9 | var exp_dir = path.join(__dirname, 'expected'); 10 | var fix_dir = path.join(__dirname, 'fixtures'); 11 | 12 | var files = fs.readdirSync(fix_dir).filter(function(file) { 13 | return /[.]css$/.test(file); 14 | }); 15 | 16 | files.forEach(function(file) { 17 | var actual_file = path.join(fix_dir, file); 18 | var expected_file = path.join(exp_dir, file); 19 | 20 | test(file, function() { 21 | var actual = npmcss(actual_file); 22 | 23 | if (kGenerate) { 24 | fs.writeFileSync(expected_file, actual, 'utf8'); 25 | return; 26 | } 27 | 28 | var expected = fs.readFileSync(expected_file, 'utf8'); 29 | assert.equal(actual, expected); 30 | }); 31 | }); 32 | 33 | --------------------------------------------------------------------------------