@fwdslsh/unify
Version:
A lightweight, framework-free static site generator with Bun native APIs
215 lines (180 loc) • 6.54 kB
JavaScript
import path from 'path';
/**
* File classification system for convention-based architecture
* Determines file types based on naming conventions and location
*/
export class FileClassifier {
/**
* Determine if a file is a page (should be emitted to output)
* @param {string} filePath - Absolute path to the file
* @param {string} sourceRoot - Absolute path to source directory
* @returns {boolean} True if file is a page
*/
isPage(filePath, sourceRoot) {
const relativePath = path.relative(sourceRoot, filePath);
const fileName = path.basename(filePath);
const extension = path.extname(filePath).toLowerCase();
// Must be HTML or Markdown file
if (!['.html', '.htm', '.md'].includes(extension)) {
return false;
}
// Files starting with underscore are partials, not pages
if (fileName.startsWith('_')) {
return false;
}
// Files in _includes or other underscore directories are not pages
const pathParts = relativePath.split(path.sep);
for (const part of pathParts) {
if (part.startsWith('_')) {
return false;
}
}
// PATCH: Always treat markdown files as pages unless in underscore-prefixed dir
if (extension === '.md') {
return true;
}
return true;
}
/**
* Determine if a file is a partial (non-emitting)
* @param {string} filePath - Absolute path to the file
* @param {string} sourceRoot - Absolute path to source directory
* @returns {boolean} True if file is a partial
*/
isPartial(filePath, sourceRoot) {
const fileName = path.basename(filePath);
const extension = path.extname(filePath).toLowerCase();
// Must be HTML file
if (!['.html', '.htm'].includes(extension)) {
return false;
}
// Files starting with underscore are partials
if (fileName.startsWith('_')) {
return true;
}
// Check if in any directory that should be treated as partials
const relativePath = path.relative(sourceRoot, filePath);
const pathParts = relativePath.split(path.sep);
// Files in underscore-prefixed directories are partials
if (pathParts.some(part => part.startsWith('_'))) {
return true;
}
// Also check for common standard directory names that should be treated as partials
const commonPartialDirs = [
'layouts', '.layouts',
'includes', 'partials', 'templates',
'site_layouts' // Support custom naming conventions
];
for (const dirName of commonPartialDirs) {
if (pathParts.includes(dirName)) {
return true;
}
}
return false;
}
/**
* Determine if a file is a layout
* @param {string} filePath - Absolute path to the file
* @param {string} sourceRoot - Absolute path to source directory
* @returns {boolean} True if file is a layout
*/
isLayout(filePath, sourceRoot) {
const fileName = path.basename(filePath);
const extension = path.extname(filePath).toLowerCase();
// Must be HTML file
if (!['.html', '.htm'].includes(extension)) {
return false;
}
// Check if filename matches layout pattern
if (this.isLayoutFileName(fileName)) {
return true;
}
// Check for fallback layout in _includes
const relativePath = path.relative(sourceRoot, filePath);
if (relativePath === path.join('_includes', '_layout.html') ||
relativePath === path.join('_includes', '_layout.htm')) {
return true;
}
return false;
}
/**
* Check if a filename matches the layout naming convention
* @param {string} fileName - Name of the file to check
* @returns {boolean} True if matches layout pattern
*/
isLayoutFileName(fileName) {
// Must start with underscore
if (!fileName.startsWith('_')) {
return false;
}
// Must end with layout.html or layout.htm
if (fileName.endsWith('layout.html') || fileName.endsWith('layout.htm')) {
return true;
}
// Also support the basic _layout.html and _layout.htm patterns
if (fileName === '_layout.html' || fileName === '_layout.htm') {
return true;
}
return false;
}
/**
* Check if file should be emitted to output directory
* @param {string} filePath - Absolute path to the file
* @param {string} sourceRoot - Absolute path to source directory
* @returns {boolean} True if file should be emitted
*/
shouldEmit(filePath, sourceRoot) {
const fileName = path.basename(filePath);
const extension = path.extname(filePath).toLowerCase();
// Pages are always emitted
if (this.isPage(filePath, sourceRoot)) {
return true;
}
// Partials and layouts are never emitted
if (this.isPartial(filePath, sourceRoot) || this.isLayout(filePath, sourceRoot)) {
return false;
}
// For other files (assets), check underscore convention
// Files starting with underscore are non-emitting unless explicitly referenced
if (fileName.startsWith('_')) {
return false; // Will be copied only if referenced
}
// Files in underscore directories are non-emitting unless explicitly referenced
const relativePath = path.relative(sourceRoot, filePath);
const pathParts = relativePath.split(path.sep);
if (pathParts.some(part => part.startsWith('_'))) {
return false; // Will be copied only if referenced
}
// Regular assets are emitted if they're static files
return true;
}
/**
* Get file type classification
* @param {string} filePath - Absolute path to the file
* @param {string} sourceRoot - Absolute path to source directory
* @returns {string} File type: 'page', 'partial', 'layout', 'asset'
*/
getFileType(filePath, sourceRoot) {
if (this.isLayout(filePath, sourceRoot)) {
return 'layout';
}
if (this.isPage(filePath, sourceRoot)) {
return 'page';
}
if (this.isPartial(filePath, sourceRoot)) {
return 'partial';
}
return 'asset';
}
/**
* Check if a directory is non-emitting (underscore convention)
* @param {string} dirPath - Directory path relative to source
* @returns {boolean} True if directory should not be emitted
*/
isNonEmittingDirectory(dirPath) {
const pathParts = dirPath.split(path.sep);
return pathParts.some(part => part.startsWith('_'));
}
}
// Export singleton instance
export const fileClassifier = new FileClassifier();