Skip to content

Commit

Permalink
BREAKING: solves #179, loosen dependency on jstransformers
Browse files Browse the repository at this point in the history
Adds required transform option, as aligned with metalsmith/in-place
  • Loading branch information
webketje committed Feb 14, 2024
1 parent 6d4dbd8 commit 07b2683
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 288 deletions.
30 changes: 0 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
"test": "c8 mocha"
},
"dependencies": {
"inputformat-to-jstransformer": "^1.4.0",
"is-utf8": "^0.2.1",
"jstransformer": "^1.0.0"
},
Expand Down
20 changes: 0 additions & 20 deletions src/get-transformer.js

This file was deleted.

145 changes: 108 additions & 37 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
import path from 'path'
import isUtf8 from 'is-utf8'
import getTransformer from './get-transformer.js'
import { getTransformer } from './utils.js'

/* c8 ignore next 3 */
let debug = () => {
throw new Error('uninstantiated debug')
}

/**
* @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]
*/

/**
* @typedef {Object} Options `@metalsmith/layouts` options
* @property {string} [default] A default layout to apply to files, eg `default.njk`.
Expand All @@ -20,38 +63,54 @@ let debug = () => {
* Resolves layouts, in the following order:
* 1. Layouts in the frontmatter
* 2. Skips file if layout: false in frontmatter
* 3. Default layout in the settings
* 3. Default layout in the options
*/

function getLayout({ file, settings }) {
function getLayout({ file, options }) {
if (file.layout || file.layout === false) {
return file.layout
}

return settings.default
return options.default
}

/**
* Set default options based on jstransformer `transform`
* @param {JsTransformer} transform
* @returns {Options}
*/
function normalizeOptions(options, transform) {
return {
default: null,
pattern: '**',
directory: 'layouts',
suppressNoFilesError: false,
engineOptions: {},
...options,
transform
}
}

/**
* Engine, renders file with the appropriate layout
*/

function render({ filename, files, metadata, settings, metalsmith }) {
function render({ filename, files, metalsmith, options, transform }) {
const file = files[filename]
const layout = getLayout({ file, settings })
const extension = layout.split('.').pop()
const layout = getLayout({ file, options })

debug.info('Rendering "%s" with layout "%s"', filename, layout)

const metadata = metalsmith.metadata()
// Stringify file contents
const contents = file.contents.toString()

const transform = getTransformer(extension)
const locals = { ...metadata, ...file, contents }
const layoutPath = path.join(metalsmith.path(settings.directory), layout)
const layoutPath = path.join(metalsmith.path(options.directory), layout)

// Transform the contents
return transform
.renderFileAsync(layoutPath, settings.engineOptions, locals)
.renderFileAsync(layoutPath, options.engineOptions, locals)
.then((rendered) => {
// Update file with results
file.contents = Buffer.from(rendered.body)
Expand All @@ -68,9 +127,9 @@ function render({ filename, files, metadata, settings, metalsmith }) {
* Validate, checks whether a file should be processed
*/

function validate({ filename, files, settings }) {
function validate({ filename, files, options }) {
const file = files[filename]
const layout = getLayout({ file, settings })
const layout = getLayout({ file, options })

debug.info(`Validating ${filename}`)

Expand All @@ -92,15 +151,17 @@ function validate({ filename, files, settings }) {
return false
}

// Files without an applicable jstransformer are ignored
// Layouts with an extension mismatch are ignored
const extension = layout.split('.').pop()
const transformer = getTransformer(extension)
let inputFormats = options.transform.inputFormats
if (!Array.isArray(inputFormats)) inputFormats = [options.transform.inputFormats]

if (!transformer) {
debug.warn('Validation failed, no jstransformer found for layout for "%s"', filename)
if (!inputFormats.includes(extension)) {
debug.warn('Validation failed, layout for "%s" does not have an extension', filename)
return false
}

return transformer
return true
}

/**
Expand All @@ -109,41 +170,51 @@ function validate({ filename, files, settings }) {
* @returns {import('metalsmith').Plugin}
*/
function layouts(options) {
return function layouts(files, metalsmith, done) {
let transform

return async function layouts(files, metalsmith, done) {
debug = metalsmith.debug('@metalsmith/layouts')

const metadata = metalsmith.metadata()
const defaults = {
pattern: '**',
directory: 'layouts',
engineOptions: {},
suppressNoFilesError: false
if (!options.transform) {
done(new Error('"transform" option is required'))
return
}
const settings = { ...defaults, ...options }

debug('Running with options: %o', settings)

// Check whether the pattern option is valid
if (!(typeof settings.pattern === 'string' || Array.isArray(settings.pattern))) {
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/layouts#pattern'
)
new Error('invalid pattern, the pattern option should be a string or array of strings.')
)
}

// Filter files by the pattern
const matchedFiles = metalsmith.match(settings.pattern, Object.keys(files))
// 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 = normalizeOptions(options, transform)

debug('Running with options %O', options)

const matchedFiles = metalsmith.match(options.pattern, Object.keys(files))

// Filter files by validity
const validFiles = matchedFiles.filter((filename) => validate({ filename, files, settings }))
// Filter files by validity, pass basename to avoid dots in folder path
const validFiles = matchedFiles.filter((filename) => validate({ filename, files, options }))

// Let the user know when there are no files to process, unless the check is suppressed
if (validFiles.length === 0) {
const message =
'no files to process. See https://www.npmjs.com/package/@metalsmith/layouts#suppressnofileserror'

if (settings.suppressNoFilesError) {
if (options.suppressNoFilesError) {
debug.error(message)
return done()
}
Expand All @@ -153,7 +224,7 @@ function layouts(options) {

// 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, metadata, settings, metalsmith }))
validFiles.map((filename) => render({ filename, files, metalsmith, options, transform }))
)
.then(() => {
debug('Finished rendering %s file%s', validFiles.length, validFiles.length > 1 ? 's' : '')
Expand Down
36 changes: 36 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isAbsolute } from 'path'
import jstransformer from 'jstransformer'

/**
* Get a transformer by name ("jstransformer-ejs"), shortened name ("ejs") or filesystem path
* @param {string|import('./index').JsTransformer} namePathOrTransformer
* @param {import('metalsmith').Debugger} debug
* @returns {Promise<JsTransformer>}
*/
export 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 (isAbsolute(tPath) || tPath.startsWith('.') || tName.startsWith('jstransformer-')) {
debug('Importing transformer: %s', tPath)
transform = import(tPath).then((t) => t.default)
} else {
debug('Importing transformer: jstransformer-%s', tName)
// suppose a shorthand where the jstransformer- prefix is omitted, more likely
transform = import(`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 import(tName).then((t) => t.default)
})
}
}
return transform.then(jstransformer)
}
Loading

0 comments on commit 07b2683

Please sign in to comment.