metalsmith
Advanced tools
Comparing version 2.4.3 to 2.5.0
@@ -9,2 +9,36 @@ # Change Log | ||
## [2.5.0] - 2022-06-10 | ||
### Added | ||
- [#354] Added `Metalsmith#env` method. Supports passing `DEBUG` and `DEBUG_LOG` amongst others. Sets `CLI: true` when run from the metalsmith CLI. [`b42df8c`](https://github.com/metalsmith/metalsmith/commit/b42df8c), [`446c676`](https://github.com/metalsmith/metalsmith/commit/446c676), [`33d936b`](https://github.com/metalsmith/metalsmith/commit/33d936b), [`4c483a3`](https://github.com/metalsmith/metalsmith/commit/4c483a3) | ||
- [#356] Added `Metalsmith#debug` method for creating plugin debuggers | ||
- [#362] Upgraded all generator-based methods (`Metalsmith#read`,`Metalsmith#readFile`,`Metalsmith#write`,`Metalsmith#writeFile`, `Metalsmith#run` and `Metalsmith#process`) to dual callback-/ promise-based methods [`16a91c5`](https://github.com/metalsmith/metalsmith/commit/16a91c5), [`faf6ab6`](https://github.com/metalsmith/metalsmith/commit/faf6ab6), [`6cb6229`](https://github.com/metalsmith/metalsmith/commit/6cb6229) | ||
- Added org migration notification to postinstall script to encourage users to upgrade [`3a11a24`](https://github.com/metalsmith/metalsmith/commit/3a11a24) | ||
### Removed | ||
- [#231] Dropped support for Node < 12 [`0a53007`](https://github.com/metalsmith/metalsmith/commit/0a53007) | ||
- **Dependencies:** | ||
- `thunkify`: replaced with promise-based implementation [`faf6ab6`](https://github.com/metalsmith/metalsmith/commit/faf6ab6) | ||
- `unyield` replaced with promise-based implementation [`faf6ab6`](https://github.com/metalsmith/metalsmith/commit/faf6ab6) | ||
- `co-fs-extra`: replaced with native Node.js methods [`faf6ab6`](https://github.com/metalsmith/metalsmith/commit/faf6ab6) | ||
- `chalk`: not necessary for the few colors used by Metalsmith CLI [`1dae1cb`](https://github.com/metalsmith/metalsmith/commit/a1dae1cb) | ||
- `clone`: see [#247] [`a871af6`](https://github.com/metalsmith/metalsmith/commit/a871af6) | ||
### Updated | ||
- Restructured and updated `README.md` [`0da0c4d`](https://github.com/metalsmith/metalsmith/commit/0da0c4d) | ||
- [#247] Calling `Metalsmith#metadata` no longer clones the object passed to it, overwriting the previous metadata, but merges it into existing metadata. | ||
[#362]: https://github.com/metalsmith/metalsmith/issues/362 | ||
[#354]: https://github.com/metalsmith/metalsmith/issues/354 | ||
[#355]: https://github.com/metalsmith/metalsmith/issues/355 | ||
[#356]: https://github.com/metalsmith/metalsmith/issues/356 | ||
[#247]: https://github.com/metalsmith/metalsmith/issues/247 | ||
### Fixed | ||
- [#355] Proper path resolution for edge-cases using CLI, running metalsmith from outside or subfolder of `metalsmith.directory()`[`5d75539`](https://github.com/metalsmith/metalsmith/commit/5d75539) | ||
## [2.4.3] - 2022-05-16 | ||
@@ -11,0 +45,0 @@ |
const fs = require('fs') | ||
const { promisify } = require('util') | ||
const { resolve, relative, normalize } = require('path') | ||
const { resolve, relative, normalize, dirname } = require('path') | ||
const stat = promisify(fs.stat) | ||
const readFile = promisify(fs.readFile) | ||
const writeFile = promisify(fs.writeFile) | ||
const mkdir = promisify(fs.mkdir) | ||
const fsreaddir = promisify(fs.readdir) | ||
const chmod = promisify(fs.chmod) | ||
const rmrf = require('rimraf') | ||
@@ -38,2 +44,6 @@ const micromatch = require('micromatch') | ||
} | ||
function writeStream(path) { | ||
return fs.createWriteStream(path, 'utf-8') | ||
} | ||
/** | ||
@@ -71,7 +81,7 @@ * Recursively remove a directory | ||
* Recursive readdir with support for ignores | ||
* @private | ||
* @param {String} dir | ||
* @param {Array<String|Function>} ignores | ||
* @param {Function} callback - Callback for thunkify @TODO remove in the future | ||
*/ | ||
function readdir(dir, ignores, callback) { | ||
function readdir(dir, ignores) { | ||
if (Array.isArray(ignores)) { | ||
@@ -90,5 +100,3 @@ ignores = { | ||
} | ||
const stat = promisify(fs.stat) | ||
const result = promisify(fs.readdir) | ||
.bind(fs)(dir.current) | ||
const result = fsreaddir(dir.current) | ||
.then((children) => { | ||
@@ -128,13 +136,44 @@ const filtered = [] | ||
}) | ||
.catch((err) => { | ||
throw err | ||
}) | ||
if (callback) { | ||
result.then( | ||
(files) => callback(null, files), | ||
(err) => callback(err) | ||
) | ||
} else { | ||
return result | ||
return result | ||
} | ||
/** | ||
* Run `fn` in parallel on #`concurrency` number of `items`, spread over #`items / concurrency` sequential batches | ||
* @private | ||
* @param {() => Promise} fn | ||
* @param {*[]} items | ||
* @param {number} concurrency | ||
* @returns {Promise<*[]>} | ||
*/ | ||
function batchAsync(fn, items, concurrency) { | ||
let batches = Promise.resolve([]) | ||
items = [...items] | ||
while (items.length) { | ||
const slice = items.splice(0, concurrency) | ||
batches = batches.then((previousBatch) => { | ||
return Promise.all(slice.map((...args) => fn(...args))).then((currentBatch) => [ | ||
...previousBatch, | ||
...currentBatch | ||
]) | ||
}) | ||
} | ||
return batches | ||
} | ||
/** | ||
* Output a file and create any non-existing directories in the process | ||
* @private | ||
**/ | ||
function outputFile(file, data, mode) { | ||
return mkdir(dirname(file), { recursive: true }) | ||
.then(() => writeFile(file, data)) | ||
.then(() => (mode ? chmod(file, mode) : Promise.resolve())) | ||
} | ||
const helpers = { | ||
@@ -146,7 +185,13 @@ isBoolean, | ||
isUndefined, | ||
isFunction, | ||
match, | ||
rm, | ||
readdir | ||
readdir, | ||
outputFile, | ||
stat, | ||
readFile, | ||
batchAsync, | ||
writeStream | ||
} | ||
module.exports = helpers |
400
lib/index.js
@@ -0,14 +1,32 @@ | ||
'use strict' | ||
const assert = require('assert') | ||
const clone = require('clone') | ||
const fs = require('co-fs-extra') | ||
const matter = require('gray-matter') | ||
const Mode = require('stat-mode') | ||
const path = require('path') | ||
let { readdir } = require('./helpers') | ||
const { rm, isString, isBoolean, isObject, isNumber, isUndefined, match } = require('./helpers') | ||
const thunkify = require('thunkify') | ||
const unyield = require('unyield') | ||
const { | ||
readdir, | ||
batchAsync, | ||
isFunction, | ||
outputFile, | ||
stat, | ||
readFile, | ||
writeStream, | ||
rm, | ||
isString, | ||
isBoolean, | ||
isObject, | ||
isNumber, | ||
isUndefined, | ||
match | ||
} = require('./helpers') | ||
const utf8 = require('is-utf8') | ||
const Ware = require('ware') | ||
const { Debugger, fileLogHandler } = require('./debug') | ||
const symbol = { | ||
env: Symbol('env'), | ||
log: Symbol('log') | ||
} | ||
/** | ||
@@ -79,8 +97,2 @@ * Metalsmith representation of the files in `metalsmith.source()`. | ||
/** | ||
* Thunks. | ||
* @private | ||
*/ | ||
readdir = thunkify(readdir) | ||
/** | ||
* Export `Metalsmith`. | ||
@@ -113,2 +125,11 @@ */ | ||
this.frontmatter(true) | ||
Object.defineProperty(this, symbol.env, { | ||
value: Object.create(null), | ||
enumerable: false | ||
}) | ||
Object.defineProperty(this, symbol.log, { | ||
value: null, | ||
enumerable: false, | ||
writable: true | ||
}) | ||
} | ||
@@ -135,3 +156,3 @@ | ||
* | ||
* @param {Object} [directory] | ||
* @param {string} [directory] | ||
* @return {string|Metalsmith} | ||
@@ -166,3 +187,3 @@ * | ||
assert(isObject(metadata), 'You must pass a metadata object.') | ||
this._metadata = clone(metadata) | ||
this._metadata = Object.assign(this._metadata || {}, metadata) | ||
return this | ||
@@ -242,6 +263,8 @@ } | ||
/** @typedef {Object} GrayMatterOptions */ | ||
/** | ||
* Optionally turn off frontmatter parsing. | ||
* Optionally turn off frontmatter parsing or pass a [gray-matter options object](https://github.com/jonschlinkert/gray-matter/tree/4.0.2#option) | ||
* | ||
* @param {boolean} [frontmatter] | ||
* @param {boolean|GrayMatterOptions} [frontmatter] | ||
* @return {boolean|Metalsmith} | ||
@@ -252,2 +275,3 @@ * | ||
* metalsmith.frontmatter() // returns false | ||
* metalsmith.frontmatter({ excerpt: true }) | ||
*/ | ||
@@ -273,3 +297,3 @@ | ||
* @example | ||
* metalsmith.ignore() | ||
* metalsmith.ignore() // return a list of ignored file paths | ||
* metalsmith.ignore('layouts') // ignore the layouts directory | ||
@@ -303,4 +327,4 @@ * metalsmith.ignore(['.*', 'data.json']) // ignore dot files & a data file | ||
* @param {string|string[]} patterns - one or more glob patterns | ||
* @param {string[]} [input] array of strings to match against | ||
* @param {import('micromatch').Options} options - [micromatch options](https://github.com/micromatch/micromatch#options), except `format` | ||
* @param {string[]} [input] array of strings to match against | ||
* @returns {string[]} An array of matching file paths | ||
@@ -316,2 +340,43 @@ */ | ||
/** | ||
* Get or set one or multiple metalsmith environment variables. Metalsmith env vars are case-insensitive. | ||
* @param {string|Object} [vars] name of the environment variable, or an object with `{ name: 'value' }` pairs | ||
* @param {string|number|boolean} [value] value of the environment variable | ||
* @returns {string|number|boolean|Object|Metalsmith} | ||
* @example | ||
* // pass all Node env variables | ||
* metalsmith.env(process.env) | ||
* // get all env variables | ||
* metalsmith.env() | ||
* // get DEBUG env variable | ||
* metalsmith.env('DEBUG') | ||
* // set DEBUG env variable (chainable) | ||
* metalsmith.env('DEBUG', '*') | ||
* // set multiple env variables at once (chainable) | ||
* // this does not clear previously set variables | ||
* metalsmith.env({ | ||
* DEBUG: false, | ||
* ENV: 'development' | ||
* }) | ||
*/ | ||
Metalsmith.prototype.env = function (vars, value) { | ||
if (isString(vars)) { | ||
if (arguments.length === 1) { | ||
return this[symbol.env][vars.toUpperCase()] | ||
} | ||
if (!(isFunction(value) || isObject(value))) { | ||
this[symbol.env][vars.toUpperCase()] = value | ||
return this | ||
} | ||
throw new TypeError('Environment variable values can only be primitive: Number, Boolean, String or null') | ||
} | ||
if (isObject(vars)) { | ||
Object.entries(vars).forEach(([key, value]) => this.env(key, value)) | ||
return this | ||
} | ||
if (isUndefined(vars)) return Object.assign(Object.create(null), this[symbol.env]) | ||
} | ||
Metalsmith.prototype.debug = Debugger | ||
/** | ||
* Build with the current settings to the destination directory. | ||
@@ -334,35 +399,43 @@ * | ||
const dest = this.destination() | ||
let _files | ||
return ( | ||
(clean ? rm(dest) : Promise.resolve()) | ||
.then(() => { | ||
const result = (clean ? rm(dest) : Promise.resolve()) | ||
.then(() => { | ||
if (this.debug.enabled && this.env('DEBUG_LOG')) { | ||
this[symbol.log] = writeStream(this.path(this.env('DEBUG_LOG'))) | ||
this.debug.handle = fileLogHandler(this[symbol.log]) | ||
this.debug.colors = false | ||
return new Promise((resolve, reject) => { | ||
this.process((err, files) => { | ||
if (err) reject(err) | ||
resolve(files) | ||
this[symbol.log].on('error', (err) => { | ||
let error = err | ||
if (error.code === 'ENOENT') { | ||
error = new Error( | ||
`Inexistant directory path "${path.dirname(this.env('DEBUG_LOG'))}" given for DEBUG_LOG` | ||
) | ||
error.code = 'invalid_logpath' | ||
reject(error) | ||
} | ||
}) | ||
if (this[symbol.log].pending) { | ||
this[symbol.log].on('ready', () => resolve()) | ||
} else { | ||
resolve() | ||
} | ||
}) | ||
} | ||
}) | ||
.then(this.process.bind(this)) | ||
.then((files) => { | ||
return this.write(files).then(() => { | ||
if (this[symbol.log]) this[symbol.log].end() | ||
return files | ||
}) | ||
.then((files) => { | ||
_files = files | ||
return new Promise((resolve, reject) => { | ||
this.write(files, null, (err) => { | ||
if (err) reject(err) | ||
resolve(files) | ||
}) | ||
}) | ||
}) | ||
.then((files) => { | ||
if (callback) callback(null, files) | ||
else return Promise.resolve(files) | ||
}) | ||
// this catch block is required to support backwards-compatibility and pass the error to callback-driven flows | ||
.catch( | ||
/* istanbul ignore next */ (err) => { | ||
if (callback) callback(err, _files) | ||
else return Promise.reject(err) | ||
} | ||
) | ||
) | ||
}) | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
if (isFunction(callback)) { | ||
result.then((files) => callback(null, files), callback) | ||
} else { | ||
return result | ||
} | ||
} | ||
@@ -375,6 +448,6 @@ | ||
* @param {BuildCallback} [callback] | ||
* @return {Files} | ||
* @return {Promise<Files>|void} | ||
* | ||
* @example | ||
* metalsmith.process(err => { | ||
* metalsmith.process((err, files) => { | ||
* if (err) throw err | ||
@@ -386,8 +459,15 @@ * console.log('Success') | ||
Metalsmith.prototype.process = unyield(function* () { | ||
let files = yield this.read() | ||
files = yield this.run(files) | ||
return files | ||
}) | ||
Metalsmith.prototype.process = function (callback) { | ||
const result = this.read(this.source()).then((files) => { | ||
return this.run(files, this.plugins) | ||
}) | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
if (callback) { | ||
result.then((files) => callback(null, files), callback) | ||
} else { | ||
return result | ||
} | ||
} | ||
/** | ||
@@ -400,13 +480,39 @@ * Run a set of `files` through the plugins stack. | ||
* @param {Plugin[]} plugins | ||
* @return {Object} | ||
* @return {Promise<Files>|void} | ||
*/ | ||
Metalsmith.prototype.run = unyield(function* (files, plugins) { | ||
Metalsmith.prototype.run = function (files, plugins, callback) { | ||
let debugValue = this.env('DEBUG') | ||
if (debugValue === false) { | ||
this.debug.disable() | ||
} else { | ||
if (debugValue === true) debugValue = '*' | ||
this.debug.enable(debugValue) | ||
} | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
const last = arguments[arguments.length - 1] | ||
callback = isFunction(last) ? last : undefined | ||
plugins = Array.isArray(plugins) ? plugins : this.plugins | ||
this._files = files | ||
const ware = new Ware(plugins || this.plugins) | ||
const run = thunkify(ware.run.bind(ware)) | ||
const res = yield run(files, this) | ||
return res[0] | ||
}) | ||
const ware = new Ware(plugins) | ||
const run = ware.run.bind(ware) | ||
const result = new Promise((resolve, reject) => { | ||
run(files, this, (err, files) => { | ||
if (err) reject(err) | ||
else resolve(files) | ||
}) | ||
}) | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
if (callback) { | ||
result.then((files) => callback(null, files, this), callback) | ||
} else { | ||
return result | ||
} | ||
} | ||
/** | ||
@@ -419,31 +525,33 @@ * Read a dictionary of files from a `dir`, parsing frontmatter. If no directory | ||
* @param {string} [dir] | ||
* @return {Object} | ||
* @return {Promise<Files>|void} | ||
*/ | ||
Metalsmith.prototype.read = unyield(function* (dir) { | ||
dir = dir || this.source() | ||
Metalsmith.prototype.read = function (dir, callback) { | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
if (isFunction(dir) || !arguments.length) { | ||
callback = dir | ||
dir = this.source() | ||
} | ||
const read = this.readFile.bind(this) | ||
const concurrency = this.concurrency() | ||
const ignores = this.ignores || null | ||
const paths = yield readdir(dir, ignores) | ||
let files = [] | ||
let complete = 0 | ||
let batch | ||
const result = readdir(dir, ignores).then((paths) => { | ||
return batchAsync((p) => read(p), paths, concurrency).then((files) => { | ||
const result = paths.reduce((memo, file, i) => { | ||
file = path.relative(dir, file) | ||
memo[file] = files[i] | ||
return memo | ||
}, {}) | ||
return result | ||
}) | ||
}) | ||
while (complete < paths.length) { | ||
batch = paths.slice(complete, complete + concurrency) | ||
batch = yield batch.map(read) | ||
files = files.concat(batch) | ||
complete += concurrency | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
if (callback) { | ||
result.then((files) => callback(null, files), callback) | ||
} else { | ||
return result | ||
} | ||
} | ||
return paths.reduce(memoizer, {}) | ||
function memoizer(memo, file, i) { | ||
file = path.relative(dir, file) | ||
memo[file] = files[i] | ||
return memo | ||
} | ||
}) | ||
/** | ||
@@ -456,46 +564,54 @@ * Read a `file` by path. If the path is not absolute, it will be resolved | ||
* @param {string} file | ||
* @returns {File} | ||
* @returns {Promise<File>|void} | ||
*/ | ||
Metalsmith.prototype.readFile = unyield(function* (file) { | ||
Metalsmith.prototype.readFile = function (file, callback) { | ||
const src = this.source() | ||
let ret = {} | ||
if (!path.isAbsolute(file)) file = path.resolve(src, file) | ||
const frontmatter = this.frontmatter() | ||
try { | ||
const frontmatter = this.frontmatter() | ||
const stats = yield fs.stat(file) | ||
const buffer = yield fs.readFile(file) | ||
let parsed | ||
if (frontmatter && utf8(buffer)) { | ||
try { | ||
parsed = matter(buffer.toString(), this._frontmatter) | ||
} catch (e) { | ||
const err = new Error('Invalid frontmatter in the file at: ' + file) | ||
err.code = 'invalid_frontmatter' | ||
throw err | ||
const result = Promise.all([ | ||
// @TODO: this stat should be passed from the readdir function, not done twice | ||
stat(file), | ||
readFile(file) | ||
]) | ||
.then(([stats, buffer]) => { | ||
let ret = {} | ||
if (frontmatter && utf8(buffer)) { | ||
try { | ||
const parsed = matter(buffer.toString(), this._frontmatter) | ||
ret = parsed.data | ||
if (parsed.excerpt) { | ||
ret.excerpt = parsed.excerpt | ||
} | ||
ret.contents = Buffer.from(parsed.content) | ||
} catch (e) { | ||
const err = new Error('Invalid frontmatter in the file at: ' + file) | ||
err.code = 'invalid_frontmatter' | ||
return Promise.reject(err) | ||
} | ||
} else { | ||
ret.contents = buffer | ||
} | ||
ret = parsed.data | ||
if (parsed.excerpt) { | ||
ret.excerpt = parsed.excerpt | ||
} | ||
ret.contents = Buffer.from(parsed.content) | ||
} else { | ||
ret.contents = buffer | ||
} | ||
ret.mode = Mode(stats).toOctal() | ||
ret.stats = stats | ||
return ret | ||
}) | ||
.catch((e) => { | ||
if (e.code == 'invalid_frontmatter') return Promise.reject(e) | ||
e.message = 'Failed to read the file at: ' + file + '\n\n' + e.message | ||
e.code = 'failed_read' | ||
return Promise.reject(e) | ||
}) | ||
ret.mode = Mode(stats).toOctal() | ||
ret.stats = stats | ||
} catch (e) { | ||
if (e.code == 'invalid_frontmatter') throw e | ||
e.message = 'Failed to read the file at: ' + file + '\n\n' + e.message | ||
e.code = 'failed_read' | ||
throw e | ||
if (isFunction(callback)) { | ||
result.then( | ||
(file) => callback(null, file), | ||
(err) => callback(err) | ||
) | ||
} else { | ||
return result | ||
} | ||
} | ||
return ret | ||
}) | ||
/** | ||
@@ -509,23 +625,30 @@ * Write a dictionary of `files` to a destination `dir`. If no directory is | ||
* @param {string} [dir] | ||
* @returns {Promise<null>|void} | ||
*/ | ||
Metalsmith.prototype.write = unyield(function* (files, dir) { | ||
dir = dir || this.destination() | ||
Metalsmith.prototype.write = function (files, dir, callback) { | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
const last = arguments[arguments.length - 1] | ||
callback = isFunction(last) ? last : undefined | ||
dir = dir && !isFunction(dir) ? dir : this.destination() | ||
const write = this.writeFile.bind(this) | ||
const concurrency = this.concurrency() | ||
const keys = Object.keys(files) | ||
let complete = 0 | ||
let batch | ||
while (complete < keys.length) { | ||
batch = keys.slice(complete, complete + concurrency) | ||
yield batch.map(writer) | ||
complete += concurrency | ||
} | ||
const operation = batchAsync( | ||
(key) => { | ||
return write(key, files[key]) | ||
}, | ||
keys, | ||
concurrency | ||
) | ||
function writer(key) { | ||
const file = path.resolve(dir, key) | ||
return write(file, files[key]) | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
if (callback) { | ||
operation.then(() => callback(null), callback) | ||
} else { | ||
return operation | ||
} | ||
}) | ||
} | ||
@@ -540,16 +663,21 @@ /** | ||
* @param {File} data | ||
* @returns {Promise<void>|void} | ||
*/ | ||
Metalsmith.prototype.writeFile = unyield(function* (file, data) { | ||
Metalsmith.prototype.writeFile = function (file, data, callback) { | ||
const dest = this.destination() | ||
if (!path.isAbsolute(file)) file = path.resolve(dest, file) | ||
try { | ||
yield fs.outputFile(file, data.contents) | ||
if (data.mode) yield fs.chmod(file, data.mode) | ||
} catch (e) { | ||
const result = outputFile(file, data.contents, data.mode).catch((e) => { | ||
e.code = 'failed_write' | ||
e.message = 'Failed to write the file at: ' + file + '\n\n' + e.message | ||
throw e | ||
return Promise.reject(e) | ||
}) | ||
/* block required for Metalsmith 2.x callback-flow compat */ | ||
if (callback) { | ||
result.then(callback, callback) | ||
} else { | ||
return result | ||
} | ||
}) | ||
} |
{ | ||
"name": "metalsmith", | ||
"version": "2.4.3", | ||
"version": "2.5.0", | ||
"description": "An extremely simple, pluggable static site generator.", | ||
@@ -35,3 +35,6 @@ "keywords": [ | ||
"index.js", | ||
"lib/**", | ||
"lib/index.js", | ||
"lib/debug.js", | ||
"lib/helpers.js", | ||
"metalsmith-migrated-plugins.js", | ||
"bin/**", | ||
@@ -48,3 +51,4 @@ "CHANGELOG.md", | ||
"test": "nyc mocha", | ||
"release": "release-it" | ||
"release": "release-it", | ||
"postinstall": "node metalsmith-migrated-plugins.js" | ||
}, | ||
@@ -57,7 +61,5 @@ "mocha": { | ||
"dependencies": { | ||
"chalk": "^4.1.2", | ||
"clone": "^2.1.2", | ||
"co-fs-extra": "^1.2.1", | ||
"commander": "^6.2.1", | ||
"cross-spawn": "^7.0.3", | ||
"debug": "^4.3.3", | ||
"gray-matter": "^4.0.3", | ||
@@ -68,4 +70,2 @@ "is-utf8": "~0.2.0", | ||
"stat-mode": "^1.0.0", | ||
"thunkify": "^2.1.2", | ||
"unyield": "0.0.1", | ||
"ware": "^1.3.0" | ||
@@ -75,17 +75,17 @@ }, | ||
"@metalsmith/drafts": "^1.1.1", | ||
"@metalsmith/markdown": "^1.4.0", | ||
"@metalsmith/markdown": "^1.5.0", | ||
"assert-dir-equal": "^1.1.0", | ||
"eslint": "^8.8.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-import": "^2.25.4", | ||
"eslint": "^8.16.0", | ||
"eslint-config-prettier": "^8.5.0", | ||
"eslint-plugin-import": "^2.26.0", | ||
"eslint-plugin-node": "^11.1.0", | ||
"mocha": "^7.2.0", | ||
"mocha": "^9.2.2", | ||
"nyc": "^15.1.0", | ||
"prettier": "^2.5.1", | ||
"release-it": "^14.12.3", | ||
"prettier": "^2.6.2", | ||
"release-it": "^15.0.0", | ||
"toml": "^3.0.0" | ||
}, | ||
"engines": { | ||
"node": ">=8.6" | ||
"node": ">=12" | ||
} | ||
} |
188
README.md
@@ -11,22 +11,14 @@ # Metalsmith | ||
In Metalsmith, all of the logic is handled by plugins. You simply chain them together. Here's what the simplest blog looks like... | ||
In Metalsmith, all of the logic is handled by plugins. You simply chain them together. | ||
Here's what the simplest blog looks like: | ||
```js | ||
Metalsmith(__dirname) | ||
.use(markdown()) | ||
.use(layouts('handlebars')) | ||
.build(function (err) { | ||
if (err) throw err | ||
console.log('Build finished!') | ||
}) | ||
``` | ||
const Metalsmith = require('metalsmith') | ||
const layouts = require('@metalsmith/layouts') | ||
const markdown = require('@metalsmith/markdown') | ||
...but what if you want to get fancier by hiding your unfinished drafts and using custom permalinks? Just add plugins... | ||
```js | ||
Metalsmith(__dirname) | ||
.use(drafts()) | ||
.use(markdown()) | ||
.use(permalinks('posts/:title')) | ||
.use(layouts('handlebars')) | ||
.use(layouts()) | ||
.build(function (err) { | ||
@@ -38,4 +30,2 @@ if (err) throw err | ||
...it's as easy as that! | ||
## Installation | ||
@@ -55,6 +45,49 @@ | ||
## Plugins | ||
## Quickstart | ||
Check out the website for a list of [plugins](https://metalsmith.io/plugins). | ||
What if you want to get fancier by hiding unfinished drafts, grouping posts in collections, and using custom permalinks? Just add plugins... | ||
```js | ||
const Metalsmith = require('metalsmith') | ||
const collections = require('@metalsmith/collections') | ||
const layouts = require('@metalsmith/layouts') | ||
const markdown = require('@metalsmith/markdown') | ||
const permalinks = require('@metalsmith/permalinks') | ||
Metalsmith(__dirname) | ||
.source('./src') | ||
.destination('./build') | ||
.clean(true) | ||
.frontmatter({ | ||
excerpt: true | ||
}) | ||
.env({ | ||
NAME: process.env.NODE_ENV, | ||
DEBUG: '@metalsmith/*', | ||
DEBUG_LOG: 'metalsmith.log' | ||
}) | ||
.metadata({ | ||
sitename: 'My Static Site & Blog', | ||
siteurl: 'https://example.com/', | ||
description: "It's about saying »Hello« to the world.", | ||
generatorname: 'Metalsmith', | ||
generatorurl: 'https://metalsmith.io/' | ||
}) | ||
.use( | ||
collections({ | ||
posts: 'posts/*.md' | ||
}) | ||
) | ||
.use(markdown()) | ||
.use( | ||
permalinks({ | ||
relative: false | ||
}) | ||
) | ||
.use(layouts()) | ||
.build(function (err) { | ||
if (err) throw err | ||
}) | ||
``` | ||
## How does it work? | ||
@@ -70,3 +103,3 @@ | ||
```md | ||
``` | ||
--- | ||
@@ -87,3 +120,6 @@ title: A Catchy Title | ||
date: <Date >, | ||
contents: <Buffer 7a 66 7a 67...> | ||
contents: <Buffer 7a 66 7a 67...>, | ||
stats: { | ||
... | ||
} | ||
} | ||
@@ -93,44 +129,25 @@ } | ||
...which any of the plugins can then manipulate however they want. And writing the plugins is incredibly simple, just take a look at the [example drafts plugin](examples/drafts-plugin/index.js). | ||
...which any of the plugins can then manipulate however they want. Writing plugins is incredibly simple, just take a look at the [example drafts plugin](examples/drafts-plugin/index.js). | ||
Of course they can get a lot more complicated too. That's what makes Metalsmith powerful; the plugins can do anything you want! | ||
## The secret... | ||
## Plugins | ||
We keep referring to Metalsmith as a "static site generator", but it's a lot more than that. Since everything is a plugin, the core library is actually just an abstraction for manipulating a directory of files. | ||
A [Metalsmith plugin](https://metalsmith.io/api/#Plugin) is a function that is passed the file list, the metalsmith instance, and a done callback. | ||
It is often wrapped in a plugin initializer that accepts configuration options. | ||
Which means you could just as easily use it to make... | ||
Check out the official plugin registry at: https://metalsmith.io/plugins. | ||
Find all the core plugins at: https://github.com/search?q=org%3Ametalsmith+metalsmith-plugin | ||
See [the draft plugin](examples/drafts-plugin) for a simple plugin example. | ||
- [A simple project scaffolder.](examples/project-scaffolder) | ||
- [A simple build tool for Sass files.](examples/build-tool) | ||
- [A simple static site generator.](examples/static-site) | ||
- [A Jekyll-like static site generator.](examples/jekyll) | ||
- [A Wintersmith-like static site generator.](examples/wintersmith) | ||
## API | ||
## Resources | ||
Check out the full API reference at: https://metalsmith.io/api. | ||
- [Gitter community chat](https://gitter.im/metalsmith/community) | ||
- [Getting to Know Metalsmith](http://robinthrift.com/post/getting-to-know-metalsmith/) - a great series about how to use Metalsmith for your static site. | ||
- [Building a Blog With Metalsmith](https://azurelogic.com/posts/building-a-blog-with-metalsmith/) - a blog post about how to create a basic blog with Metalsmith. Check out the related [video of the talk](https://www.youtube.com/watch?v=cAq5_5Yy7Tg) too! | ||
- [Awesome Metalsmith](https://github.com/lambtron/awesome-metalsmith) - great collection of resources, examples, and tutorials | ||
## CLI | ||
In addition to a simple [Javascript API](#api), the Metalsmith CLI can read configuration from a `metalsmith.json` file, so that you can build static-site generators similar to [Jekyll](http://jekyllrb.com) or [Wintersmith](http://wintersmith.io) easily. The example blog above would be configured like this: | ||
In addition to a simple [Javascript API](#api), the Metalsmith CLI can read configuration from a `metalsmith.json` file, so that you can build static-site generators similar to [Jekyll](https://jekyllrb.com) or [Hexo](https://hexo.io) easily. The example blog above would be configured like this: | ||
```json | ||
{ | ||
"source": "src", | ||
"destination": "build", | ||
"plugins": [ | ||
{ "@metalsmith/drafts": true }, | ||
{ "@metalsmith/markdown": true }, | ||
{ "@metalsmith/permalinks": "posts/:title" }, | ||
{ "@metalsmith/layouts": {} } | ||
] | ||
} | ||
``` | ||
`metalsmith.json` | ||
You can specify your plugins as either an object or array. Using an array would allow you to specify use of the same plugin multiple times. The above example is then defined as so: | ||
```json | ||
@@ -140,7 +157,16 @@ { | ||
"destination": "build", | ||
"clean": true, | ||
"metadata": { | ||
"sitename": "My Static Site & Blog", | ||
"siteurl": "https://example.com/", | ||
"description": "It's about saying »Hello« to the world.", | ||
"generatorname": "Metalsmith", | ||
"generatorurl": "https://metalsmith.io/" | ||
}, | ||
"plugins": [ | ||
{ "@metalsmith/drafts": true }, | ||
{ "@metalsmith/collections": { "posts": "posts/*.md" } }, | ||
{ "@metalsmith/markdown": true }, | ||
{ "@metalsmith/permalinks": "posts/:title" }, | ||
{ "metalsmith-layouts": true } | ||
{ "@metalsmith/layouts": true } | ||
] | ||
@@ -150,3 +176,3 @@ } | ||
And then just install `metalsmith` and the plugins and run the metalsmith CLI... | ||
Then run: | ||
@@ -160,9 +186,8 @@ ```bash | ||
Options recognised by `metalsmith.json` are `source`, `destination`, `concurrency`, `metadata`, `clean` and `frontmatter` - See "_API_" section below for usage. | ||
Options recognised by `metalsmith.json` are `source`, `destination`, `concurrency`, `metadata`, `clean` and `frontmatter`. | ||
Checkout the [static site](examples/static-site), [Jekyll](examples/jekyll) examples to see the CLI in action. | ||
Checkout the [static site](examples/static-site), [Jekyll](examples/jekyll) or [Wintersmith](examples/wintersmith) examples to see the CLI in action. | ||
### Local plugins | ||
If you want to use a custom plugin, but feel like it's too domain-specific to | ||
be published to the world, you can include plugins as local npm modules: | ||
(simply use a relative path from your root directory) | ||
If you want to use a custom plugin, but feel like it's too domain-specific to be published to the world, you can include plugins as local npm modules: (simply use a relative path from your root directory) | ||
@@ -175,40 +200,31 @@ ```json | ||
## API | ||
## The secret... | ||
See [API reference at metalsmith.io](https://metalsmith.io/api) | ||
We often refer to Metalsmith as a "static site generator", but it's a lot more than that. Since everything is a plugin, the core library is just an abstraction for manipulating a directory of files. | ||
## Metadata API | ||
Which means you could just as easily use it to make... | ||
Add metadata to your files to access these build features. By default, Metalsmith uses a few different metadata fields: | ||
- [A project scaffolder.](examples/project-scaffolder) | ||
- [A build tool for Sass files.](examples/build-tool) | ||
- [A simple static site generator.](examples/static-site) | ||
- [A Jekyll-like static site generator.](examples/jekyll) | ||
- `contents` - The body content of the file, not including any [YAML frontmatter](https://middlemanapp.com/basics/frontmatter/). | ||
- `mode` - The numeric version of the [file's mode](http://en.wikipedia.org/wiki/Modes_%28Unix%29). | ||
## Resources | ||
You can add your own metadata in two ways: | ||
- [Gitter community chat](https://gitter.im/metalsmith/community) for chat, questions | ||
- [Twitter announcements](https://twitter.com/@metalsmithio) and the [metalsmith.io news page](https://metalsmith.io/news) for updates | ||
- [Awesome Metalsmith](https://github.com/metalsmith/awesome-metalsmith) - great collection of resources, examples, and tutorials | ||
- [emmer.dev on metalsmith](https://emmer.dev/blog/tag/metalsmith/) - A good collection of various how to's for metalsmith | ||
- [glinka.co on metalsmith](https://www.glinka.co/blog/) - Another great collection of advanced approaches for developing metalsmith | ||
- [Getting to Know Metalsmith](http://robinthrift.com/post/getting-to-know-metalsmith/) - a great series about how to use Metalsmith for your static site. | ||
- Using [YAML frontmatter](https://middlemanapp.com/basics/frontmatter/) at the top of any file. | ||
- Enabling [a plugin](https://github.com/metalsmith/metalsmith/blob/master/README.md#plugins) that adds metadata programmatically. | ||
## Troubleshooting | ||
#### mode | ||
Use [debug](https://github.com/debug-js/debug/) to debug your build with `export DEBUG=metalsmith-*,@metalsmith/*` (Linux) or `set DEBUG=metalsmith-*,@metalsmith/*` for Windows. | ||
Use the excellent [metalsmith-debug-ui plugin](https://github.com/leviwheatcroft/metalsmith-debug-ui) to get a snapshot UI for every build step. | ||
Set the mode of the file. For example, a `cleanup.sh` file with the contents | ||
```md | ||
--- | ||
mode: 0764 | ||
--- | ||
#!/bin/sh | ||
rm -rf . | ||
``` | ||
would be built with mode `-rwxrw-r--`, i.e. user-executable. | ||
## Troubleshooting | ||
### Node Version Requirements | ||
Metalsmith 3.0.0 will support NodeJS versions 12 and higher. | ||
Metalsmith 2.4.0 supports NodeJS versions 8 and higher. | ||
Metalsmith 2.5.x supports NodeJS versions 12 and higher. | ||
Metalsmith 2.4.x supports NodeJS versions 8 and higher. | ||
Metalsmith 2.3.0 and below support NodeJS versions all the way back to 0.12. | ||
@@ -215,0 +231,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
61387
9
12
948
238
1
3
+ Addeddebug@^4.3.3
+ Addeddebug@4.4.0(transitive)
+ Addedms@2.1.3(transitive)
- Removedchalk@^4.1.2
- Removedclone@^2.1.2
- Removedco-fs-extra@^1.2.1
- Removedthunkify@^2.1.2
- Removedunyield@0.0.1
- Removedansi-styles@4.3.0(transitive)
- Removedchalk@4.1.2(transitive)
- Removedclone@2.1.2(transitive)
- Removedco-from-stream@0.0.0(transitive)
- Removedco-fs-extra@1.2.1(transitive)
- Removedco-read@0.0.1(transitive)
- Removedcolor-convert@2.0.1(transitive)
- Removedcolor-name@1.1.4(transitive)
- Removedenable@1.3.2(transitive)
- Removedfs-extra@0.26.7(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedhas-flag@4.0.0(transitive)
- Removedjsonfile@2.4.0(transitive)
- Removedklaw@1.3.1(transitive)
- Removedrimraf@2.7.1(transitive)
- Removedsupports-color@7.2.0(transitive)
- Removedthunkify@2.1.2(transitive)
- Removedthunkify-wrap@1.0.4(transitive)
- Removedunyield@0.0.1(transitive)