Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

gulp-metalsmith

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gulp-metalsmith - npm Package Compare versions

Comparing version 0.3.0 to 1.0.0

test/fixtures/contact.html

131

lib/index.js

@@ -11,43 +11,25 @@ var path = require('path');

var PLUGIN_NAME = 'gulp-metalsmith';
var TYPE_VINYL = 'vinyl';
var TYPE_JSON = 'json';
var plugin = _.partial(pluginFn, TYPE_VINYL);
plugin.json = _.partial(pluginFn, TYPE_JSON);
plugin.PLUGIN_NAME = PLUGIN_NAME;
module.exports = plugin;
function pluginFn(type, opts) {
function plugin(opts) {
var s = through.obj(transform, flush);
var add = type === TYPE_JSON ? addJsonDefinedFiles : addVinylFile;
var files = {};
opts = _.isObject(opts) ? opts : {};
opts.root = opts.root || process.cwd();
opts.use = (_.isFunction(opts.use) ? [opts.use] : opts.use) || [];
opts.frontmatter = opts.frontmatter !== false;
opts.json = opts.json === true ? '**/*.json' : opts.json;
var root = opts.root || process.cwd();
var m = metalsmith(root);
var ignored = prepareIgnored();
var m = metalsmith(opts.root);
var ignored = matchGlobs(opts.ignore, opts.root);
var jsonDefinitions = matchGlobs(opts.json, opts.root);
prepareMiddleware().forEach(m.use);
m.metadata(opts.metadata || {});
opts.use.forEach(m.use);
return s;
function prepareMiddleware() {
var use = opts.use || [];
return _.isFunction(use) ? [use] : use;
}
function prepareIgnored() {
if (!_.isString(opts.ignore) && !_.isArray(opts.ignore)) {
return [];
}
return globby.sync(opts.ignore, {cwd: root}).map(function (match) {
return path.join(root, match);
});
}
function transform(file, enc, cb) {

@@ -57,7 +39,4 @@ if (file.isStream()) {

}
if (file.isBuffer() && ignored.indexOf(file.path) < 0) {
var key = file.path.replace(file.base, '');
var contents = file.contents;
add(key, contents);
else if (file.isBuffer() && !isIgnored(file)) {
processFile(file);
}

@@ -68,2 +47,25 @@

function processFile(file) {
var key = file.path.replace(file.base, '');
var contents = file.contents;
if (isJsonDefinition(file) && utf8(contents)) {
_.extend(files, createFilesFromJsonDefinition(contents.toString()));
}
else if (opts.frontmatter && utf8(contents)) {
files[key] = parseFrontmatter(contents.toString());
}
else {
files[key] = {contents: contents};
}
}
function isIgnored(file) {
return ignored.indexOf(file.path) > -1;
}
function isJsonDefinition(file) {
return jsonDefinitions.indexOf(file.path) > -1;
}
function flush(cb) {

@@ -87,36 +89,9 @@ m.run(files, function (err, transformed) {

function addVinylFile(key, contents) {
if (opts.frontmatter && utf8(contents)) {
var parsed = fm(contents.toString());
var bodyBuf = new Buffer(parsed.body);
files[key] = _.extend({contents: bodyBuf}, parsed.attributes);
} else {
files[key] = {contents: contents};
}
}
function addJsonDefinedFiles(key, contents) {
if (!utf8(contents)) {
files[key] = {contents: contents};
return;
}
var parsed = {};
function createFilesFromJsonDefinition(contents) {
try {
parsed = JSON.parse(contents.toString());
return createFilesExtension(parseJsonDefinition(contents));
} catch (err) {
emitError(err);
return {};
}
if (!_.isPlainObject(parsed)) {
emitError('JSON file should contain a single root object.');
}
_.forOwn(parsed, function (value, key) {
if (_.isObject(value)) {
value.contents = new Buffer(value.contents || '');
files[key] = value;
}
});
}

@@ -130,1 +105,35 @@

}
function matchGlobs(globs, root) {
if (!_.isString(globs) && !_.isArray(globs)) {
return [];
}
return globby.sync(globs, {cwd: root}).map(function (match) {
return path.join(root, match);
});
}
function parseFrontmatter(contents) {
var parsed = fm(contents);
var bodyBuf = new Buffer(parsed.body);
return _.extend({contents: bodyBuf}, parsed.attributes);
}
function createFilesExtension(files) {
return _.transform(files, function (result, value, key) {
if (_.isPlainObject(value)) {
value.contents = new Buffer(value.contents || '');
result[key] = value;
}
}, {});
}
function parseJsonDefinition(contents) {
var parsed = JSON.parse(contents);
if (_.isPlainObject(parsed)) {
return parsed;
} else {
throw new Error('JSON file should contain a single root object.');
}
}
{
"name": "gulp-metalsmith",
"version": "0.3.0",
"version": "1.0.0",
"description": "Lightweight gulp plugin for Metalsmith",

@@ -17,7 +17,7 @@ "keywords": [

"scripts": {
"lint": "jshint lib/*.js test/*.js example/*.js",
"lint": "jshint lib/*.js test/*.js tutorial/*.js",
"hint": "npm run lint",
"test": "faucet test/*.js",
"test-all": "npm run lint && npm test",
"clean": "rm -rf node_modules example/build",
"clean": "rm -rf node_modules coverage tutorial/node_modules tutorial/build",
"coverage": "istanbul cover tape test/*.js",

@@ -38,12 +38,7 @@ "coverage-codecov": "cat ./coverage/coverage.json | codecov"

"codecov": "^1.0.1",
"del": "^2.2.0",
"faucet": "0.0.1",
"gulp": "^3.8.11",
"istanbul": "^0.4.2",
"jshint": "^2.9.1",
"metalsmith-layouts": "^1.0.0",
"metalsmith-permalinks": "^0.5.0",
"swig": "^1.4.2",
"tape": "^4.5.0"
}
}

@@ -10,8 +10,30 @@ # gulp-metalsmith

`gulp-metalsmith` is a [gulp](https://github.com/gulpjs/gulp) plugin that incorporates [Metalsmith](http://www.metalsmith.io) builds into gulp pipelines. It aims to be as lightweight as possible. It ships with Metalsmith's replacement that has compatible API (can reuse Metalsmith plugins) and is able to recive JavaScript object containing page definitions. After build, it streams out `vinyl` files. The main difference between bundled Metalsmith and normal Metalsmith is that it does not perform any disc read/write operations, leaving it out to `gulp`.
`gulp-metalsmith` can be feed with specially formatted JSON. It allows building static pages using content providers, like [prismic.io](https://prismic.io) or [Contentful](https://www.contentful.com).
## API changes!
### Installation
Please note that as of v1.0.0 `metalsmith.json()` method is not available. Use
the [`json` configuration option](#use-it-with-json) instead.
## Tutorial
This README file doesn't make sense at first glance or is too technical? **See
[the `gulp-metalsmith` tutorial](./tutorial)**!
## About
`gulp-metalsmith` is a [gulp](https://github.com/gulpjs/gulp) plugin that
incorporates [Metalsmith](http://www.metalsmith.io) builds into gulp pipelines.
It aims to be as lightweight as possible. It is shipped with an API-compatible
Metalsmith replacement that can reuse Metalsmith plugins. It can be [fed with
JSON files containing page definitions](#use-it-with-json).
After build `gulp-metalsmith` streams out `vinyl`files. The main difference
between the bundled Metalsmith and the normal Metalsmith is that it does not
perform any disc read/write operations, leaving it to `gulp`.
## Installation
```sh

@@ -21,7 +43,9 @@ $ npm install --save-dev gulp-metalsmith

### Use it with gulp
## Usage
The simplest build task (just copies all files from `src/` to `build/`):
```js
gulp.task('metalsmith', ['clean'], function() {
gulp.task('metalsmith', function() {
return gulp.src('src/**')

@@ -34,23 +58,32 @@ .pipe(metalsmith())

All options:
```js
s.pipe(metalsmith({
// set Metalsmith's root directory, for example for locating templates, defaults to CWD
gulp.src('src/**').pipe(metalsmith({
// Metalsmith's root directory, for example for locating templates, defaults to CWD
root: __dirname,
// files to exclude from the build
// Files to exclude from the build
ignore: ['src/*.tmp'],
// read frontmatter, defaults to true
// Parsing frontmatter, defaults to true
frontmatter: true,
// Metalsmith plugins to use
use: [ permalinks(), layouts({ engine: 'swig' }) ],
// Initial Metalsmith metadata:
// Metalsmith plugins to use:
use: [
markdown(),
layouts({engine: 'swig'})
],
// Initial Metalsmith metadata, defaults to {}
metadata: {
site_title: 'Sample static site'
}
},
// List of JSON files that contain page definitions
// true means "all JSON files", see the section below
json: ['src/pages.json']
}));
```
### Feed it with JSON
## Use it with JSON
Given the file `src/pages.json`:
```json
```js
{

@@ -71,22 +104,30 @@ "index.html": {

You can do this:
```js
gulp.task('metalsmith-json', ['clean'], function() {
return gulp.src('src/pages.json')
.pipe(metalsmith.json({ use: [ layouts({ engine: 'swig' }) ]))
.pipe(gulp.dest('build'));
});
gulp.src('src/**').pipe(metalsmith({
use: [layouts({engine: 'swig'})],
json: true
}));
```
Multiple JSON files (`*.json`) are also accepted.
This way your Metalsmith build will contain two additional files, `index.html`
and `contact.html`. The source file `pages.json` won't be included. Following
rules apply:
### Examples, tests
- be default all JSON files in the pipeline are included "as is"
- when the `json` configuration options is set to `true`, all JSON files are
parsed and replaced with files defined in their content
- when the `json` configuration option is a glob string or an array of globs,
only JSON files matching these globs are parsed and define new files. The rest
of JSON files is passed "as is"
Examples are stored in `example/`. Tests can be run with `npm test`.
### Author
## Author
[Jakub Elżbieciak](https://elzbieciak.pl)
[Jakub Elżbieciak](https://elzbieciak.pl) /
[@jelzbieciak](https://twitter.com/jelzbieciak)
### License
## License
MIT

@@ -7,39 +7,30 @@ var test = require('tape');

test('Metalsmith-compatible API', function (t) {
var API = [
'build',
'source',
'destination',
'clean',
'frontmatter',
'use',
'run',
'metadata',
'path'
test('Metalsmith exposes a compatible API', function (t) {
var m = metalsmith();
var methods = [
'build', 'source', 'destination', 'clean',
'frontmatter', 'use', 'run', 'metadata', 'path'
];
var m = metalsmith();
t.plan(API.length);
API.forEach(function (method) {
t.true(_.isFunction(m[method]));
methods.forEach(function (method) {
t.ok(_.isFunction(m[method]));
});
t.end();
});
test('Not implemented methods', function (t) {
test('Metalsmith throws for not implemented methods', function (t) {
var m = metalsmith();
var NOT_IMPLEMENTED = metalsmith.NOT_IMPLEMENTED;
t.plan(NOT_IMPLEMENTED.length);
NOT_IMPLEMENTED.forEach(function (method) {
metalsmith.NOT_IMPLEMENTED.forEach(function (method) {
t.throws(m[method]);
});
t.end();
});
test('Metadata getter/setter', function (t) {
test('Metalsmith exposes a metadata getter/setter', function (t) {
var m = metalsmith();
var value = {test: 123, nested: {msg: 'Hello'}};
t.deepEquals(m.metadata(), {});
t.equals(m.metadata(value), m);
t.equals(m.metadata(), value);
t.equal(m.metadata(value), m);
t.equal(m.metadata(), value);
t.deepEquals(m.metadata(), {test: 123, nested: {msg: 'Hello'}});

@@ -49,15 +40,15 @@ t.end();

test('Path getter', function (t) {
test('Metalsmith exposes a path getter', function (t) {
var cwd = process.cwd();
var m = metalsmith();
t.equals(m.path(), cwd);
t.equals(m.path('../hello.txt'), path.resolve(cwd, '..', 'hello.txt'));
t.equal(m.path(), cwd);
t.equal(m.path('../hello.txt'), path.resolve(cwd, '..', 'hello.txt'));
m = metalsmith('/tmp/metalsmith');
t.equals(m.path(), '/tmp/metalsmith');
t.equals(m.path('test/hello.txt'), '/tmp/metalsmith/test/hello.txt');
t.equals(m.path('../../test.html'), '/test.html');
t.equal(m.path(), '/tmp/metalsmith');
t.equal(m.path('test/hello.txt'), '/tmp/metalsmith/test/hello.txt');
t.equal(m.path('../../test.html'), '/test.html');
t.end();
});
test('Middleware flow', function (t) {
test('Metalsmith flows files through middleware functions', function (t) {
var m = metalsmith();

@@ -75,8 +66,6 @@ t.plan(3);

m.run({}, function () {
t.pass();
});
m.run({}, t.pass);
});
test('Running middleware functions', function (t) {
test('Metalsmith passes the same files/metadata to middleware', function (t) {
var m = metalsmith();

@@ -95,9 +84,9 @@ t.plan(3);

m.run({}, function (err, files) {
t.true(_.isNull(err));
t.true(_.isObject(files.added_file));
t.equals(m.metadata().added_metadata, 123);
t.ok(_.isNull(err));
t.ok(_.isObject(files.added_file));
t.equal(m.metadata().added_metadata, 123);
});
});
test('Handling middleware errors', function (t) {
test('Metalsmith handles middleware errors', function (t) {
var m = metalsmith();

@@ -117,5 +106,5 @@ t.plan(2);

m.run({}, function (err) {
t.true(_.isError(err));
t.equals(err.message, 'Second middleware error.');
t.ok(_.isError(err));
t.equal(err.message, 'Second middleware error.');
});
});

@@ -5,3 +5,3 @@ var test = require('tape');

var vinyl = require('vinyl-fs');
var path = require('path');
var pm = require('path');
var fs = require('fs');

@@ -12,16 +12,16 @@ var _ = require('lodash');

var base = _.partial(path.join, __dirname, 'fixtures');
var prepare = _.partial(prepareFn, 'src');
var prepareJson = _.partial(prepareFn, 'json');
function base(path) {
return pm.join(__dirname, 'fixtures', path || '');
}
function prepareFn(dir, globs, opts) {
var fn = dir === 'json' ? plugin.json : plugin;
globs = _.isString(globs) ? [globs] : globs;
globs = _.map(globs, function (glob) {
return base(dir, glob || '');
});
function read(path) {
return fs.readFileSync(base(path));
}
function prepare(globs, opts) {
opts = _.extend({root: base()}, opts);
globs = _.isString(globs) ? [globs] : globs;
globs = _.map(globs, base);
return vinyl.src(globs).pipe(fn(opts));
return vinyl.src(globs).pipe(plugin(opts));
}

@@ -33,3 +33,3 @@

if (fn && fileMatches && contentMatches) {
if (_.isFunction(fn) && fileMatches && contentMatches) {
fn();

@@ -39,19 +39,14 @@ }

test('Plugin API', function (t) {
t.true(_.isFunction(plugin));
t.true(_.isFunction(plugin.json));
t.end();
});
test('Plugin returns a stream', function (t) {
var methods = ['emit', 'on', 'pipe', 'push', 'write', 'end'];
test('Plugin is a function that returns a stream', function (t) {
t.ok(_.isFunction(plugin));
var p = plugin();
t.plan(methods.length);
methods.forEach(function (m) {
t.true(_.isFunction(p[m]));
['emit', 'on', 'pipe', 'push', 'write', 'end'].forEach(function (method) {
t.ok(_.isFunction(p[method]));
});
t.end();
});
test('Failure on a file containing a stream', function (t) {
test('Plugin emits error for vinyl files of type "stream"', function (t) {
var file = new gutil.File({contents: through.obj()});

@@ -62,14 +57,14 @@ var s = plugin();

s.on('error', function (err) {
t.true(err instanceof gutil.PluginError);
t.equals(err.plugin, plugin.PLUGIN_NAME);
t.true(_.includes(err.message, 'not supported'));
t.ok(err instanceof gutil.PluginError);
t.equal(err.plugin, plugin.PLUGIN_NAME);
t.ok(_.includes(err.message, 'not supported'));
});
t.true(file.isStream());
t.ok(file.isStream());
s.end(file);
});
test('Handle a stream of buffered vinyl files', function (t) {
test('Plugin handles a stream of vinyl files of type "buffer"', function (t) {
t.plan(3);
prepare('**').pipe(through.obj(function (f, enc, cb) {
prepare('*').pipe(through.obj(function (f, enc, cb) {
testContent(f, 'index.html', 'Index page', t.pass);

@@ -81,49 +76,37 @@ testContent(f, 'contact.html', 'Contact page', t.pass);

test('Omit ignored files', function (t) {
t.plan(5);
prepare('**', {ignore: 'src/index.html', use: testSingle});
prepare('**', {ignore: ['src/**', '!src/*.jpg'], use: testMultiple});
test('Plugin omits ignored files defined in opts.ignore', function (t) {
t.plan(6);
prepare('*.html', {ignore: 'index.html', use: testSingle});
prepare('*', {ignore: ['*', '!*.jpg'], use: testMultiple});
function testSingle(files) {
t.equals(_.values(files).length, 2);
t.equals(files['contact.html'].title, 'Contact');
t.ok(files['trees.jpg']);
t.equal(_.values(files).length, 1);
t.equal(files['contact.html'].title, 'Contact');
t.notOk(files['index.html']);
}
function testMultiple(files) {
t.equals(_.values(files).length, 1);
t.equal(_.values(files).length, 1);
t.ok(files['trees.jpg']);
t.notOk(files['pc.png']);
}
});
test('Do not touch non-utf8 files', function (t) {
t.plan(6);
test('Plugin does not touch non-UTF8 files', function (t) {
t.plan(3);
prepare('*.jpg').pipe(through.obj(function (f, enc, cb) {
t.true(f.isBuffer());
t.true(f.contents.equals(fs.readFileSync(base('src', 'trees.jpg'))));
t.ok(f.isBuffer());
t.ok(f.contents.equals(read('trees.jpg')));
cb();
}, t.pass));
prepareJson('*.png').pipe(through.obj(function (f, enc, cb) {
t.true(f.isBuffer());
t.true(f.contents.equals(fs.readFileSync(base('json', 'pc.png'))));
cb();
}, t.pass));
});
test('Accept a single middleware function', function (t) {
test('Plugin accepts a single middleware function', function (t) {
t.plan(1);
prepare('index.html', {
use: function () {
t.pass();
}
});
prepare('index.html', {use: t.pass});
});
test('Add metadata from configuration options', function (t) {
test('Plugin adds metadata items from opts.metadata', function (t) {
t.plan(3);
prepare('index.html', {
prepare('*.html', {
metadata: {

@@ -133,14 +116,14 @@ item1: true,

},
use: [testMiddleware]
use: [testMetadata]
});
function testMiddleware(files, m, next) {
function testMetadata(files, m, next) {
var meta = m.metadata();
t.true(meta.item1);
t.equals(meta.item2.length, 3);
t.equals(meta.item2[2], 'things');
t.ok(meta.item1);
t.equal(meta.item2.length, 3);
t.equal(meta.item2[2], 'things');
}
});
test('Frontmatter configuration option', function (t) {
test('Plugin parses frontmatter if opts.frontmatter is true', function (t) {
t.plan(2);

@@ -152,3 +135,3 @@ prepare('index.html').pipe(stillIncludes(false));

return through.obj(function (f, enc, cb) {
t[method ? 'true' : 'false'](_.includes(f.contents.toString(), '---'));
t[method ? 'ok' : 'notOk'](_.includes(f.contents.toString(), '---'));
cb();

@@ -159,34 +142,45 @@ });

test('Failure when Metalsmith fails', function (t) {
var s = prepare('**', {
use: [function (f, m, next) {
next(new Error('boom!'));
}]
});
test('Plugin fails when Metalsmith or middleware fails', function (t) {
t.plan(2);
var s = prepare('**', {use: failingMiddleware});
s.on('error', function (err) {
t.true(err instanceof gutil.PluginError);
t.equals(err.message, 'boom!');
t.ok(err instanceof gutil.PluginError);
t.equal(err.message, 'boom!');
});
function failingMiddleware(f, m, next) {
next(new Error('boom!'));
}
});
test('Failure on an invalid JSON', function (t) {
test('Plugin does not touch JSON files if are not definitions', function (t) {
t.plan(3);
prepare('**', {use: testJson});
prepareJson('invalid_page.json').on('error', isPluginError);
prepareJson('array.json').on('error', function (err) {
function testJson(files) {
t.ok(files['json/invalid.json']);
t.ok(files['json/pages.json']);
t.ok(files['json/pages.json'].contents.equals(read('json/pages.json')));
}
});
test('Plugin fails when JSON definitions are invalid', function (t) {
t.plan(3);
prepare('**', {json: 'json/invalid.json'}).on('error', isPluginError);
prepare('**', {json: 'json/array.json'}).on('error', function (err) {
isPluginError(err);
t.true(_.includes(err.message, 'single root object'));
t.ok(_.includes(err.message, 'single root object'));
});
function isPluginError(err) {
t.true(err instanceof gutil.PluginError);
t.ok(err instanceof gutil.PluginError);
}
});
test('Handle JSON input', function (t) {
test('Plugin creates pages from JSON definitions', function (t) {
t.plan(3);
prepareJson('pages.json').pipe(through.obj(function (f, enc, cb) {
var s = prepare('**/*.json', {json: 'json/pages.json'});
s.pipe(through.obj(function (f, enc, cb) {
testContent(f, 'index.html', 'Index page', t.pass);

@@ -198,5 +192,6 @@ testContent(f, 'contact.html', 'Contact page', t.pass);

test('Handle a multi-file JSON input', function (t) {
var s = prepareJson(['pages.json', 'offer_page.json']);
test('Plugin merges and creates pages from multiple JSON inputs', function (t) {
t.plan(4);
var json = ['json/pages.json', 'json/offer_page.json'];
var s = prepare('**/*.json', {json: json});

@@ -211,16 +206,47 @@ s.pipe(through.obj(function (f, enc, cb) {

test('Handle a JSON defined page w/o contents', function (t) {
t.plan(2);
prepareJson('empty_page.json').pipe(through.obj(function (f) {
t.equals(f.path, 'empty.html');
t.equals(f.contents.toString(), '');
}));
test('Plugin mixes both regular and JSON defined files', function (t) {
t.plan(5);
var globs = ['*.html', 'json/map_page.json'];
var s = prepare(globs, {json: 'json/*.json', use: testFiles});
function testFiles(files) {
t.equal(_.values(files).length, 3);
t.ok(files['index.html']);
t.ok(files['index.html'].contents.toString().indexOf('Index') > -1);
t.ok(files['map.html']);
t.equal(files['map.html'].contents.toString(), '<h2>Map page</h2>');
}
});
test('Ignore invalid file keys', function (t) {
test('Plugin converts all JSON files if opts.json is true', function (t) {
t.plan(3);
var globs = ['json/pages.json', 'json/*_page.json'];
var s = prepare(globs, {json: true, use: testFiles});
function testFiles(files) {
t.equal(_.values(files).length, 5);
t.ok(files['index.html']);
t.ok(files['map.html']);
}
});
test('Plugin handles a JSON defined page w/o contents', function (t) {
t.plan(3);
var s = prepare('json/empty_page.json', {json: true});
s.pipe(through.obj(function (f, enc, cb) {
t.equal(f.path, 'empty.html');
t.equal(f.contents.toString(), '');
cb();
}, t.pass));
});
test('Plugin ignores invalid file keys in JSON definition', function (t) {
t.plan(2);
prepareJson('map_page.json').pipe(through.obj(function (f, enc, cb) {
t.equals(f.path, 'map.html');
var s = prepare('json/map_page.json', {json: true});
s.pipe(through.obj(function (f, enc, cb) {
t.equal(f.path, 'map.html');
cb();
}, t.pass));
});

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc