@metalsmith/in-place
Version:
A metalsmith plugin for in-place templating
294 lines (265 loc) • 9.75 kB
JavaScript
;
var isUtf8 = require('is-utf8');
var path = require('path');
var jstransformer = require('jstransformer');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return n;
}
var isUtf8__default = /*#__PURE__*/_interopDefaultLegacy(isUtf8);
var jstransformer__default = /*#__PURE__*/_interopDefaultLegacy(jstransformer);
/**
* Parse a filepath into dirname, base & extensions
* @param {string} filename
*/
function parseFilepath(filename) {
const isNested = filename.includes(path.sep);
const dir = isNested ? path.dirname(filename) : '';
const [base, ...extensions] = path.basename(filename).split('.');
return {
dirname: dir,
base,
extensions
};
}
/**
* @param {string} filename
* @param {import('./index').Options} opts
* @returns {string}
*/
function handleExtname(filename, opts) {
const {
dirname,
base,
extensions
} = parseFilepath(filename);
const extname = opts.extname && opts.extname.slice(1);
// decouples file extension chaining order from transformer usage order
for (let i = extensions.length; i--;) {
if (opts.transform.inputFormats.includes(extensions[i])) {
extensions.splice(i, 1);
break;
}
}
const isLast = !extensions.length;
if (isLast && extname) extensions.push(extname);
return [path.join(dirname, base), ...extensions].join('.');
}
/**
* Get a transformer by name ("jstransformer-ejs"), shortened name ("ejs") or filesystem path
* @param {string|JsTransformer} namePathOrTransformer
* @param {import('metalsmith').Debugger} debug
* @returns {Promise<JsTransformer>}
*/
function getTransformer(namePathOrTransformer, debug) {
let transform = null;
const t = namePathOrTransformer;
const tName = t;
const tPath = t;
// let the jstransformer constructor throw errors
if (typeof t !== 'string') {
transform = Promise.resolve(t);
} else {
if (path.isAbsolute(tPath) || tPath.startsWith('.') || tName.startsWith('jstransformer-')) {
debug('Importing transformer: %s', tPath);
transform = (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(tPath).then(t => t.default);
} else {
debug('Importing transformer: jstransformer-%s', tName);
// suppose a shorthand where the jstransformer- prefix is omitted, more likely
transform = (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(`jstransformer-${tName}`).then(t => t.default).catch(() => {
// else fall back to trying to import the name
debug.warn('"jstransformer-%s" not found, trying "%s" instead', tName, tName);
return (function (t) { return Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(t)); }); })(tName).then(t => t.default);
});
}
}
return transform.then(t => {
return jstransformer__default["default"](t);
});
}
/**
* @callback Render
* @param {string} source
* @param {Object} options
* @param {Object} locals
* @returns {string}
*/
/**
* @callback RenderAsync
* @param {string} source
* @param {Object} options
* @param {Object} locals
* @param {Function} callback
* @returns {Promise<string>}
*/
/**
* @callback Compile
* @param {string} source
* @param {Object} options
* @returns {string}
*/
/**
* @callback CompileAsync
* @param {string} source
* @param {Object} options
* @param {Function} callback
* @returns {Promise<string>}
*/
/**
* @typedef {Object} JsTransformer
* @property {string} name
* @property {string[]} inputFormats
* @property {string} outputFormat
* @property {Render} [render]
* @property {RenderAsync} [renderAsync]
* @property {Compile} [compile]
* @property {CompileAsync} [compileAsync]
*/
/* c8 ignore start */
let debug = () => {
throw new Error('uninstantiated debug');
};
/* c8 ignore end */
async function render({
filename,
files,
metalsmith,
options,
transform
}) {
const file = files[filename];
const engineOptions = Object.assign({}, options.engineOptions);
if (options.engineOptions.filename) {
Object.assign(engineOptions, {
// set the filename in options for jstransformers requiring it (like Pug)
filename: metalsmith.path(metalsmith.source(), filename)
});
}
const metadata = metalsmith.metadata();
debug(`rendering ${filename}`);
const locals = Object.assign({}, metadata, file);
const contents = file.contents.toString();
return transform.renderAsync(contents, engineOptions, locals).then(rendered => {
const newName = handleExtname(filename, {
...options,
transform
});
debug('Done rendering %s', filename);
debug('Renaming "%s" to "%s"', filename, newName);
if (newName !== filename) {
delete files[filename];
files[newName] = file;
}
files[newName].contents = Buffer.from(rendered.body);
}).catch(err => {
err.message = `${filename}: ${err.message}`;
throw err;
});
}
/**
* Validate, checks whether a file should be processed
*/
function validate({
filename,
files,
transform
}) {
const {
extensions
} = parseFilepath(filename);
debug(`validating ${filename} %O %O`, extensions, transform.inputFormats);
// IF the transform has inputFormats defined, invalidate the file if it has no matching extname
if (transform.inputFormats && !extensions.some(fmt => transform.inputFormats.includes(fmt))) {
debug.warn('Validation failed for file "%s", transformer %s supports extensions %s.', filename, transform.name, transform.inputFormats.map(i => `.${i}`).join(', '));
}
// Files that are not utf8 are ignored
if (!isUtf8__default["default"](files[filename].contents)) {
debug.warn(`Validation failed, %s is not utf-8`, filename);
return false;
}
return true;
}
/**
* @typedef {Object} Options
* @property {string|JsTransformer} transform Jstransformer to run: name of a node module or local JS module path (starting with `.`) whose default export is a jstransformer. As a shorthand for existing transformers you can remove the `jstransformer-` prefix: `marked` will be understood as `jstransformer-marked`. Or an actual jstransformer; an object with `name`, `inputFormats`,`outputFormat`, and at least one of the render methods `render`, `renderAsync`, `compile` or `compileAsync` described in the [jstransformer API docs](https://github.com/jstransformers/jstransformer#api)
* @property {string} [pattern='**\/*.<transform.inputFormats>'] (*optional*) One or more paths or glob patterns to limit the scope of the transform. Defaults to `'**\/*.<transform.inputFormats>*'`
* @property {Object} [engineOptions={}] (*optional*) Pass options to the jstransformer templating engine that's rendering your files. The default is `{}`
* @property {string} [extname] (*optional*) Pass `''` to remove the extension or `'.<extname>'` to keep or rename it. Defaults to `transform.outputFormat`
**/
/**
* Set default options based on jstransformer `transform`
* @param {JsTransformer} transform
* @returns {Options}
*/
function normalizeOptions(transform) {
const extMatch = transform.inputFormats.length === 1 ? transform.inputFormats[0] : `{${transform.inputFormats.join(',')}}`;
return {
pattern: `**/*.${extMatch}*`,
extname: `.${transform.outputFormat}`,
engineOptions: {}
};
}
/**
* A metalsmith plugin for in-place templating
* @param {Options} options
* @returns {import('metalsmith').Plugin}
*/
function inPlace(options = {}) {
let transform;
return async function inPlace(files, metalsmith, done) {
debug = metalsmith.debug('@metalsmith/in-place');
// Check whether the pattern option is valid
if (options.pattern && !(typeof options.pattern === 'string' || Array.isArray(options.pattern))) {
return done(new Error('invalid pattern, the pattern option should be a string or array of strings. See https://www.npmjs.com/package/@metalsmith/in-place#pattern'));
}
// skip resolving the transform option on repeat runs
if (!transform) {
try {
transform = await getTransformer(options.transform, debug);
} catch (err) {
// pass through jstransformer & Node import resolution errors
return done(err);
}
}
options = Object.assign(normalizeOptions(transform), options);
debug('Running with options %O', options);
const matchedFiles = metalsmith.match(options.pattern);
// Filter files by validity, pass basename to avoid dots in folder path
const validFiles = matchedFiles.filter(filename => validate({
filename,
files,
transform
}));
// Let the user know when there are no files to process
if (validFiles.length === 0) {
debug.warn('No valid files to process.');
return done();
} else {
debug('Rendering %s files', validFiles.length);
}
// Map all files that should be processed to an array of promises and call done when finished
return Promise.all(validFiles.map(filename => render({
filename,
files,
metalsmith,
options,
transform
}))).then(() => done()).catch(error => done(error));
};
}
module.exports = inPlace;
//# sourceMappingURL=index.cjs.map