An extremely simple, pluggable static site generator for NodeJS
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Plugin management and execution system for extending Metalsmith functionality with custom transformations. All Metalsmith logic is handled through plugins that manipulate files in a middleware-style pattern.
Add plugins to the Metalsmith processing pipeline. Plugins are executed in the order they are added.
/**
* Add a plugin function to the processing stack
* @param plugin - Plugin function or array of plugin functions
* @returns Metalsmith instance for chaining
*/
use(plugin: Plugin | Plugin[]): Metalsmith;
/**
* Metalsmith plugin function signature
* @param files - Object containing all files being processed
* @param metalsmith - The Metalsmith instance
* @param callback - Callback to signal completion (for async plugins)
*/
type Plugin = (
files: Files,
metalsmith: Metalsmith,
callback: (error?: Error) => void
) => void | Promise<void>;Usage Examples:
import Metalsmith from "metalsmith";
import markdown from "@metalsmith/markdown";
import layouts from "@metalsmith/layouts";
// Add plugins in sequence
metalsmith
.use(markdown())
.use(layouts({
pattern: "**/*.html"
}));
// Add multiple plugins at once
metalsmith.use([
markdown(),
layouts({ pattern: "**/*.html" })
]);Plugins have full access to the Metalsmith instance and can read configuration, metadata, and other settings.
/**
* Plugin can access all Metalsmith methods and properties
*/
function examplePlugin(files, metalsmith, done) {
// Access configuration
const srcDir = metalsmith.source();
const destDir = metalsmith.destination();
const metadata = metalsmith.metadata();
// Access environment variables
const debugMode = metalsmith.env('DEBUG');
// Manipulate files
Object.keys(files).forEach(filepath => {
const file = files[filepath];
// Transform file...
});
// Signal completion
done();
}Metalsmith supports both synchronous and asynchronous plugins.
Synchronous Plugin:
function syncPlugin(files, metalsmith) {
// No callback needed for sync plugins
Object.keys(files).forEach(filepath => {
// Transform files synchronously
files[filepath].processed = true;
});
}Asynchronous Plugin with Callback:
function asyncPlugin(files, metalsmith, done) {
// Perform async operations
setTimeout(() => {
Object.keys(files).forEach(filepath => {
files[filepath].delayed = true;
});
done(); // Must call done() when finished
}, 100);
}Promise-based Plugin:
async function promisePlugin(files, metalsmith) {
// Return a promise or use async/await
await new Promise(resolve => setTimeout(resolve, 100));
Object.keys(files).forEach(filepath => {
files[filepath].promised = true;
});
}Common patterns for configurable plugins that accept options.
// Plugin factory pattern
function configurablePlugin(options = {}) {
return function plugin(files, metalsmith, done) {
const settings = {
pattern: '**/*',
...options
};
// Use settings to configure behavior
Object.keys(files)
.filter(filepath => metalsmith.match(settings.pattern, [filepath]).length)
.forEach(filepath => {
// Process matching files
});
done();
};
}
// Usage
metalsmith.use(configurablePlugin({
pattern: '**/*.md',
customOption: 'value'
}));Plugins work with file objects that contain contents, metadata, and filesystem information.
interface File {
/** File contents as Buffer */
contents: Buffer;
/** Filesystem stats object */
stats?: import('fs').Stats;
/** File permission mode */
mode?: string;
/** Front-matter and custom properties */
[key: string]: any;
}
interface Files {
/** Mapping of file paths to File objects */
[filepath: string]: File;
}File Manipulation Examples:
function fileManipulationPlugin(files, metalsmith, done) {
Object.keys(files).forEach(filepath => {
const file = files[filepath];
// Read file contents
const content = file.contents.toString();
// Access front-matter data
const title = file.title;
const date = file.date;
// Modify contents
file.contents = Buffer.from(content.toUpperCase());
// Add metadata
file.processed = true;
file.processedAt = new Date();
// Change file path (rename/move)
if (filepath.endsWith('.md')) {
const newPath = filepath.replace('.md', '.html');
files[newPath] = file;
delete files[filepath];
}
});
done();
}Proper error handling patterns for plugins.
function errorHandlingPlugin(files, metalsmith, done) {
try {
Object.keys(files).forEach(filepath => {
// Operations that might fail
if (someCondition) {
throw new Error(`Processing failed for ${filepath}`);
}
});
done(); // Success
} catch (error) {
done(error); // Pass error to Metalsmith
}
}
// Promise-based error handling
async function promiseErrorPlugin(files, metalsmith) {
for (const filepath of Object.keys(files)) {
try {
await someAsyncOperation(files[filepath]);
} catch (error) {
throw new Error(`Failed to process ${filepath}: ${error.message}`);
}
}
}Best practices for developing Metalsmith plugins:
function wellBehavedPlugin(options = {}) {
// Validate options
if (options.required && typeof options.required !== 'string') {
throw new TypeError('required option must be a string');
}
return function plugin(files, metalsmith, done) {
// Create debug logger
const debug = metalsmith.debug('my-plugin');
debug('Processing %d files', Object.keys(files).length);
// Access metalsmith configuration
const metadata = metalsmith.metadata();
const srcDir = metalsmith.source();
try {
Object.keys(files).forEach(filepath => {
debug('Processing file: %s', filepath);
const file = files[filepath];
// Plugin logic here
debug('Finished processing: %s', filepath);
});
debug('Plugin completed successfully');
done();
} catch (error) {
debug('Plugin failed: %s', error.message);
done(error);
}
};
}