├── .gitignore ├── CHANGELOG.md ├── gulpfile.coffee ├── package.json ├── gulpsmith.litcoffee ├── spec.coffee └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | gulpsmith.js 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Revision History 2 | 3 | 0.4.2 4 | 5 | - Skip any directories found in the Gulp stream, since Metalsmith can't use 6 | them 7 | 8 | 0.4.1 9 | 10 | - Fix missing .npmignore, doc bugs, and other minor packaging issues. 11 | No code changes. 12 | 13 | 0.4.0 14 | 15 | - Initial release 16 | 17 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp') 2 | mocha = require 'gulp-mocha' 3 | coffee = require 'gulp-coffee' 4 | 5 | require 'coffee-script/register' 6 | 7 | package_name = JSON.parse(require('fs').readFileSync "package.json").name 8 | main = "#{package_name}.litcoffee" 9 | 10 | gulp.task 'build', -> 11 | gulp.src(main) 12 | .pipe coffee() 13 | #.on 'error', ->gutil.log 14 | .pipe gulp.dest('.') 15 | #.pipe filelog() 16 | 17 | gulp.task 'test', ['build'], -> 18 | gulp.src 'spec.*coffee' 19 | .pipe mocha 20 | reporter: "spec" 21 | #bail: yes 22 | .on "error", (err) -> 23 | console.log err.toString() 24 | console.log err.stack if err.stack? 25 | @emit 'end' 26 | 27 | gulp.task 'default', ['test'], -> 28 | gulp.watch [main, 'spec.*coffee'], ['test'] 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulpsmith", 3 | "version": "0.6.0", 4 | "description": "Use gulp plugins in Metalsmith, or Metalsmith plugins in gulp", 5 | "repository": "git@github.com:pjeby/gulpsmith.git", 6 | "homepage": "https://github.com/pjeby/gulpsmith", 7 | "main": "gulpsmith.js", 8 | "files": ["gulpsmith.js"], 9 | "dependencies": { 10 | "clone-stats": "0.0.1", 11 | "highland": "^1.24.1", 12 | "vinyl": "^2.1.0" 13 | }, 14 | "peerDependencies": { 15 | "metalsmith": "^0.10||^0.11||^1.0.0||^2.0.0" 16 | }, 17 | "devDependencies": { 18 | "chai": "^1.9.1", 19 | "gulp-mocha": "^0.4.1", 20 | "coffee-script": "^1.7.1", 21 | "gulp": "^3.8.0", 22 | "mocha": "^1.18.2", 23 | "gulp-coffee": "^1.4.3", 24 | "sinon": "^1.9.1", 25 | "sinon-chai": "^2.5.0" 26 | }, 27 | "scripts": { 28 | "test": "gulp test", 29 | "prepublish": "gulp build" 30 | }, 31 | "keywords": [ 32 | "gulp", 33 | "gulpplugin", 34 | "gulpfriendly", 35 | "metalsmith" 36 | ], 37 | "author": "PJ Eby ", 38 | "license": "ISC" 39 | } 40 | -------------------------------------------------------------------------------- /gulpsmith.litcoffee: -------------------------------------------------------------------------------- 1 | # The ``gulpsmith`` API 2 | 3 | The ``gulpsmith()`` function exported by this module accepts a single, optional 4 | argument: a directory name that defaults to ``process.cwd()``. The return 5 | value is a stream (aka "Gulp plugin") wrapping a ``Metalsmith`` instance. 6 | 7 | module.exports = gulpsmith = (dir = process.cwd()) -> 8 | 9 | stream = gulp_stream(smith = require("metalsmith")(dir)) 10 | 11 | The returned stream gets ``.use()`` and ``.metadata()`` methods that delegate 12 | to the underlying Metalsmith instance, but return the stream instead of the 13 | Metalsmith instance. (Calling ``.metadata()`` with no arguments returns the 14 | metadata, however.) 15 | 16 | stream.use = (plugin) -> 17 | smith.use(plugin) 18 | return this 19 | 20 | stream.metadata = -> 21 | if !arguments.length 22 | return smith.metadata() 23 | smith.metadata(arguments...) 24 | return this 25 | 26 | return stream 27 | 28 | ``gulpsmith.pipe()``, on the other hand, accepts one or more streams to be 29 | wrapped for use as a Metalsmith plugin, and returns a plugin function with a 30 | ``.pipe()`` method for extending the pipeline: 31 | 32 | gulpsmith.pipe = make_pipe = (pipeline...) -> 33 | plugin = metal_plugin(pipeline) 34 | plugin.pipe = (streams...) -> make_pipe(pipeline..., streams...) 35 | return plugin 36 | 37 | 38 | 39 | 40 | 41 | 42 | Both conversion directions use ``highland`` streams, require conversions 43 | to/from ``vinyl`` File objects, and do mode/stats translations (via 44 | ``clone-stats``). 45 | 46 | highland = require 'highland' 47 | File = require 'vinyl' 48 | clone_stats = require 'clone-stats' 49 | {resolve} = require 'path' 50 | 51 | ### Table of Contents 52 | 53 | 54 | 55 | * [Wrapping A Gulp Pipeline as a Metalsmith Plugin](#wrapping-a-gulp-pipeline-as-a-metalsmith-plugin) 56 | * [Wrapping Metalsmith as a Gulp Plugin](#wrapping-metalsmith-as-a-gulp-plugin) 57 | * [File Conversions](#file-conversions) 58 | * [``vinyl`` Files To Metalsmith Files](#vinyl-files-to-metalsmith-files) 59 | * [Metalsmith Files To ``vinyl`` Files](#metalsmith-files-to-vinyl-files) 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | ## Wrapping A Gulp Pipeline as a Metalsmith Plugin 84 | 85 | A ``gulpsmith.pipe()`` plugin is a function that runs Metalsmith's files 86 | through a Gulp pipeline and back into Metalsmith. 87 | 88 | metal_plugin = (streams) -> (files, smith, done) -> 89 | 90 | pipeline = highland.pipeline(streams...) 91 | 92 | To handle errors, we define an error handler that can be invoked at most once. 93 | It works by passing the error on to the next step in the Metalsmith plugin 94 | chain (or the ``run()/build()`` error handler). It saves the error, so that 95 | other parts of the plugin know not to keep processing files afterwards, and not 96 | to call ``done()`` a second time. 97 | 98 | error = null 99 | 100 | pipeline.on "error", error_handler = (e) -> 101 | if !error? 102 | done error = e 103 | return 104 | 105 | Each file received from the Gulp pipeline is converted to a Metalsmith file and 106 | stored in a map for sending back to Metalsmith. 107 | 108 | pipeline.toArray (fileArray) -> 109 | 110 | outfiles = {} 111 | for file in fileArray 112 | try outfiles[file.relative] = gulpsmith.to_metal(file) 113 | catch e then return error_handler(e) 114 | 115 | Assuming no errors occurred, we delete from Metalsmith's files any files 116 | that were dropped (or renamed) in the Gulp pipeline. Then we add any new or 117 | renamed files (and/or overwrite the modified ones), and tell Metalsmith we 118 | finished without errors. 119 | 120 | for own path of files 121 | if not outfiles.hasOwnProperty path 122 | delete files[path] 123 | 124 | for own path, file of outfiles 125 | files[path] = file 126 | 127 | done() unless error? 128 | 129 | Now that the pipeline is ready, we can push our converted versions of all the 130 | Metalsmith files into its head end (stopping if an error happens at any point). 131 | 132 | for own path, file of files 133 | pipeline.write gulpsmith.to_vinyl(path, file, smith) 134 | break if error? 135 | 136 | pipeline.end() 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | ## Wrapping Metalsmith as a Gulp Plugin 166 | 167 | The result of wrapping a Metalsmith instance as a Gulp plugin is a ``highland`` 168 | pipeline with a couple of Metalsmith wrapper methods. 169 | 170 | All the pipeline really does at first is accumulate Gulp file objects and 171 | convert them to Metalsmith file objects. If an error occurs in the conversion, 172 | an error event is emitted at the output end of the pipeline, and the file is 173 | skipped. 174 | 175 | gulp_stream = (smith) -> pipeline = highland.pipeline (stream) -> 176 | 177 | stream = stream.reduce {}, (files, file) -> 178 | unless file.isDirectory() 179 | try 180 | files[file.relative] = gulpsmith.to_metal(file) 181 | catch err 182 | pipeline.emit 'error', err 183 | return files 184 | 185 | Once all the files have arrived, we run them through our Metalsmith's ``run()`` 186 | method, converting any Metalsmith error into an ``error`` event on the 187 | pipeline. If no errors happened, we simply stream out the converted files to 188 | the next step in the overall Gulp pipeline flow. Either way, the pipeline's 189 | output is ended afterwards. 190 | 191 | return stream.flatMap (files) -> highland (push, next) -> 192 | 193 | smith.run files, (err, files) -> 194 | if err 195 | push(err) 196 | next([]) 197 | else 198 | next(gulpsmith.to_vinyl(relative, file) \ 199 | for own relative, file of files) 200 | return 201 | 202 | 203 | 204 | 205 | 206 | ## File Conversions 207 | 208 | Metalsmith and gulp use almost, but not quite, *completely different* 209 | conventions for their file objects. Gulp uses ``vinyl`` instances, which know 210 | their own path information, and Metalsmith uses plain objects with a 211 | ``contents`` buffer, that intentionally do *not* know their own path info. 212 | Also, Metalsmith uses relative paths, while Gulp uses absolute ones. Gulp uses 213 | ``fs.stat`` objects, while Metalsmith uses octal mode strings that refer only 214 | to permissions! 215 | 216 | Basically, the ``contents`` buffer attribute is the *only* thing they have in 217 | common, and even there, Metalsmith uses a plain property that's always a 218 | ``Buffer``, while ``vinyl`` objects use a getter property that wraps a 219 | private``_contents`` attribute that can be a stream or null! 220 | 221 | Both kinds of files can have more-or-less arbitrary metadata attributes, but in 222 | Metalsmith's case these are read from files' YAML "front matter", whereas 223 | gulp's can come from any plugin, and e.g. the ``gulp-front-matter`` plugin adds 224 | front-matter data to a single ``frontMatter`` property by default. 225 | 226 | In short, there is no single, simple, canonical transformation possible *in 227 | either direction*, only some general guidelines and heuristics. 228 | 229 | All in all, the following property names must be considered reserved, and not 230 | available for use as arbitrary data values. They are not copied from metal 231 | to vinyl file objects, or vice versa, except in cases where they are translated 232 | from another reserved property during conversion. (``vinyl`` methods and 233 | getter/setters are also reserved, including ``contents`` and ``relative``.) 234 | 235 | reserved_names = Object.create null, 236 | _contents: value: yes 237 | mode: value: yes 238 | stats: value: yes 239 | _base: value: yes 240 | 241 | do -> (reserved_names[_prop]=true) \ 242 | for _prop in ( 243 | Object.getOwnPropertyNames(File::).concat Object.getOwnPropertyNames(new File()) 244 | ) 245 | 246 | 247 | ### ``vinyl`` Files To Metalsmith Files 248 | 249 | Because ``vinyl`` files can be empty or streamed instead of buffered, 250 | ``gulpsmith.to_metal()`` raises an error if its argument isn't buffered. 251 | 252 | gulpsmith.to_metal = (vinyl_file) -> 253 | 254 | if not vinyl_file.isBuffer() 255 | throw new Error( 256 | "Metalsmith needs buffered files: #{vinyl_file.relative}" 257 | ) 258 | 259 | The ``vinyl`` file's attributes are copied, skipping path information and any 260 | other reserved properties. (The path properties need to be removed because 261 | they can become stale as the file is processed by Metalsmith plugins, and the 262 | contents are transferred separately along with a conversion from vinyl's 263 | ``stat`` to Metalsmith's ``stats`` and ``mode``.) 264 | 265 | metal_file = {} 266 | for own key, val of vinyl_file 267 | unless key of reserved_names 268 | metal_file[key] = val 269 | 270 | metal_file.contents = vinyl_file.contents 271 | 272 | if (stats = vinyl_file.stat)? 273 | metal_file.stats = stats 274 | if stats.mode? 275 | metal_file.mode = ( 276 | '0000'+ (vinyl_file.stat.mode & 4095).toString(8) 277 | ).slice(-4) 278 | 279 | return metal_file 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | ### Metalsmith Files To ``vinyl`` Files 289 | 290 | Since Metalsmith files don't know their own path, ``gulpsmith.to_vinyl()`` 291 | needs a path as well as the file object, and an optional ``Metalsmith`` 292 | instance. 293 | 294 | gulpsmith.to_vinyl = (relative, metal_file, smith) -> 295 | 296 | opts = { contents: metal_file.contents } 297 | 298 | In addition to a path, ``vinyl`` files need a ``cwd``, and ``base`` in order to 299 | function properly. If a ``Metalsmith`` instance is available, we use it to 300 | simulate them. (By assuming that Metalsmith file paths are relative to 301 | Metalsmith's source path.) Otherwise, we pretend both are the process's 302 | current directory. 303 | 304 | if smith? 305 | opts.cwd = if smith.join then smith.join() else smith.path() 306 | opts.base = smith.source() 307 | else 308 | opts.cwd = process.cwd() 309 | opts.base = opts.cwd 310 | 311 | opts.path = resolve opts.base, relative 312 | 313 | The rest is just copying attributes and converting Metalsmith's ``mode`` and 314 | ``stats`` to a ``vinyl`` ``.stat``, if needed. We skip any ``.relative`` 315 | property because it's not writable on ``vinyl`` files, and we skip all other 316 | reserved names to prevent confusion and data corruption. 317 | 318 | opts.stat = null 319 | if metal_file.stats? or metal_file.mode? 320 | opts.stat = clone_stats metal_file.stats ? {} 321 | opts.stat.mode = parseInt(metal_file.mode, 8) if metal_file.mode? 322 | 323 | vinyl_file = new File opts 324 | for own key, val of metal_file 325 | vinyl_file[key] = val unless key of reserved_names 326 | 327 | return vinyl_file 328 | 329 | -------------------------------------------------------------------------------- /spec.coffee: -------------------------------------------------------------------------------- 1 | {expect, should} = chai = require 'chai' 2 | should = should() 3 | chai.use require 'sinon-chai' 4 | 5 | gulpsmith = require './' 6 | {to_metal, to_vinyl} = gulpsmith 7 | fs = require 'fs' 8 | {resolve, sep:pathsep} = require('path') 9 | File = require 'vinyl' 10 | _ = require 'highland' 11 | Metalsmith = require 'metalsmith' 12 | clone_stats = require 'clone-stats' 13 | 14 | expect_fn = (item) -> expect(item).to.exist.and.be.a('function') 15 | {spy} = sinon = require 'sinon' 16 | 17 | spy.named = (name, args...) -> 18 | s = if this is spy then spy(args...) else this 19 | s.displayName = name 20 | return s 21 | 22 | compare_gulp = (infiles, transform, outfiles, done) -> 23 | _(file for own path, file of infiles) 24 | .pipe(transform).toArray (files) -> 25 | transformed = {} 26 | for file in files 27 | transformed[file.relative] = file 28 | transformed.should.eql outfiles 29 | done() 30 | 31 | compare_metal = (infiles, smith, outfiles, done) -> 32 | smith.run infiles, (err, transformed) -> 33 | if err 34 | done(err) 35 | else 36 | transformed.should.eql outfiles 37 | done() 38 | 39 | check_mode = (vinylmode, metalmode) -> 40 | (vinylmode).should.equal parseInt(metalmode, 8) 41 | 42 | describe "Metal -> Vinyl Conversion", -> 43 | 44 | mf = mystat = null 45 | beforeEach -> 46 | mystat = fs.statSync('README.md') 47 | mf = contents: Buffer(''), mode: (mystat.mode).toString(8) 48 | 49 | it "assigns a correct relative path", -> 50 | to_vinyl("path1", mf).relative.should.equal "path1" 51 | to_vinyl("README.src", mf).relative.should.equal "README.src" 52 | 53 | it "converts Metalsmith .mode to Gulp .stat", -> 54 | check_mode to_vinyl("README.md", mf).stat.mode, mf.mode 55 | mf.mode = (~parseInt(mf.mode, 8)).toString(8) 56 | check_mode to_vinyl("README.md", mf).stat.mode, mf.mode 57 | 58 | it "always ignores Metalsmith .stat", -> 59 | delete mf.mode 60 | mf.stat = mystat 61 | expect(to_vinyl("README.md", mf).stat).to.not.exist 62 | 63 | it "converts Metalsmith .stats to Gulp .stat", -> 64 | delete mf.mode 65 | mf.stats = mystat 66 | to_vinyl("README.md", mf).stat.should.eql mystat 67 | 68 | it "overrides the Gulp .stat.mode with the Metalsmith .mode", -> 69 | cstat = clone_stats(mystat) 70 | mf.stats = mystat 71 | mf.mode = (cstat.mode = ~parseInt(mf.mode, 8)).toString(8) 72 | to_vinyl("README.md", mf).stat.should.eql cstat 73 | 74 | it "removes the Metalsmith.mode", -> 75 | expect(to_vinyl("README.md", mf).mode).not.to.exist 76 | 77 | it "assigns Metalsmith .contents to Gulp .contents", -> 78 | to_vinyl("xyz", mf).contents.should.equal mf.contents 79 | mf.contents = Buffer("blah blah blah") 80 | to_vinyl("abc", mf).contents.should.eql Buffer("blah blah blah") 81 | 82 | 83 | it "adds .cwd and .base to files (w/Metalsmith instance given)", -> 84 | verify = (smith) -> 85 | vf = to_vinyl("mnop", mf, smith) 86 | vf.base.should.equal smith.source() 87 | vf.cwd.should.equal (if smith.join then smith.join() else smith.path()) 88 | verify smith = Metalsmith "/foo/bar" 89 | verify smith.source "spoon" 90 | verify Metalsmith __dirname 91 | 92 | it "copies arbitrary attributes (exactly)", -> 93 | verify = new File( 94 | base: __dirname, cwd: __dirname, stat: mystat, path:resolve "README.md" 95 | ) 96 | mf.x = verify.x = 1 97 | mf.y = verify.y = z: 2 98 | mf.stats = mystat 99 | res = to_vinyl("README.md", mf) 100 | delete mf.contents 101 | delete res._contents 102 | delete verify._contents 103 | res.should.eql verify 104 | 105 | it "doesn't overwrite the .relative property on Vinyl files", -> 106 | mf.relative = "ping!" 107 | to_vinyl("pong/whiz", mf, Metalsmith __dirname) 108 | .relative.should.equal "pong#{pathsep}whiz" 109 | 110 | it "doesn't overwrite any ``vinyl`` methods or properties", -> 111 | for own name of (File::) 112 | mf[name] = "bad data for .#{name}" 113 | for own name of new File() 114 | mf[name] = "bad data for .#{name}" 115 | vf = to_vinyl("what/ever", mf, Metalsmith __dirname) 116 | for own name, prop of (File::) 117 | expect(vf[name]).to.equal prop 118 | for own name of vf 119 | expect(vf[name]).to.not.equal "bad data for .#{name}" 120 | 121 | 122 | 123 | 124 | describe "Vinyl -> Metal Conversion", -> 125 | gf = null 126 | beforeEach -> gf = new File( 127 | path: "README.md", contents: Buffer(''), stat: fs.statSync('README.md') 128 | ) 129 | 130 | it "throws an error for non-buffered (empty or stream) files", -> 131 | expect( 132 | -> to_metal new File(path:"foo.bar") 133 | ).to.throw /foo\.bar/ 134 | expect( 135 | -> to_metal new File(path:"spam.baz", 136 | contents: fs.createReadStream('README.md') 137 | )).to.throw /spam\.baz/ 138 | 139 | it "converts Gulp .stat to Metalsmith .mode", -> 140 | (parseInt(to_metal(gf).mode, 8)).should.equal gf.stat.mode & 4095 141 | gf.stat.mode = ~gf.stat.mode 142 | (parseInt(to_metal(gf).mode, 8)).should.equal gf.stat.mode & 4095 143 | 144 | it "assigns Gulp .contents to Metalsmith .contents", -> 145 | to_metal(gf).contents.should.equal gf.contents 146 | gf.contents = Buffer("blah blah blah") 147 | to_metal(gf).contents.should.eql Buffer("blah blah blah") 148 | 149 | it "copies arbitrary attributes (exactly)", -> 150 | verify = 151 | x: 1, y: {z:2}, stats: gf.stat 152 | gf.x = 1 153 | gf.y = z: 2 154 | res = to_metal(gf) 155 | delete gf.contents 156 | delete res.contents 157 | delete res.mode 158 | res.should.eql verify 159 | 160 | it "doesn't keep any ``vinyl`` methods or properties", -> 161 | to_metal(gf).should.not.have.property name for own name of (File::) 162 | to_metal(gf).should.not.have.property name for own name of gf 163 | to_metal(gf).should.not.have.property "relative" 164 | 165 | describe "gulpsmith() streams", -> 166 | 167 | s = testfiles = null 168 | beforeEach -> s = gulpsmith() 169 | 170 | null_plugin = (files, smith, done) -> done() 171 | 172 | describe ".use() method", -> 173 | plugin1 = spy.named "plugin1", null_plugin 174 | plugin2 = spy.named "plugin2", null_plugin 175 | it "returns self", -> expect(s.use(plugin1)).to.equal s 176 | it "invokes passed plugins during build", (done) -> 177 | s.use(plugin1).use(plugin2) 178 | _([]).pipe(s).toArray -> 179 | plugin1.should.be.calledOnce.and.calledBefore plugin2 180 | plugin2.should.be.calledOnce.and.calledAfter plugin1 181 | done() 182 | 183 | describe ".metadata() method", -> 184 | data = {a: 1, b:2} 185 | it "returns self when setting", -> 186 | expect(s.metadata(data)).to.equal s 187 | 188 | it "returns matching metadata when getting", -> 189 | s.metadata(data) 190 | expect(s.metadata()).to.eql data 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | describe "streaming", -> 207 | 208 | beforeEach -> 209 | testfiles = 210 | f1: new File(path:resolve("f1"), contents:Buffer('f1')) 211 | f2: new File(path:resolve("f2"), contents:Buffer('f2')) 212 | testfiles.f1.a = "b" 213 | testfiles.f2.c = 3 214 | 215 | it "should yield the same files (if no plugins)", (done) -> 216 | compare_gulp testfiles, s, testfiles, done 217 | 218 | it "should delete files deleted by a Metalsmith plugin", (done) -> 219 | s.use (f,s,d) -> delete f.f1; d() 220 | compare_gulp testfiles, s, {f2:testfiles.f2}, done 221 | 222 | it "should add files added by a Metalsmith plugin", (done) -> 223 | s.use (files, smith, done) -> 224 | files.f3 = contents:Buffer "f3" 225 | done() 226 | compare_gulp( 227 | {}, s, f3: new File( 228 | path:resolve("f3"), base:__dirname, contents:Buffer "f3" 229 | ), done 230 | ) 231 | 232 | it "yields errors for non-buffered files (and continues)", (done) -> 233 | testfiles.f1.contents = null 234 | done = should_error done, /buffered.*f1/ 235 | compare_gulp testfiles, s, {f2:testfiles.f2}, 236 | -> done new Error "No error caught" 237 | 238 | it "yields errors for errors produced by Metalsmith plugins", (done) -> 239 | error_message = "demo error!" 240 | s.use (files, smith, d) -> d new Error(error_message) 241 | done = should_error done, error_message 242 | _([]).pipe(s).toArray -> done Error "Error wasn't caught" 243 | 244 | 245 | 246 | 247 | it "excludes Gulp directories", (done) -> 248 | testfiles.f2.isDirectory = -> true 249 | compare_gulp testfiles, s, {f1:testfiles.f1}, done 250 | 251 | it "converts Gulp files to Metalsmith and back", (done) -> 252 | 253 | vinyl_spy = spy.named 'vinyl_spy', gulpsmith, 'to_vinyl' 254 | metal_spy = spy.named 'metal_spy', gulpsmith, 'to_metal' 255 | err = metal_files = null 256 | 257 | # Capture files coming into Metalsmith 258 | catch_metal = (files, smith, done) -> 259 | metal_files = files 260 | done() 261 | 262 | _(file for own path, file of testfiles) 263 | .pipe(gulpsmith().use(catch_metal)).toArray (files) -> 264 | try 265 | for file in files 266 | vinyl_spy.should.have.returned file 267 | metal_spy.should.have.been.calledWithExactly file 268 | 269 | for own relative, file of metal_files 270 | vinyl_spy.should.have.been 271 | .calledWithExactly relative, file 272 | 273 | metal_spy.should.have.returned file 274 | catch err 275 | return done(err) 276 | finally 277 | vinyl_spy.restore() 278 | metal_spy.restore() 279 | done() 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | should_error = (done, ematch, etype=Error) -> 289 | 290 | s.on "error", (e) -> 291 | try 292 | e.should.be.instanceOf Error 293 | if ematch? 294 | if ematch instanceof RegExp 295 | e.message.should.match ematch 296 | else 297 | e.message.should.equal ematch 298 | cb() 299 | catch err 300 | cb(err) 301 | 302 | return cb = -> 303 | done arguments... 304 | done = -> 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | describe "gulpsmith.pipe() plugins", -> 330 | 331 | smith = testfiles = null 332 | 333 | it "are functions", -> 334 | expect(gulpsmith.pipe()).to.be.a('function') 335 | 336 | describe ".pipe() method", -> 337 | it "is a function", -> expect(gulpsmith.pipe().pipe).to.exist.and.be.a('function') 338 | it "returns a function with another pipe() method", -> 339 | expect(gulpsmith.pipe().pipe().pipe).to.exist.and.be.a('function') 340 | 341 | describe "streaming", -> 342 | 343 | beforeEach -> 344 | smith = Metalsmith(process.cwd()) 345 | testfiles = 346 | f1: contents:Buffer('f1') 347 | f2: contents:Buffer('f2') 348 | testfiles.f1.a = "b" 349 | testfiles.f2.c = 3 350 | 351 | it "should yield the same files (if no plugins)", (done) -> 352 | compare_metal testfiles, smith.use(gulpsmith.pipe()), testfiles, done 353 | 354 | it "should delete files deleted by a Gulp plugin", (done) -> 355 | s = smith.use gulpsmith.pipe _.where relative: 'f2' 356 | compare_metal testfiles, s, {f2:testfiles.f2}, done 357 | 358 | it "should add files added by a Gulp plugin", (done) -> 359 | f3 = new File path: "f3", contents:Buffer "f3" 360 | f3.x = "y"; f3.z = 42 361 | s = smith.use gulpsmith.pipe(_.append f3) 362 | compare_metal {}, s, {f3: { 363 | x: "y", z:42, contents:Buffer "f3" 364 | }}, done 365 | 366 | 367 | 368 | 369 | 370 | it "converts Metalsmith files to Gulp and back", (done) -> 371 | 372 | vinyl_spy = spy.named 'vinyl_spy', gulpsmith, 'to_vinyl' 373 | metal_spy = spy.named 'metal_spy', gulpsmith, 'to_metal' 374 | vinyl_files = [] 375 | 376 | # Capture files coming into Gulp 377 | catch_vinyl = (file) -> 378 | vinyl_files.push file 379 | return file 380 | 381 | smith = Metalsmith(__dirname) 382 | smith.use(gulpsmith.pipe((_.map catch_vinyl))) 383 | 384 | smith.run testfiles, (err, files) -> 385 | return done(err) if err? 386 | try 387 | for file in vinyl_files 388 | vinyl_spy.should.have.returned file 389 | metal_spy.should.have.been.calledWithExactly file 390 | 391 | for own relative, file of files 392 | vinyl_spy.should.have.been 393 | .calledWithExactly relative, file, smith 394 | metal_spy.should.have.returned file 395 | catch err 396 | return done(err) 397 | finally 398 | vinyl_spy.restore() 399 | metal_spy.restore() 400 | done() 401 | 402 | it "exits with any error yielded by a Gulp plugin", (done) -> 403 | message = "dummy error!" 404 | smith.use gulpsmith.pipe _ (push) -> 405 | push new Error message 406 | push null, _.nil 407 | 408 | done = should_error done, {}, message 409 | 410 | 411 | it "exits with an error if a Gulp plugin yields an unbuffered file", 412 | (done) -> 413 | smith.use gulpsmith.pipe _.append new File(path: "README.md") 414 | done = should_error done, {}, /buffered.*README.md/ 415 | 416 | should_error = (done, files, ematch, etype=Error) -> 417 | 418 | smith.run files, (e, files) -> 419 | try 420 | e.should.be.instanceOf Error 421 | if ematch? 422 | if ematch instanceof RegExp 423 | e.message.should.match ematch 424 | else 425 | e.message.should.equal ematch 426 | cb() 427 | catch err 428 | cb(err) 429 | 430 | return cb = -> 431 | done arguments... 432 | done = -> 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gulp + Metalsmith = ``gulpsmith`` 2 | 3 | ``gulpsmith`` lets you use [Gulp](http://gulpjs.com/) plugins (or ``vinyl`` pipelines) with Metalsmith, and use [Metalsmith](http://www.metalsmith.io/) plugins as part of a Gulp or ``vinyl`` pipeline. This can be helpful if you: 4 | 5 | * Don't want Metalsmith to slurp up an entire directory tree of files, 6 | * Want to upload your Metalsmith build to Amazon S3 or send it someplace via SFTP without first generating files locally and then running a separate uploading process, 7 | * Want to pre- or post-process your Metalsmith build with Gulp plugins, or 8 | * Already run your build process with one tool or the other and don't want to switch, but need both kinds of plugins. 9 | 10 | ``gulpsmith().use(metal_plugin1).use(metal_plugin2)``... wraps one or more Metalsmith plugins for use in Gulp, whereas ``gulpsmith.pipe(stream1).pipe(stream2)``... turns a series of Gulp plugins (or ``vinyl`` streaming operations) into a plugin that can be passed to Metalsmith's ``.use()`` method. 11 | 12 | (In addition,``gulpsmith.pipe()`` is [Highland](http://highlandjs.org/)-friendly and lets you pass in Highland stream transforms and functions as well as Gulp plugins.) 13 | 14 | While a *perfect* translation between Gulp and Metalsmith is impossible, ``gulpsmith`` does its best to be lossless and corruption-free in both directions. When "corruption-free" and "lossless" are in conflict, however, ``gulpsmith`` prioritizes being "corruption-free". That is, it chooses to drop conflicting properties during translation, rather than create problems downstream. (See [File Conversions and Compatibility](#file-conversions-and-compatibility), below, for more details.) 15 | 16 | **Important**: starting with gulpsmith 0.6.0, gulp files are created using vinyl 2.1, which means that the list of dropped properties has changed, and the list in this document may not be accurate for vinyl versions > 2.1. You may need to stay on gulpsmith 0.5.5 if your project uses any of the new reserved property names (like `history`) as metalsmith properties. But if your project uses gulp 4, upgrading to gulpsmith 0.6.0 is required. 17 | 18 | ### Table of Contents 19 | 20 | 21 | 22 | * [Using Metalsmith in a Gulp Pipeline](#using-metalsmith-in-a-gulp-pipeline) 23 | * [Front Matter and File Properties](#front-matter-and-file-properties) 24 | * [Using a Gulp Pipeline as a Metalsmith Plugin](#using-a-gulp-pipeline-as-a-metalsmith-plugin) 25 | * [Enhanced Features of ``gulpsmith.pipe()``](#enhanced-features-of-gulpsmithpipe) 26 | * [Advanced Pipelines and Error Handling](#advanced-pipelines-and-error-handling) 27 | * [Reusing Pipelines](#reusing-pipelines) 28 | * [Using Pre-assembled Pipelines](#using-pre-assembled-pipelines) 29 | * [Stream Operations Other Than ``.pipe()``](#stream-operations-other-than-pipe) 30 | * [File Conversions and Compatibilty](#file-conversions-and-compatibility) 31 | * [Reserved Properties](#reserved-properties) 32 | * [Gulp Reserved Property Names](#gulp-reserved-property-names) 33 | * [Metalsmith Reserved Property Names](#metalsmith-reserved-property-names) 34 | * [Gulp Directories and Metalsmith](#gulp-directories-and-metalsmith) 35 | 36 | 37 | 38 | 39 | 40 | ## Using Metalsmith in a Gulp Pipeline 41 | 42 | To use Metalsmith in a Gulp pipeline, call ``gulpsmith()`` with an optional directory name (default is ``process.cwd()``) which will be used to create a Metalsmith instance. The return value is a stream that can be used in a Gulp ``.pipe()`` chain, but which also has ``.use()`` and ``.metadata()`` methods that can be used to configure the Metalsmith instance. 43 | 44 | Instead of reading from a source directory and writing to a destination directory, the wrapped Metalsmith instance obtains all its files in-memory from the Gulp pipeline, and will send all its files in-memory to the next stage of the pipeline. 45 | 46 | (Because Metalsmith processes files in a group, note that your overall Gulp pipeline's output will pause until all the files from previous stages have been processed by Metalsmith. All of Metalsmith's output files will then be streamed to the next stage of the pipeline, all at once.) 47 | 48 | Example: 49 | 50 | ```javascript 51 | gulpsmith = require('gulpsmith'); 52 | 53 | gulp.src("./src/**/*") 54 | .pipe(some_gulp_plugin(some_options)) 55 | .pipe( 56 | gulpsmith() // defaults to process.cwd() if no dir supplied 57 | 58 | // You can initialize the metalsmith instance with metadata 59 | .metadata({site_name: "My Site"}) 60 | 61 | // and .use() as many Metalsmith plugins as you like 62 | .use(markdown()) 63 | .use(permalinks('posts/:title')) 64 | ) 65 | .pipe(another_gulp_plugin(more_options)) 66 | .pipe(gulp.dest("./build") 67 | ``` 68 | 69 | ### Front Matter and File Properties 70 | 71 | Unlike Metalsmith, Gulp doesn't read YAML front matter by default. So if you want the front matter to be available in Metalsmith, you will need to use the ``gulp-front-matter`` plugin, and insert something like this to promote the ``.frontMatter`` properties before piping to ``gulpsmith()``: 72 | 73 | ```javascript 74 | gulp_front_matter = require('gulp-front-matter'); 75 | assign = require('lodash.assign'); 76 | 77 | gulp.src("./src/**/*") 78 | 79 | .pipe(gulp_front_matter()).on("data", function(file) { 80 | assign(file, file.frontMatter); 81 | delete file.frontMatter; 82 | }) 83 | 84 | .pipe(gulpsmith() 85 | .use(...) 86 | .use(...) 87 | ) 88 | ``` 89 | 90 | This will extract the front matter and promote it to properties on the file, where Metalsmith expects to find it. (Alternately, you could use ``gulp-append-data`` and the ``data`` property instead, to load data from adjacent ``.json`` files in place of YAML front matter!) 91 | 92 | Of course, there are other Gulp plugins that add useful properties to files, and those properties will of course be available to your Metalsmith plugins as well. 93 | 94 | (For example, if you pass some files through the ``gulp-jshint`` plugin before they go to Metalsmith, the Metalsmith plugins will see a ``jshint`` property on the files, with sub-properties for ``success``, ``errorCount``, etc. If you use ``gulp-sourcemaps``, your files will have a ``sourceMap`` property, and so on.) 95 | 96 | 97 | ## Using a Gulp Pipeline as a Metalsmith Plugin 98 | 99 | To use Gulp plugins or other streams as a Metalsmith plugin, simply begin the pipeline with ``gulpsmith.pipe()``: 100 | 101 | ```javascript 102 | gulpsmith = require('gulpsmith') 103 | 104 | Metalsmith(__dirname) 105 | .use(drafts()) 106 | .use(markdown()) 107 | .use(gulpsmith 108 | .pipe(some_gulp_plugin(some_options)) 109 | .pipe(another_gulp_plugin(more_options)) 110 | .pipe(as_many_as(you_like)) 111 | ) 112 | .use(more_metalsmith_plugins()) 113 | .build() 114 | ``` 115 | 116 | From the point of view of the Gulp plugins, the file objects will have a ``cwd`` property equal to the Metalsmith base directory, and a ``base`` property equal to the Metalsmith source directory. They will have a dummy ``stat`` property containing only the Metalsmith file's ``mode``, along with any other data properties that were attached to the file by Metalsmith or its plugins (e.g. from the files' YAML front matter). 117 | 118 | In this usage pattern, there is no ``gulp.src()`` or ``gulp.dest()``, because Metalsmith handles the reading and writing of files. If the Gulp pipeline drops or renames any of the input files, they will be dropped or renamed in the Metalsmith pipeline as well. 119 | 120 | (If you want to, though, you *can* include a ``gulp.dest()``, or any other Gulp output plugin in your pipeline. Just make sure that you also do something to drop the written files from the resulting stream (e.g. using ``gulp-filter``), unless you want Metalsmith to *also* output the files itself. Doing both can be useful if you use a Gulp plugin to upload files, but you also want Metalsmith to output a local copy.) 121 | 122 | ### Enhanced Features of ``gulpsmith.pipe()`` 123 | 124 | Under the hood, ``gulpsmith.pipe()`` is a thin wrapper around Highland's ``_.pipeline()`` function. This means that: 125 | 126 | * You can pass multiple plugins in (e.g. ``gulpsmith.pipe(plugin1, plugin2,...)`` 127 | * You can pass in Highland transforms as plugins (e.g. using ``gulpsmith.pipe(_.where({published:true}))`` to pass through only posts with a true ``.published`` property.) 128 | * You can pass in functions that accept a Highland stream and return a modified version of it, e.g.: 129 | 130 | ```javascript 131 | gulpsmith.pipe( 132 | function(stream) { 133 | return stream.map(something).filter(otherthing); 134 | } 135 | ) 136 | ``` 137 | 138 | In addition, Highland's error forwarding makes sure that errors in anything passed to ``gulpsmith.pipe()`` are passed on to Metalsmith. (More on this in the next section.) 139 | 140 | 141 | ### Advanced Pipelines and Error Handling 142 | 143 | If the pipeline you're using in Metalsmith is built strictly via a series of of ``gulpsmith.pipe().pipe()...`` calls, and you don't save a Metalsmith instance to repeatedly call ``.build()`` or ``.run()`` on, you probably don't need to read the rest of this section. 144 | 145 | If you need to do something more complex, however, you need to be aware of three things: 146 | 147 | 1. Unlike most Metalsmith plugins, Gulp plugins/pipelines are *stateful* and **cannot** be used for more than one build run. 148 | 149 | 2. If you pass a *precomposed* pipeline of plugins to ``gulpsmith.pipe()``, it may not report errors properly, thereby hanging or crashing your build if an error occurs. 150 | 151 | 3. Unlike the normal stream ``.pipe()`` method, ``gulpsmith.pipe()`` *does not return the piped-to stream*: it returns a Metalsmith plugin that just happens to also have a ``.pipe()`` method for further chaining! 152 | 153 | The following three sub-sections will tell you what you need to know to apply or work around these issues. 154 | 155 | #### Reusing Pipelines 156 | 157 | If you want to reuse the same Metalsmith instance over and over with the same Gulp pipeline embedded as a plugin, you must recreate the pipeline *on each run*. (Sorry, that's just how Node streams work!) 158 | 159 | It's easy to do that though, if you need to. Just write a short in-line plugin that re-creates the pipeline each time, like this: 160 | 161 | ```javascript 162 | Metalsmith(__dirname) 163 | .use(drafts()) 164 | .use(markdown()) 165 | .use(function() { // inline Metalsmith plugin... 166 | return gulpsmith 167 | .pipe(some_gulp_plugin(some_options)) 168 | .pipe(another_gulp_plugin(more_options)) 169 | .pipe(as_many_as(you_like)) 170 | .apply(this, arguments) // that calls the gulpsmith-created plugin 171 | }) 172 | .use(more_metalsmith_plugins()) 173 | ``` 174 | 175 | Make sure, however, that *all* of the Gulp plugins are *created* within the function passed to ``.use()``, or your pipeline may mysteriously drop files on the second and subsequent ``.run()`` or ``.build()``. 176 | 177 | #### Using Pre-assembled Pipelines 178 | 179 | By default, the standard ``.pipe()`` method of Node stream objects does not chain errors forward to the destination stream. This means that if you build a pipeline with the normal ``.pipe()`` method of Gulp plugins, you're going to run into problems if one of your source streams emits errors. 180 | 181 | Specifically, your build process can hang, because as far as Gulp or Metalsmith are concerned, the build process is still running! (If you've ever had a Gulp build mysteriously hang on you, you now know the likely reason why.) 182 | 183 | Fortunately for you, if you use ``gulpsmith.pipe()`` to build up your pipeline, it will automatically add an error handler to each stream, so that no matter where in the pipeline an error occurs, Metalsmith will be notified, and the build will end with an error instead of hanging indefinitely or crashing the process. 184 | 185 | However, if for some reason you *must* pass a pre-assembled pipeline into ``gulpsmith.pipe()``, you should probably add error handlers to any part of the pipeline that can generate errors. These handlers should forward the error to the last stream in the pipeline, so that it can be forwarded to Metalsmith by ``gulpsmith.pipe()``. 186 | 187 | (Alternately, you could use Highland's ``_.pipeline()`` to construct your Gulp pipelines -- which can be a good idea, even when you're not using them in Metalsmith!) 188 | 189 | 190 | #### Stream Operations Other Than ``.pipe()`` 191 | 192 | Because ``gulpsmith.pipe()`` returns a Metalsmith plugin rather than a stream, you cannot perform stream operations (like adding event handlers) on the *result* of the call. Instead, you must perform those operations on the *argument* of the call. 193 | 194 | For example, instead of doing this: 195 | 196 | ```javascript 197 | Metalsmith(__dirname) 198 | .use(gulpsmith 199 | .pipe(some_gulp_plugin(some_options)) 200 | .on("data", function(file){...}) // WRONG: this is not a stream! 201 | .pipe(another_gulp_plugin(more_options)) 202 | ) 203 | ``` 204 | 205 | You would need to do this instead: 206 | 207 | ```javascript 208 | Metalsmith(__dirname) 209 | .use(gulpsmith 210 | .pipe( 211 | some_gulp_plugin(some_options) 212 | .on("data", function(file){...}) // RIGHT 213 | ) 214 | .pipe(another_gulp_plugin(more_options)) 215 | ) 216 | ``` 217 | 218 | In other words, you will need to perform any stream-specific operations directly on the component streams, rather than relying on the output of ``.pipe()`` to return the stream you passed in. 219 | 220 | (The same principle applies if you're saving a stream in a variable to use later: save the value being passed *in* to ``.pipe()``, instead of saving the *result* of calling ``.pipe()``.) 221 | 222 | 223 | ## File Conversions and Compatibility 224 | 225 | Regardless of whether you are using Gulp plugins in Metalsmith or vice versa, ``gulpsmith()`` must convert the file objects involved *twice*: once in each direction at either end of the plugin list. For basic usage, you will probably not notice anything unusual, since Gulp plugins rarely do anything with file properties other than the path and contents, and Metalsmith plugins don't usually expect to do anything with ``vinyl`` file properties. 226 | 227 | In particular, if you only use Gulp to pre- and post-process files for Metalsmith (whether it's by using Gulp plugins in Metalsmith or vice-versa), you will probably not encounter any problems with the conversions. It's only if you use Gulp plugins in the *middle* of your Metalsmith plugin list that you may run into issues with reserved properties. 228 | 229 | ### Reserved Properties 230 | 231 | Both Gulp and Metalsmith have certain file properties that have special meaning. When translating between systems, ``gulpsmith`` *always* either **deletes** or **overwrites** them, so that they have correct values for the system where they have special meaning, and *do not exist* in the system where they don't. 232 | 233 | If you put data in *any* of the property names below from a system other than the one that reserves it, you will **lose** that data when the file is converted for use by the other system. 234 | 235 | That's because, when translating between systems, ``gulpsmith`` first *deletes* **all** of these properties, then adds back the ones that are needed for the target system, based on reserved information from the source system. 236 | 237 | So, if you have a Metalsmith file with a ``.base`` or ``.path`` (for example), those properties will **not** be used to create the Gulp ``.base`` or ``.path``. They will simply be deleted, and replaced with suitable values calculated from Metalsmith's internal path information. 238 | 239 | This approach avoids collision with any properties in your Metalsmith project that just *happened* to be named ``.base``, ``.path``, ``.relative``, etc., that could mess up Gulp plugins expecting these values to have their reserved meanings. (Similarly, when converting from Gulp to Metalsmith, these path properties will again be deleted, so that they don't confuse any Metalsmith plugins that are expecting, say, ``.path`` to be a URL path.) 240 | 241 | In short, ``gulpsmith`` prefers to possibly lose data (but do so every single time), rather than to pass through properties that might later corrupt a build when you begin using those properties for something else. 242 | 243 | (After all, if the properties are *always* removed, there is no way for you to have a build that *seems* to work most of the time, until suddenly it doesn't any more!) 244 | 245 | 246 | #### Gulp Reserved Property Names 247 | 248 | **Bold** names are new in gulpsmith 0.6.0, due to use of vinyl 2.x for compatibility with Gulp 4. 249 | 250 | |Property |Contents | 251 | |---------------|--------------------------------------------------------| 252 | | ``.base`` |the directory from which the relative path is calculated| 253 | | **``.basename``** |the filename without its path | 254 | | ``.cwd`` |the original working directory when the file was created| 255 | | **``.dirname``** |the directory portion of ``path`` | 256 | | **``.extname``** |the file extension | 257 | | **``.history``** |history of ``.path`` values | 258 | | ``.path`` |the file's *absolute* filesystem path | 259 | | ``.relative`` |the file's ``.path``, *relative* to its ``.base`` | 260 | | ``.stat`` |the file's filesystem stat | 261 | | **``.stem``** |the file's ``.basename``, minus its extension | 262 | | **``.symlink``** |the file's symlink target, if applicable | 263 | | ``._contents``|private property for the file's contents | 264 | | ``.isBuffer`` |method of ``vinyl`` file objects| 265 | | **``.isCustomProp``** |method of ``vinyl`` file objects| 266 | | ``.isNull`` |method of ``vinyl`` file objects| 267 | | ``.isStream`` |method of ``vinyl`` file objects| 268 | | **``.isSymbolic``** |method of ``vinyl`` file objects| 269 | | **``.isVinyl``** |method of ``vinyl`` file objects| 270 | | ``.isDirectory`` |method of ``vinyl`` file objects| 271 | | ``.inspect`` |method of ``vinyl`` file objects| 272 | | ``.clone`` |method of ``vinyl`` file objects| 273 | 274 | 275 | #### Metalsmith Reserved Property Names 276 | 277 | |Property |Contents | 278 | |----------|----------------------------------------------------| 279 | |``.mode`` | An octal string specifying the file permissions | 280 | |``.stats``| the file's filesystem stat (NEW in Metalsmith 0.10)| 281 | 282 | 283 | ### Gulp Directories and Metalsmith 284 | 285 | Gulp is usually described as operating on streams of file objects. But those file objects can also be *directories*: i.e., file objects whose ``.isDirectory()`` method returns true. If you use a sufficiently broad wildcard in ``gulp.src()``, (e.g. ``**/*``) it will scoop up directories, as well as the files alongside them. 286 | 287 | Normally, you wouldn't notice this is happening, because most Gulp plugins (including ``gulp.dest()``!) basically ignore the directories and just process the files. 288 | 289 | Metalsmith also operates only on files, so Gulpsmith filters the directories out before they can reach Metalsmith, and it *does not restore them afterward*. 290 | 291 | If you happen to need a rare Gulp plugin that *does* do something with directories, you should probably put it before Gulpsmith in your pipeline, or use ``gulp-filter`` and its ``.restore`` option to sift the directories out ahead of Gulpsmith and put them back in afterwards. 292 | 293 | --------------------------------------------------------------------------------