UNPKG

@metalsmith/in-place

Version:

A metalsmith plugin for in-place templating

294 lines (265 loc) 9.75 kB
'use strict'; 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