├── .DS_Store ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── cmd.js └── help.txt ├── index.js ├── lib ├── asset.js ├── package.js └── parcel.js ├── package.json └── test ├── .DS_Store ├── cli ├── index.html ├── main.js ├── node_modules │ └── my-module │ │ ├── index.js │ │ ├── myModule.css │ │ ├── package.json │ │ └── template.tmpl ├── package.json ├── run-cli-tests.sh └── scripts │ ├── bundle.sh │ ├── cli-tests.sh │ └── watch.sh ├── node_modules ├── .DS_Store ├── base-css │ ├── base.css │ ├── index.js │ └── package.json ├── footer │ ├── header.css │ ├── index.js │ └── package.json ├── header │ ├── header.css │ ├── index.js │ └── package.json ├── my-module │ ├── index.js │ ├── myModule.css │ ├── myModule.tmpl │ └── package.json └── my-other-module │ ├── index.js │ ├── myOtherModuleBlue.css │ ├── myOtherModuleRed.css │ └── package.json ├── package.json ├── page1 ├── empty.css ├── main.js └── package.json ├── page2 ├── index.js ├── package.json └── styles.css ├── page3 ├── index.js └── package.json ├── page4 ├── main.js ├── main.scss ├── main.tmpl └── package.json ├── page5 └── main.js ├── page6 ├── main.js ├── main.scss └── package.json ├── page7 ├── main.css ├── main.js └── package.json ├── test.js ├── watch-test-output ├── bundle.css └── bundle.js └── watchTest.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotundasoftware/parcelify/8ba813a6a1e93bddf398c8d5042e9e7583254dbe/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### v1.2.0 4 | * added support for multiple javascript entry points via the `bundlesByEntryPoint` option 5 | 6 | ### v1.1.0 7 | * added support for single string as appTransformDirs 8 | * added single letter aliases for logLevel and appTransformDirs options 9 | 10 | ### v1.0.0 11 | * changed to browserify plugin architecture 12 | * now can be used with any compatible version of browserify 13 | * the `transforms` package.json no longer supports js transforms 14 | * the following API options were removed: packageTransform, browserifyInstance, browserifyBundleOptions 15 | 16 | ### v0.12.0 17 | * add log level option 18 | 19 | ### v0.11.0 20 | * BREAKING CHANGE: Corrected spelling of "appTranform" and "appTranformDir" to "appTransforms" and "appTransformDirs" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Rotunda Software, LLC. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parcelify 2 | 3 | Add css to your npm modules consumed with [browserify](http://browserify.org/). 4 | 5 | * Just add a `style` key to your `package.json` to specify the package's css file(s). 6 | * Efficiently transform scss / less to css, etc. using transform streams. 7 | * Rebuild css bundles automatically on changes in watch mode. 8 | * Leverage a robust API to create larger build tools like [cartero](https://github.com/rotundasoftware/cartero). 9 | 10 | Many thanks to [James Halliday](https://twitter.com/substack) for his help and guidance in bringing this project into reality. 11 | 12 | [](https://api.travis-ci.org/rotundasoftware/parcelify.svg?branch=master) 13 | 14 | ## How dat work? 15 | 16 | ``` 17 | ├── node_modules 18 | │ └── my-module 19 | │ ├── index.js 20 | │ ├── myModule.css 21 | │ └── package.json 22 | └── main.js 23 | ``` 24 | 25 | In my-module's `package.json`, the module's style assets just need to be enumerated (in [glob](https://github.com/isaacs/node-glob#glob-primer) notation): 26 | 27 | ``` 28 | { 29 | "name" : "my-module", 30 | "version": "1.5.0", 31 | "style" : "*.css" // glob notation. can optionally be an array 32 | } 33 | ``` 34 | 35 | In `main.js`, everything looks the same: 36 | 37 | ```javascript 38 | myModule = require( 'my-module' ); 39 | 40 | console.log( 'hello world' ); 41 | ``` 42 | 43 | Now just run parcelify as a [browserify plugin](https://github.com/substack/node-browserify#plugins) using browserify's `-p` flag: 44 | 45 | ``` 46 | $ browserify main.js -o bundle.js -p [ parcelify -o bundle.css ] 47 | ``` 48 | 49 | Parcelify will concatenate all the css files in the modules on which `main.js` depends -- in this case just `myModule.css` -- in the order of the js dependency graph, and write the output to `bundle.css`. 50 | 51 | Use the `-w` flag to keep the bundle up to date when changes are made in dev mode: 52 | 53 | ``` 54 | $ watchify main.js -o bundle.js -p [ parcelify -wo bundle.css ] 55 | ``` 56 | 57 | ## Installation 58 | 59 | In your project directory, 60 | 61 | ``` 62 | $ npm install parcelify 63 | ``` 64 | 65 | ## Plugin options 66 | 67 | ``` 68 | --cssBundle, -o Path of the destination css bundle. 69 | 70 | --watch, -w Watch mode - automatically rebuild css bundle as appropriate for changes. 71 | 72 | --transform, -t Name or path of an application transform. (See discussion of application transforms.) 73 | 74 | --transformDir, -d Path of an application transform directory. (See discussion of application transforms.) 75 | 76 | --loglevel -l Set the verbosity of npmlog, eg. "silent", "error", "warn", "info", "verbose" 77 | ``` 78 | 79 | ## Transforms 80 | 81 | ### Local (package specific) transforms 82 | 83 | The safest and most portable way to apply transforms like sass -> css is using the `transforms` key in a package's package.json. The key should be an array of names or file paths of [transform modules](https://github.com/substack/module-deps#transforms). For example, 84 | 85 | ``` 86 | { 87 | "name": "my-module", 88 | "description": "Example module.", 89 | "version": "1.5.0", 90 | "style" : "*.scss", 91 | "transforms" : [ "sass-css-stream" ], 92 | "dependencies" : { 93 | "sass-css-stream": "^0.0.1" 94 | } 95 | } 96 | ``` 97 | 98 | All transform modules are called on all assets. It is up to the transform module to determine whether or not it should apply itself to a file (usually based on the file extension). 99 | 100 | ### Application level transforms 101 | 102 | You can apply transforms to all packages within an entire branch of the directory tree using the `appTransforms` and `appTransformDirs` options or their corresponding command line arguments. (Packages inside a `node_modules` folder located inside one of the supplied directories are not effected.) For example, to transform all sass files inside the current working directory to css, 103 | 104 | ``` 105 | $ browserify main.js -o bundle.js -p [ parcelify -o bundle.css -t sass-css-stream -d . ] 106 | ``` 107 | 108 | ### Catalog of transforms 109 | 110 | The following transforms can be used with parcelify. Please let us know if you develop a transform and we'll include it in this list. 111 | 112 | * [sass-css-stream](https://github.com/rotundasoftware/sass-css-stream) - convert sass to css. 113 | * [less-css-stream](https://github.com/jsdf/less-css-stream) - convert less to css. 114 | * [sass-bourbon-transform](https://github.com/rotundasoftware/sass-bourbon-transform) - convert sass to css with [bourbon](http://bourbon.io/). 115 | * [css-img-datauri-stream](https://github.com/jbkirby/css-img-datauri-stream) - inline images in your css with data urls. 116 | * [parcelify-import-resolver](https://github.com/johanneslumpe/parcelify-import-resolver) - resolve paths using the node resolve algorithm. 117 | 118 | ## API 119 | 120 | First instantiate a parcelify instance. 121 | 122 | #### p = parcelify( b, [options] ) 123 | 124 | `b` is a browserify instance. Then you must call `b.bundle()` to start browserify, which will automatically trigger parcelify. Options are: 125 | 126 | * `bundles` - A hash that maps asset types to bundle paths. You will generally just want an entry for a `style` bundle, but arbitrary asset types are supported. Default: 127 | 128 | ```javascript 129 | bundles : { 130 | style : 'bundle.css' // bundle `style` assets and output here 131 | } 132 | ``` 133 | * `bundlesByEntryPoint` (default: undefined) - If multiple entry points have been supplied to the browserify instance, this option is used to determine the output bundles for each entry point, instead of `bundles`. For example: 134 | ``` 135 | { 136 | '/Users/me/myWebApp/views/page1/page1.js' : { style : 'static/page1.css' }, 137 | '/Users/me/myWebApp/views/page2/page2.js' : { style : 'static/page2.css' } 138 | } 139 | ``` 140 | * `appTransforms` (default: undefined) - An array of [transform modules](https://github.com/substack/module-deps#transforms) names / paths or functions to be applied to all packages in directories in the `appTransformDirs` array. 141 | * `appTransformDirs` (default: undefined) - `appTransforms` are applied to any packages that are within one of the directories in this array. (The recursive search is stopped on `node_module` directories.) 142 | * `logLevel` - set the [npmlog](https://www.npmjs.org/package/npmlog) logging level. 143 | * `watch` (default: false) - automatically rebuild bundles as appropriate for changes. 144 | 145 | A parcelify object is returned, which is an event emitter. 146 | 147 | ### p.on( 'done', function(){} ); 148 | Called when all bundles have been output. 149 | 150 | ### p.on( 'error', function( err ){} ); 151 | Called when an error occurs. 152 | 153 | ### p.on( 'packageCreated', function( package ){} ); 154 | Called when a new package is created. `package` is a package object as defined in `lib/package.js`. 155 | 156 | ### p.on( 'assetUpdated', function( eventType, asset ){} ); 157 | Called when a style asset is updated in watch mode. `eventType` is `'added'`, `'changed'`, or `'deleted'`, and `asset` is an asset object as defined in `lib/asset.js`. 158 | 159 | ## Client side templates and other assets 160 | 161 | Parcelify actually supports concatenation / enumeration of arbitrary asset types. Just add a bundle for an asset type in the `bundles` option and use the same key to enumerate assets of that type in package.json. 162 | 163 | A tempting use case for this feature is client side templates - just include a `template` key in package.json and a corresponding entry in the `bundles` option, and you have a bundle of client side templates. However, if you plan to share your packages we recommend against this practice as it makes your packages difficult to consume. Instead we recommend using a browserify transform like [nunjucksify](https://github.com/rotundasoftware/nunjucksify) or [node-hbsfy](https://github.com/epeli/node-hbsfy) to precompile templates and `require` them explicitly from your JavaScript files. 164 | 165 | For the case of assets like images, that do not need to be concatenated, you can specify a `null` path for the bundle. Parcelify will collect all assets of that type but not concatenate them. You can then process the individual assets further using the event callbacks. See [cartero](https://github.com/rotundasoftware/cartero) for an example of this more advanced use case. 166 | 167 | ### Command line usage (depreciated) 168 | 169 | You can also run parcelify directly from the command line, although this functionality is depreciated. Note browserify needs to be installed (or watchify, in the case that the -w flag is used). 170 | 171 | ``` 172 | $ parcelify main.js -o bundle.css 173 | ``` 174 | 175 | In addition to the options available when running parcelify as a browserify plugin, the follow options are also supported from the command line. 176 | 177 | ``` 178 | --jsBundle, -j Path to save the JavaScript bundle (i.e. browserify's output). 179 | 180 | --maps, -m Enable JavaScript source maps in js bundles (for dev mode). 181 | 182 | --help, -h Show this message 183 | ``` 184 | 185 | ## Contributors 186 | 187 | * [James Halliday](https://twitter.com/substack) (Initial design, sage advice, supporting modules) 188 | * [David Beck](https://twitter.com/davegbeck) 189 | * [Oleg Seletsky](https://github.com/go-oleg) 190 | 191 | ## License 192 | 193 | MIT 194 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var parcelify = require( '../' ); 4 | var browserify = require( 'browserify' ); 5 | var watchify = require( 'watchify' ); 6 | var minimist = require( 'minimist' ); 7 | var path = require( 'path' ); 8 | var fs = require( 'fs' ); 9 | var os = require( 'os' ); 10 | var tmpdir = ( os.tmpdir || os.tmpDir )(); 11 | 12 | var argv = minimist( process.argv.slice(2), 13 | { 14 | alias : { 15 | jsBundle : 'j', 16 | cssBundle : 'c', 17 | transform : 't', 18 | transformDirs : 'd', 19 | watch : 'w', 20 | maps : 'm', 21 | help : 'h' 22 | }, 23 | boolean : [ 'watch', 'help', 'maps' ] 24 | } 25 | ); 26 | 27 | if( argv.help ) { 28 | return fs.createReadStream( __dirname + '/help.txt' ).pipe( process.stdout ).on( 'close', function() { 29 | process.exit( 0 ); 30 | } ); 31 | } 32 | 33 | // resolve to absolute paths 34 | var jsBundle = resolvePath( argv.jsBundle ) || path.resolve( tmpdir, 'parcelify-js-bundle-' + Math.random() ); 35 | var cssBundle = resolvePath( argv.cssBundle ); 36 | var tmplBundle = resolvePath( argv.tmplBundle ); 37 | var mainPath = resolvePath( argv._[0] ); 38 | var appTransforms = argv.transform; 39 | var appTransformDirs = argv.transformDirs; 40 | var defaultTransforms = argv.transform; 41 | var logLevel = argv.loglevel; 42 | var watch = argv.watch; 43 | var maps = argv.maps; 44 | 45 | if( typeof appTransformDirs === 'string' ) appTransformDirs = [ appTransformDirs ]; 46 | 47 | if( ! mainPath ) { 48 | console.log( 'No entry point specified' ); 49 | process.exit( 1 ); 50 | } 51 | 52 | var browserifyInstance = browserify( mainPath ); 53 | if( watch ) watchify( browserifyInstance ); 54 | 55 | var p = parcelify( browserifyInstance, { 56 | bundles : { 57 | script : jsBundle, 58 | style : cssBundle, 59 | template : tmplBundle 60 | }, 61 | appTransforms : appTransforms, 62 | appTransformDirs : appTransformDirs, 63 | browserifyBundleOptions : { 64 | debug : maps 65 | }, 66 | watch : watch, 67 | logLevel : logLevel 68 | } ); 69 | 70 | browserifyInstance.bundle().pipe( fs.createWriteStream( jsBundle ) ); 71 | 72 | p.on( 'error', function( err ) { 73 | console.log( err.stack ); 74 | process.exit( 1 ); 75 | } ); 76 | 77 | p.on( 'done', function() { 78 | if( ! watch ) 79 | process.exit( 0 ); 80 | } ); 81 | 82 | function resolvePath( inputPath ) { 83 | return inputPath ? path.resolve( inputPath ) : inputPath; 84 | } 85 | -------------------------------------------------------------------------------- /bin/help.txt: -------------------------------------------------------------------------------- 1 | 2 | EXAMPLE: 3 | $ parcelify main.js -j bundle.js -c bundle.css 4 | 5 | OPTIONS: 6 | --cssBundle, -o Path of a destination css bundle. 7 | 8 | --jsBundle, -j Path of the JavaScript bundle (i.e. browserify's output). 9 | 10 | --watch, -w Watch mode - automatically rebuild bundles as appropriate for changes. 11 | 12 | --maps, -m Enable JavaScript source maps in js bundles (for dev mode). 13 | 14 | --transform, -t Name or path of an application transform. 15 | 16 | --transformDir Path of an application transform directory. 17 | 18 | --loglevel Set the verbosity of npmlog, eg. "silent", "error", "warn", "info", "verbose" 19 | 20 | --help, -h Show this message 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var parcelMap = require( 'parcel-map' ); 3 | var shasum = require( 'shasum' ); 4 | var through2 = require( 'through2' ); 5 | var path = require( 'path' ); 6 | var _ = require( 'underscore' ); 7 | var async = require( 'async' ); 8 | var glob = require( 'glob' ); 9 | var Package = require( './lib/package' ); 10 | var Parcel = require( './lib/parcel' ); 11 | var inherits = require( 'inherits' ); 12 | var log = require( 'npmlog' ); 13 | 14 | var EventEmitter = require('events').EventEmitter; 15 | var Package = require('./lib/package.js'); 16 | 17 | module.exports = Parcelify; 18 | 19 | inherits( Parcelify, EventEmitter ); 20 | 21 | function Parcelify( browserifyInstance, options ) { 22 | var _this = this; 23 | 24 | if( ! ( this instanceof Parcelify ) ) return new Parcelify( browserifyInstance, options ); 25 | 26 | options = _.defaults( {}, options, { 27 | bundles : {}, // ignored when bundlesByEntryPoint is provided 28 | bundlesByEntryPoint : undefined, // required when there are multiple entry points 29 | 30 | appTransforms : undefined, 31 | appTransformDirs : undefined, 32 | 33 | watch : undefined, 34 | logLevel : undefined, 35 | 36 | // used internally or in order to share packages between multiple parcelify instances 37 | existingPackages : undefined 38 | } ); 39 | 40 | // option aliases 41 | if( _.isUndefined( options.bundles.style ) ) options.bundles.style = options.o || 'bundle.css'; 42 | if( _.isUndefined( options.appTransforms ) ) options.appTransforms = options.t || []; 43 | if( _.isUndefined( options.appTransformDirs ) ) options.appTransformDirs = options.d || []; 44 | if( _.isUndefined( options.watch ) ) options.watch = options.w || false; 45 | if( _.isUndefined( options.logLevel ) ) options.logLevel = options.l; 46 | 47 | if( _.isString( options.appTransforms ) ) options.appTransforms = [ options.appTransforms ]; 48 | 49 | this.watching = false; 50 | 51 | if( options.logLevel ) log.level = options.logLevel; 52 | 53 | // before we jump the gun, return from this function so we can listen to events from the calling function 54 | process.nextTick( function() { 55 | var existingPackages = options.existingPackages || {}; 56 | var mappedAssets = {}; 57 | 58 | _this.on( 'error', function( err ) { 59 | log.error( '', err ); // otherwise errors kill our watch task. Especially bad for transform errors 60 | } ); 61 | 62 | if( options.watch ) { 63 | browserifyInstance.on( 'update', _.debounce( function( changedMains ) { 64 | _this.watching = true; 65 | 66 | var processParcelOptions = _.clone( options ); 67 | processParcelOptions.existingPackages = existingPackages; 68 | processParcelOptions.mappedAssets = mappedAssets; 69 | 70 | _this.processParcels( browserifyInstance, processParcelOptions, function( err ) { 71 | if( err ) _this.emit( 'error', err ); 72 | } ); 73 | }, 1000, true ) ); 74 | } 75 | 76 | var processParcelOptions = _.clone( options ); 77 | processParcelOptions.existingPackages = existingPackages; 78 | processParcelOptions.mappedAssets = mappedAssets; 79 | 80 | _this.processParcels( browserifyInstance, processParcelOptions, function( err ) { 81 | if( err ) _this.emit( 'error', err ); 82 | } ); 83 | } ); 84 | 85 | return _this; 86 | } 87 | 88 | Parcelify.prototype.processParcels = function( browserifyInstance, options, callback ) { 89 | var _this = this; 90 | 91 | var existingPackages = options.existingPackages || {}; 92 | var assetTypes; 93 | 94 | if( options.bundlesByEntryPoint ) { 95 | assetTypes = _.reduce( options.bundlesByEntryPoint, function( assetTypesMemo, bundlesForThisEntryPoint ) { 96 | return _.union( assetTypesMemo, _.keys( bundlesForThisEntryPoint ) ); 97 | }, [] ) 98 | } else assetTypes = _.keys( options.bundles ); 99 | 100 | var packages = _.reduce( existingPackages, function( memo, thisPackage, thisPackageId ) { 101 | memo[ thisPackage.path ] = thisPackage.package; 102 | return memo; 103 | }, {} ); 104 | 105 | var dependencies = _.reduce( existingPackages, function( memo, thisPackage, thisPackageId ) { 106 | memo[ thisPackage.path ] = _.map( thisPackage.dependencies, function( thisDependency ) { return thisDependency.path; } ); 107 | return memo; 108 | }, {} ); 109 | 110 | var parcelMapEmitter = parcelMap( browserifyInstance, { 111 | keys : assetTypes, 112 | files : options.mappedAssets, 113 | packages : packages, 114 | dependencies : dependencies 115 | } ); 116 | 117 | parcelMapEmitter.on( 'error', function( err ) { 118 | return callback( err ); 119 | } ); 120 | 121 | parcelMapEmitter.on( 'done', function( parcelMapResult ) { 122 | _this.instantiateParcelAndPackagesFromMap( parcelMapResult, existingPackages, assetTypes, options.appTransforms, options.appTransformDirs, function( err, packagesThatWereCreated ) { 123 | if( err ) return callback( err ); 124 | 125 | var parcelsThatWereCreated = []; 126 | 127 | process.nextTick( function() { 128 | async.series( [ function( nextSeries ) { 129 | // fire package events for any new packages 130 | _.each( packagesThatWereCreated, function( thisPackage ) { 131 | var isParcel = thisPackage.isParcel; 132 | 133 | log.verbose( 'Created new ' + ( isParcel ? 'parcel' : 'package' ) + ' ' + thisPackage.path + ' with id ' + thisPackage.id ); 134 | 135 | existingPackages[ thisPackage.id ] = thisPackage; 136 | if( isParcel ) { 137 | _this._setupParcelEventRelays( thisPackage ); 138 | parcelsThatWereCreated.push( thisPackage ); 139 | } 140 | 141 | thisPackage.on( 'error', function( err ) { 142 | _this.emit( 'error', err ); 143 | } ); 144 | 145 | _this.emit( 'packageCreated', thisPackage ); 146 | } ); 147 | 148 | nextSeries(); 149 | }, function( nextSeries ) { 150 | if( parcelsThatWereCreated.length > 1 && ! options.bundlesByEntryPoint ) { 151 | return nextSeries( new Error( 'Multiple entry points detected, but bundlesByEntryPoint option was not supplied.' ) ); 152 | } 153 | 154 | if( parcelsThatWereCreated.length === 1 && ! options.bundlesByEntryPoint ) { 155 | options.bundlesByEntryPoint = {}; 156 | options.bundlesByEntryPoint[ _.first( parcelsThatWereCreated ).mainPath ] = options.bundles; 157 | } 158 | 159 | // we are done copying packages and collecting our asset streams. Now write our bundles to disk. 160 | async.each( _.values( packagesThatWereCreated ), function( thisParcel, nextEach ) { 161 | if( ! thisParcel.isParcel ) return nextEach(); 162 | 163 | var thisParcelBundles = options.bundlesByEntryPoint[ thisParcel.mainPath ]; 164 | 165 | async.each( Object.keys( thisParcelBundles ), function( thisAssetType, nextEach ) { 166 | var thisBundlePath = thisParcelBundles[ thisAssetType ]; 167 | if( ! thisBundlePath ) return nextEach(); 168 | 169 | thisParcel.writeBundle( thisAssetType, thisBundlePath, function( err, bundleWasWritten ) { 170 | // don't stop writing other bundles if there was an error on this one. errors happen 171 | // frequently with transforms.. like invalid scss, etc. don't stop the show, just 172 | // keep going with our other bundles. 173 | 174 | if( err ) _this.emit( 'error', err ); 175 | else if( bundleWasWritten ) _this.emit( 'bundleWritten', thisBundlePath, thisAssetType, thisParcel, _this.watching ); 176 | 177 | nextEach(); 178 | } ); 179 | }, nextEach ); 180 | }, nextSeries ); 181 | }, function( nextSeries ) { 182 | if( options.watch ) { 183 | // we only create glob watchers for the packages that parcel added to the manifest. Again, we want to avoid doubling up 184 | // work in situations where we have multiple parcelify instances running that share common bundles 185 | _.each( packagesThatWereCreated, function( thisPackage ) { 186 | thisPackage.createWatchers( assetTypes, browserifyInstance._options.packageFilter, options.appTransforms, options.appTransformDirs ); 187 | if( thisPackage.isParcel ) { 188 | thisPackage.attachWatchListeners( options.bundlesByEntryPoint[ thisPackage.mainPath ] ); 189 | } 190 | } ); 191 | } 192 | 193 | if( ! _this.watching ) _this.emit( 'done' ); 194 | 195 | nextSeries(); 196 | } ], callback ); 197 | } ); 198 | 199 | return callback( null ); 200 | } ); 201 | } ); 202 | }; 203 | 204 | Parcelify.prototype.instantiateParcelAndPackagesFromMap = function( parcelMapResult, existingPacakages, assetTypes, appTransforms, appTransformDirs, callback ) { 205 | var _this = this; 206 | var mappedParcel = null; 207 | var packagesThatWereCreated = {}; 208 | 209 | async.series( [ function( nextSeries ) { 210 | async.each( Object.keys( parcelMapResult.packages ), function( thisPackageId, nextPackageId ) { 211 | var packageJson = parcelMapResult.packages[ thisPackageId ]; 212 | var packageOptions = {}; 213 | 214 | async.waterfall( [ function( nextWaterfall ) { 215 | Package.getOptionsFromPackageJson( thisPackageId, packageJson.__path, packageJson, assetTypes, appTransforms, appTransformDirs, nextWaterfall ); 216 | }, function( packageOptions, nextWaterfall ) { 217 | var thisPackage; 218 | 219 | var thisPackageIsAParcel = packageJson.__isParcel; 220 | 221 | if( ! existingPacakages[ thisPackageId ] ) { 222 | if( thisPackageIsAParcel ) { 223 | thisPackage = packagesThatWereCreated[ thisPackageId ] = new Parcel( _.extend( packageOptions, { mainPath : packageJson.__mainPath } ) ); 224 | } 225 | else thisPackage = packagesThatWereCreated[ thisPackageId ] = new Package( packageOptions ); 226 | 227 | thisPackage.createAllAssets( assetTypes ); 228 | } 229 | else if( thisPackageIsAParcel && ! existingPacakages[ thisPackageId ] instanceof Parcel ) { 230 | // k tricky here.. if this package is a parcel, but it exists in the manifest as a plain 231 | // old package, then we gotta recreate this package as a parcel. also we have to update 232 | // any parcels that are dependENTS of this package/parcel in order to use the new 233 | // assets that we are about to create. man, scary, hope nothing gets broke in the process. 234 | // we could also pre-preemptively list out which packages are parcels by adding an option 235 | // to parcelify itself, but that seems a little weird. In the context of cartero that 236 | // depends on the path of each package relative to the parcelDirs cartero option. 237 | var oldPackage = existingPacakages[ thisPackageId ]; 238 | var oldDependentParcels = oldPackage.dependentParcels; 239 | 240 | oldPackage.destroy(); 241 | 242 | thisPackage = packagesThatWereCreated[ thisPackageId ] = new Parcel( packageOptions ); 243 | thisPackage.createAllAssets( assetTypes ); 244 | 245 | oldDependentParcels.forEach( function( thisDependentParcel ) { 246 | thisPackage.addDependentParcel( thisDependentParcel ); 247 | thisDependentParcel.calcSortedDependencies(); 248 | thisDependentParcel.calcParcelAssets( assetTypes ); 249 | } ); 250 | 251 | log.warn( '', 'Recreated package at ' + thisPackage.path + ' as Parcel.' ); 252 | } else 253 | thisPackage = existingPacakages[ thisPackageId ]; 254 | 255 | nextWaterfall(); 256 | } ], nextPackageId ); 257 | }, nextSeries ); 258 | }, function( nextSeries ) { 259 | var allPackages = _.extend( {}, existingPacakages, packagesThatWereCreated ); 260 | 261 | // now that we have all our packages instantiated, hook up dependencies 262 | _.each( parcelMapResult.dependencies, function( dependencyIds, thisPackageId ) { 263 | var thisPackage = allPackages[ thisPackageId ]; 264 | 265 | if( ! thisPackage ) return nextSeries( new Error( 'Unknown package id in dependency ' + thisPackageId ) ); 266 | 267 | var thisPackageDependencies = _.map( dependencyIds, function( thisDependencyId ) { return allPackages[ thisDependencyId ]; } ); 268 | thisPackage.setDependencies( thisPackageDependencies ); 269 | } ); 270 | 271 | // finally, we can calculate the topo sort of any parcels that were created 272 | _.each( packagesThatWereCreated, function( thisParcel ) { 273 | if( thisParcel.isParcel ) { 274 | thisParcel.calcSortedDependencies(); 275 | thisParcel.calcParcelAssets( assetTypes ); 276 | 277 | _.each( thisParcel.sortedDependencies, function( thisDependentPackage ) { 278 | thisDependentPackage.addDependentParcel( thisParcel ); 279 | } ); 280 | } 281 | } ); 282 | 283 | nextSeries(); 284 | } ], function( err ) { 285 | return callback( err, packagesThatWereCreated ); 286 | } ); 287 | }; 288 | 289 | Parcelify.prototype._setupParcelEventRelays = function( parcel ) { 290 | var _this = this; 291 | var eventsToRelay = [ 'assetUpdated', 'packageJsonUpdated' ]; 292 | 293 | eventsToRelay.forEach( function( thisEvent ) { 294 | parcel.on( thisEvent, function() { 295 | var args = Array.prototype.slice.call( arguments ); 296 | _this.emit.apply( _this, [].concat( thisEvent, args ) ); 297 | } ); 298 | } ); 299 | 300 | parcel.on( 'bundleUpdated', function( bundlePath, assetType ) { 301 | _this.emit( 'bundleWritten', bundlePath, assetType, parcel, true ); 302 | } ); 303 | }; -------------------------------------------------------------------------------- /lib/asset.js: -------------------------------------------------------------------------------- 1 | var fs = require( 'fs' ); 2 | var async = require( 'async' ); 3 | var mkdirp = require( 'mkdirp' ); 4 | var combine = require( 'stream-combiner' ); 5 | var path = require( 'path' ); 6 | var _ = require( 'underscore' ); 7 | 8 | module.exports = Asset; 9 | 10 | function Asset( srcPath, type, transforms, appData ) { 11 | this.srcPath = srcPath; 12 | this.type = type; 13 | this.transforms = transforms; 14 | this.appData = appData; 15 | } 16 | 17 | Asset.prototype.addTransform = function( transform, prepend ) { 18 | if( _.isUndefined( prepend ) ) prepend = false; 19 | 20 | if( prepend ) this.transforms.unshift( transform ); 21 | else this.transforms.push( transform ); 22 | }; 23 | 24 | Asset.prototype.createReadStream = function() { 25 | var stream = fs.createReadStream( this.srcPath ); 26 | return this._applyTransforms( stream, this.transforms ); 27 | }; 28 | 29 | Asset.prototype.writeToDisk = function( dstPath, callback ) { 30 | var _this = this; 31 | 32 | this.dstPath = dstPath; // save this for later 33 | 34 | async.series( [ function( nextSeries ) { 35 | mkdirp( path.dirname( dstPath ), nextSeries ); 36 | }, function( nextSeries ) { 37 | var stream = _this.createReadStream(); 38 | stream.on( 'error', function( err ) { 39 | nextSeries( new Error( 'While reading or transforming "' + _this.srcPath + '":\n' + err.message ) ); 40 | } ); 41 | stream.on( 'end', nextSeries ); 42 | stream.pipe( fs.createWriteStream( dstPath ) ); 43 | } ], callback ); 44 | }; 45 | 46 | Asset.prototype._applyTransforms = function( stream, transforms ) { 47 | var _this = this; 48 | 49 | if( ! transforms || transforms.length === 0 ) return stream; 50 | 51 | var combinedStream = combine.apply( null, transforms.map( function( thisTransform ) { 52 | return thisTransform( _this.srcPath ); 53 | } ) ); 54 | 55 | return stream.pipe( combinedStream ); 56 | }; 57 | -------------------------------------------------------------------------------- /lib/package.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require( 'fs' ); 3 | var inherits = require( 'inherits' ); 4 | var EventEmitter = require( 'events' ).EventEmitter; 5 | var _ = require( 'underscore' ); 6 | var async = require( 'async' ); 7 | var glob = require( 'glob' ); 8 | var globwatcher = require( 'globwatcher' ).globwatcher; 9 | var Asset = require( './asset' ); 10 | var resolve = require( 'resolve' ); 11 | var log = require( 'npmlog' ); 12 | 13 | module.exports = Package; 14 | 15 | inherits( Package, EventEmitter ); 16 | 17 | function Package( options ) { 18 | 19 | _.extend( this, _.pick( options, 20 | 'id', 21 | 'package', 22 | 'path', 23 | 'dependencies', 24 | 'assetSrcPathsByType', 25 | 'assetGlobsByType', 26 | 'assetTransformsByType' 27 | ) ); 28 | 29 | this.dependencies = []; 30 | this.dependentParcels = []; 31 | this.assetsByType = {}; 32 | 33 | EventEmitter.call( this ); 34 | } 35 | 36 | Package.prototype.createAllAssets = function( assetTypes ) { 37 | var _this = this; 38 | 39 | _this.assetsByType = {}; 40 | assetTypes.forEach( function( thisAssetType ) { _this.assetsByType[ thisAssetType ] = []; } ); 41 | 42 | Object.keys( this.assetSrcPathsByType ).forEach( function( assetType ) { 43 | _this.assetSrcPathsByType[ assetType ].forEach( function( thisAssetSrcPath ) { 44 | var thisAsset = _this.createAsset( thisAssetSrcPath, assetType ); 45 | } ); 46 | } ); 47 | }; 48 | 49 | Package.prototype.createAsset = function( thisAssetSrcPath, assetType, appData ) { 50 | var thisAsset = new Asset( thisAssetSrcPath, assetType, _.clone( this.assetTransformsByType[ assetType ] ), appData ); 51 | 52 | log.verbose( '', assetType + ' asset registered "%s"', path.relative( process.cwd(), thisAssetSrcPath ) ); 53 | 54 | if( ! this.assetsByType[ assetType ] ) this.assetsByType[ assetType ] = []; 55 | this.assetsByType[ assetType ].push( thisAsset ); 56 | 57 | return thisAsset; 58 | }; 59 | 60 | Package.prototype.getAssets = function( types ) { 61 | return _.reduce( this.assetsByType, function( memo, assetsOfThisType, thisAssetType ) { 62 | if( types && ! _.contains( types, thisAssetType ) ) return memo; 63 | 64 | return memo.concat( assetsOfThisType ); 65 | }, [] ); 66 | }; 67 | 68 | Package.prototype.addTransform = function( transform, transformOptions, toAssetTypes, prepend ) { 69 | if( _.isUndefined( prepend ) ) prepend = false; 70 | 71 | var t = transformOptions ? function( file ) { return transform( file, transformOptions ); } : transform; 72 | 73 | toAssetTypes = toAssetTypes || Object.keys( this.assetsByType ); 74 | if( ! _.isArray( toAssetTypes ) ) toAssetTypes = [ toAssetTypes ]; 75 | 76 | // add transform to existing assets 77 | this.getAssets( toAssetTypes ).forEach( function( thisAsset ) { 78 | thisAsset.addTransform( t, prepend ); 79 | } ); 80 | 81 | // and also add it to the package itself so it is added to assets created from this point forward 82 | _.each( _.pick( this.assetTransformsByType, toAssetTypes ), function( transformsForThisAssetType ) { 83 | if( prepend ) transformsForThisAssetType.unshift( t ); 84 | else transformsForThisAssetType.push( t ); 85 | } ); 86 | }; 87 | 88 | Package.prototype.setDependencies = function( dependencies ) { 89 | this.dependencies = dependencies; 90 | }; 91 | 92 | Package.prototype.addDependentParcel = function( parcel ) { 93 | this.dependentParcels = _.union( this.dependentParcels, parcel ); 94 | }; 95 | 96 | Package.prototype.createWatchers = function( assetTypes, packageFilter, appTransforms, appTransformDirs ) { 97 | this._createPackageJsonWatcher( assetTypes, packageFilter, appTransforms, appTransformDirs ); 98 | this._createAssetGlobWatchers(); 99 | }; 100 | 101 | Package.prototype.destroy = function() { 102 | this._destroyAssetGlobWatchers(); 103 | this.assetJsonWatcher.close(); 104 | }; 105 | 106 | /********************* Private instance methods *********************/ 107 | 108 | Package.prototype._createPackageJsonWatcher = function( assetTypes, packageFilter, appTransforms, appTransformDirs ) { 109 | var _this = this; 110 | 111 | this.assetJsonWatcher = globwatcher( path.resolve( this.path, "package.json" ) ); 112 | this.assetJsonWatcher.on( 'changed', function( srcPath ) { 113 | log.info( 'watch', 'package.json changed "%s"', path.relative( process.cwd(), srcPath ) ); 114 | 115 | fs.readFile( srcPath, 'utf8', function( err, packageJson ) { 116 | if( err ) return _this.emit( 'error', err ); 117 | 118 | try { 119 | packageJson = JSON.parse( packageJson ); 120 | } catch( err ) { 121 | return _this.emit( 'error', new Error( 'While parsing "' + srcPath + '", ' + err ) ); 122 | } 123 | 124 | packageJson.__path = _this.path; 125 | 126 | if( packageFilter ) packageJson = packageFilter( packageJson, _this.path ); 127 | 128 | Package.getOptionsFromPackageJson( _this.id, _this.path, packageJson, assetTypes, appTransforms, appTransformDirs, function( err, options ) { 129 | if( err ) return _this.emit( 'error', err ); 130 | 131 | _.extend( _this, options ); 132 | 133 | _this.createAllAssets( assetTypes ); 134 | 135 | _this._destroyAssetGlobWatchers(); 136 | _this._createAssetGlobWatchers(); 137 | 138 | _this._emitEventOnRelevantParcels( 'packageJsonUpdated', _this ); 139 | } ); 140 | } ); 141 | } ); 142 | }; 143 | 144 | Package.prototype._createAssetGlobWatchers = function() { 145 | var _this = this; 146 | 147 | this.assetGlobWatchers = []; 148 | 149 | _.each( _this.assetGlobsByType, function( globs, thisAssetType ) { 150 | var thisWatcher = globwatcher( globs ); 151 | 152 | thisWatcher.on( 'changed', function( srcPath ) { 153 | try { 154 | log.info( 'watch', '"%s" changed', path.relative( process.cwd(), srcPath ) ); 155 | 156 | var asset = _.findWhere( _this.assetsByType[ thisAssetType ], { srcPath : srcPath } ); 157 | if( ! asset ) return _this.emit( 'error', new Error( 'Couldn\'t find changed file ' + srcPath + ' in assets of type ' + thisAssetType ) ); 158 | 159 | _this._emitEventOnRelevantParcels( 'assetUpdated', 'changed', asset, _this ); 160 | } catch( err ) { 161 | return _this.emit( 'error', err ); 162 | } 163 | } ); 164 | 165 | thisWatcher.on( 'added', function( srcPath ) { 166 | try { 167 | log.info( 'watch', '"%s" added', path.relative( process.cwd(), srcPath ) ); 168 | 169 | var asset = _.findWhere( _this.assetsByType[ thisAssetType ], { srcPath : srcPath } ); 170 | // watching is weird... sometimes we get double events. make sure we don't add the same asset twice. 171 | if( asset ) return _this.emit( 'error', new Error( 'Asset ' + srcPath + ' already exists in assets of type ' + thisAssetType ) ); 172 | 173 | asset = _this.createAsset( srcPath, thisAssetType ); 174 | 175 | _this._emitEventOnRelevantParcels( 'assetUpdated', 'added', asset, _this ); 176 | } catch( err ) { 177 | return _this.emit( 'error', err ); 178 | } 179 | } ); 180 | 181 | thisWatcher.on( 'deleted', function( srcPath ) { 182 | try { 183 | log.info( 'watch', '"%s" deleted', path.relative( process.cwd(), srcPath ) ); 184 | 185 | var asset = _.findWhere( _this.assetsByType[ thisAssetType ], { srcPath : srcPath } ); 186 | if( ! asset ) return _this.emit( 'error', new Error( 'Couldn\'t find changed file ' + srcPath + ' in assets of type ' + thisAssetType ) ); 187 | 188 | _this.assetsByType[ thisAssetType ] = _.without( _this.assetsByType[ thisAssetType ], asset ); 189 | 190 | _this._emitEventOnRelevantParcels( 'assetUpdated', 'deleted', asset, _this ); 191 | } catch( err ) { 192 | return _this.emit( 'error', err ); 193 | } 194 | } ); 195 | 196 | _this.assetGlobWatchers.push( thisWatcher ); 197 | } ); 198 | }; 199 | 200 | Package.prototype._destroyAssetGlobWatchers = function() { 201 | this.assetGlobWatchers.forEach( function( thisAssetGlobWatcher ) { 202 | thisAssetGlobWatcher.close(); 203 | } ); 204 | 205 | this.assetGlobWatchers = []; 206 | }; 207 | 208 | Package.prototype._emitEventOnRelevantParcels = function() { 209 | var args = Array.prototype.slice.call( arguments ); 210 | 211 | this.dependentParcels.forEach( function( thisParcel ) { 212 | thisParcel.emit.apply( thisParcel, args ); 213 | } ); 214 | }; 215 | 216 | /********************* Static class methods *********************/ 217 | 218 | Package.getOptionsFromPackageJson = function( packageId, packagePath, packageJson, assetTypes, appTransforms, appTransformDirs, callback ) { 219 | var packageOptions = {}; 220 | 221 | if( appTransforms ) { 222 | var pkgIsInAppTransformsDir = _.find( appTransformDirs, function( thisAppDirPath ) { 223 | var relPath = path.relative( thisAppDirPath, packagePath ); 224 | var needToBackup = relPath.charAt( 0 ) === '.' && relPath.charAt( 1 ) === '.'; 225 | var appTransformsApplyToThisDir = ! needToBackup && relPath.indexOf( 'node_modules' ) === -1; 226 | return appTransformsApplyToThisDir; 227 | } ); 228 | 229 | if( pkgIsInAppTransformsDir ) 230 | packageJson.transforms = appTransforms.concat( packageJson.transforms || [] ); 231 | } 232 | 233 | packageOptions.package = packageJson; 234 | packageOptions.id = packageId; 235 | packageOptions.path = packagePath; 236 | 237 | packageOptions.assetSrcPathsByType = {}; 238 | packageOptions.assetTransformsByType = {}; 239 | packageOptions.assetGlobsByType = {}; 240 | 241 | async.each( assetTypes, function( thisAssetType, nextAssetType ) { 242 | 243 | async.parallel( [ function( nextParallel ) { 244 | packageOptions.assetSrcPathsByType[ thisAssetType ] = []; 245 | 246 | // resolve relative globs to absolute globs 247 | var relativeGlobsOfThisType = packageJson[ thisAssetType ] || []; 248 | if( _.isString( relativeGlobsOfThisType ) ) relativeGlobsOfThisType = [ relativeGlobsOfThisType ]; 249 | var absoluteGlobsOfThisType = relativeGlobsOfThisType.map( function( thisGlob ) { return path.resolve( packagePath, thisGlob ); } ); 250 | packageOptions.assetGlobsByType[ thisAssetType ] = absoluteGlobsOfThisType; 251 | 252 | // resolve absolute globs to actual src files 253 | async.map( absoluteGlobsOfThisType, glob, 254 | function( err, arrayOfResolvedGlobs ) { 255 | if( err ) return nextParallel( err ); 256 | 257 | var assetsOfThisType = _.flatten( arrayOfResolvedGlobs ); 258 | packageOptions.assetSrcPathsByType[ thisAssetType ] = assetsOfThisType; 259 | 260 | nextParallel(); 261 | } ); 262 | }, function( nextParallel ) { 263 | // resolve transform names to actual transform 264 | packageOptions.assetTransformsByType[ thisAssetType ] = []; 265 | 266 | if( packageJson.transforms ) { 267 | if( _.isArray( packageJson.transforms ) ) 268 | transformNames = packageJson.transforms; 269 | else 270 | transformNames = packageJson.transforms[ thisAssetType ] || []; 271 | } 272 | else 273 | transformNames = []; 274 | 275 | async.map( transformNames, function( thisTransformName, nextTransform ) { 276 | if( _.isFunction( thisTransformName ) ) return nextTransform( null, thisTransformName ); 277 | 278 | resolve( thisTransformName, { basedir : packageJson.__path }, function( err, modulePath ) { 279 | if( err ) return nextTransform( err ); 280 | 281 | nextTransform( null, require( modulePath ) ); 282 | } ); 283 | }, function( err, transforms ) { 284 | if( err ) return nextParallel( err ); 285 | 286 | 287 | packageOptions.assetTransformsByType[ thisAssetType ] = transforms; 288 | nextParallel(); 289 | } ); 290 | } ], nextAssetType ); 291 | }, function( err ) { 292 | if( err ) return callback( err ); 293 | 294 | callback( null, packageOptions ); 295 | } ); 296 | }; 297 | -------------------------------------------------------------------------------- /lib/parcel.js: -------------------------------------------------------------------------------- 1 | var inherits = require( 'inherits' ); 2 | var Package = require( './package' ); 3 | var _ = require( 'underscore' ); 4 | var async = require( 'async' ); 5 | var toposort = require( 'toposort' ); 6 | var through2 = require( 'through2' ); 7 | var path = require( 'path' ); 8 | var crypto = require( 'crypto' ); 9 | var fs = require( 'fs' ); 10 | 11 | module.exports = Parcel; 12 | 13 | inherits( Parcel, Package ); 14 | 15 | function Parcel( options ) { 16 | var _this = this; 17 | 18 | Package.call( this, options ); 19 | 20 | this.mainPath = options.mainPath; 21 | this.isParcel = true; 22 | this.bundlePathsByType = {}; 23 | this.parcelAssetsByType = {}; 24 | 25 | this.dependentParcels.push( this ); // parcels depend on themselves! 26 | } 27 | 28 | Parcel.prototype.calcSortedDependencies = function() { 29 | var packagesWithDependencies = []; 30 | 31 | function getEdgesForPackageDependencyGraph( thisPackage, thisTreeLevel, packageTreeLevels ) { 32 | if( _.isUndefined( thisTreeLevel ) ) thisTreeLevel = 0; 33 | if( _.isUndefined( packageTreeLevels ) ) packageTreeLevels = {}; 34 | 35 | if( ! packageTreeLevels[ thisPackage.path ] ) packageTreeLevels[ thisPackage.path ] = thisTreeLevel; 36 | 37 | return thisPackage.dependencies.reduce( function( memo, thisDependentPackage ) { 38 | // these conditionals are to avoid cycles and infinite recursion. 39 | // first, we only traverse each node once to avoid infinite recursion. 40 | if( _.isUndefined( packageTreeLevels[ thisDependentPackage.path ] ) ) { 41 | memo = memo.concat( getEdgesForPackageDependencyGraph( thisDependentPackage, thisTreeLevel + 1, packageTreeLevels ) ); 42 | } 43 | 44 | // second, we keep track of the levels of the nodes in the dependency tree (where 45 | // level 0 is the root node i.e. the parcel itself). nodes can only have dependencies 46 | // on other nodes with a level equal to or greater than their own. done. 47 | if( packageTreeLevels[ thisDependentPackage.path ] >= packageTreeLevels[ thisPackage.path ] ) { 48 | memo = memo.concat( [ [ thisPackage, thisDependentPackage ] ] ); 49 | } 50 | 51 | return memo; 52 | }, [] ); 53 | } 54 | 55 | var edges = getEdgesForPackageDependencyGraph( this ); 56 | var sortedPackages = toposort( edges ).reverse(); 57 | 58 | //sortedPackages = _.union( sortedPackages, Object.keys( packageManifest ) ); // union cuz some packages have no dependencies! 59 | sortedPackages = _.without( sortedPackages, this ); 60 | 61 | this.sortedDependencies = sortedPackages; 62 | }; 63 | 64 | Parcel.prototype.calcParcelAssets = function( assetTypes ) { 65 | memo = {}; 66 | assetTypes.forEach( function( thisAssetType ) { memo[ thisAssetType ] = []; } ); 67 | 68 | var sortedAssets = this.sortedDependencies.concat( this ).reduce( function( memo, thisPackage ) { 69 | var thisPackageAssets = thisPackage.assetsByType; 70 | 71 | _.each( thisPackageAssets, function( assets, thisAssetType ) { 72 | if( _.contains( assetTypes, thisAssetType ) ) 73 | memo[ thisAssetType ] = memo[ thisAssetType ].concat( assets ); 74 | } ); 75 | 76 | return memo; 77 | }, memo ); 78 | 79 | this.parcelAssetsByType = _.extend( {}, this.parcelAssetsByType, sortedAssets ); 80 | }; 81 | 82 | Parcel.prototype.attachWatchListeners = function( bundles ) { 83 | var _this = this; 84 | 85 | this.on( 'assetUpdated', function( eventType, asset ) { 86 | if( _.contains( [ 'added', 'deleted' ], eventType ) ) 87 | this.calcParcelAssets( [ asset.type ] ); 88 | 89 | if( bundles[ asset.type ] ) { 90 | _this.writeBundle( asset.type, bundles[ asset.type ], function( err, bundleWasWritten ) { 91 | if( err ) return _this.emit( 'error', err ); 92 | 93 | if( bundleWasWritten ) _this.emit( 'bundleUpdated', bundles[ asset.type ], asset.type ); 94 | // ... done! 95 | } ); 96 | } 97 | } ); 98 | 99 | this.on( 'packageJsonUpdated', function( thePackage ) { 100 | var bundlesToRewrite = _.pick( bundles, _.without( Object.keys( bundles ), 'script' ) ); 101 | this.calcParcelAssets( Object.keys( bundlesToRewrite ) ); 102 | 103 | async.each( Object.keys( bundlesToRewrite ), function( thisAssetType, nextEach ) { 104 | var thisBundlePath = bundlesToRewrite[ thisAssetType ]; 105 | if( ! thisBundlePath ) return nextEach(); 106 | 107 | _this.writeBundle( thisAssetType, thisBundlePath, function( err, bundleWasWritten ) { 108 | // don't stop writing other bundles if there was an error on this one. errors happen 109 | // frequently with transforms.. like invalid scss, etc. don't stop the show, just 110 | // keep going with our other bundles. 111 | 112 | if( err ) _this.emit( 'error', err ); 113 | else if( bundleWasWritten ) _this.emit( 'bundleWritten', thisBundlePath, thisAssetType, true ); 114 | 115 | nextEach(); 116 | } ); 117 | }, function( err ) { 118 | if( err ) _this.emit( 'error', err ); 119 | 120 | // done ); 121 | } ); 122 | } ); 123 | }; 124 | 125 | Parcel.prototype.writeBundle = function( assetType, dstPath, callback ) { 126 | var _this = this; 127 | 128 | var srcAssets = _this.parcelAssetsByType[ assetType ]; 129 | if( ! srcAssets || srcAssets.length === 0 ) return callback( null, false ); // we don't want to create an empty bundle just because we have no source files 130 | 131 | var bundle = through2(); 132 | var tempBundlePath = path.join( path.dirname( dstPath ), '.temp_' + path.basename( dstPath ) ); 133 | 134 | bundle.pipe( fs.createWriteStream( dstPath ) ).on( 'close', function ( err ) { 135 | // execution resumes here after all the individual asset streams 136 | // have been piped to this bundle. we need to pipe the bundle to the writable 137 | // stream first (before individual assets are piped to bundle stream) 138 | // so that if the high water mark is reached on one of the readable streams 139 | // it doesn't pause (with no way to resume). See github issue #15. 140 | 141 | if( err ) return callback( err, false ); 142 | 143 | // fs.rename( tempBundlePath, dstPath, function( err ) { 144 | // if( err ) console.log( 'yoyoyoo', fs.existsSync( tempBundlePath ), dstPath, srcAssets ); 145 | 146 | // if( err ) return callback( err ); 147 | 148 | //_this.bundlePathsByType[ assetType ] = dstPath; // don't do this. isn't really a property of the parcel so much as an input to parcelify 149 | 150 | callback( null, true ); 151 | // } ); 152 | } ); 153 | 154 | // pipe all our individual style streams to the bundle in order to concatenate them 155 | async.eachSeries( srcAssets, function( thisAsset, nextAsset ) { 156 | var thisAssetStream = thisAsset.createReadStream(); 157 | 158 | thisAssetStream.on( 'error', function( err ) { 159 | nextAsset( new Error( 'While reading or transforming "' + thisAsset.srcPath + '":\n' + err.message ) ); 160 | } ); 161 | 162 | thisAssetStream.on( 'end', function( err ) { 163 | nextAsset(); 164 | } ); 165 | 166 | thisAssetStream.pipe( bundle, { end : false } ); 167 | }, function( err ) { 168 | if( err ) return callback( err, false ); 169 | 170 | bundle.end(); 171 | 172 | // execution will resume up above on the 173 | // `close` event handler for our bundle 174 | } ); 175 | }; 176 | 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcelify", 3 | "version": "2.2.1", 4 | "description": "Create css bundles from npm packages using the browserify dependency graph.", 5 | "main": "index.js", 6 | "bin": { 7 | "parcelify": "bin/cmd.js" 8 | }, 9 | "dependencies": { 10 | "async": "~0.2.10", 11 | "glob": "~3.2.9", 12 | "globwatcher": "~1.2.3", 13 | "inherits": "~2.0.1", 14 | "minimist": "0.0.8", 15 | "mkdirp": "~0.3.5", 16 | "npmlog": "0.0.6", 17 | "parcel-map": "^3.0.0", 18 | "resolve": "~0.6.1", 19 | "shasum": "~1.0.1", 20 | "stream-combiner": "^0.2.2", 21 | "through2": "~0.6.3", 22 | "toposort": "~0.2.10", 23 | "underscore": "~1.6.0" 24 | }, 25 | "devDependencies": { 26 | "browserify": "^9.0.8", 27 | "sass-css-stream": "^0.1.4", 28 | "tape": "~2.3.2" 29 | }, 30 | "scripts": { 31 | "test": "tape test/test.js" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/rotundasoftware/parcelify.git" 36 | }, 37 | "homepage": "https://github.com/rotundasoftware/parcelify", 38 | "keywords": [ 39 | "parcel", 40 | "asset", 41 | "css", 42 | "commonjs", 43 | "browserify" 44 | ], 45 | "author": { 46 | "name": "Rotunda Software", 47 | "email": "support@rotundasoftware.com" 48 | }, 49 | "licenses": [ 50 | { 51 | "type": "MIT", 52 | "url": "https://github.com/rotundasoftware/parcelify/blob/master/LICENSE" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rotundasoftware/parcelify/8ba813a6a1e93bddf398c8d5042e9e7583254dbe/test/.DS_Store -------------------------------------------------------------------------------- /test/cli/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |