@mdfriday/foundry
Version:
The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.
344 lines • 13.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Resources = void 0;
const resource_1 = require("./resource");
const resources_1 = require("../../../domain/resources");
const minifier_1 = require("./minifier");
const integrity_1 = require("./integrity");
const template_1 = require("./template");
const publisher_1 = require("./publisher");
const type_1 = require("../../../../pkg/media/type");
const path = __importStar(require("path"));
const crypto_1 = require("crypto");
const log_1 = require("../../../../pkg/log");
const log = (0, log_1.getDomainLogger)('resources', { component: 'resources' });
/**
* Resources aggregation root - the main entry point for all resource operations
* Implements the four main operations: GetResource, ExecuteAsTemplate, Minify, Fingerprint
*
* This is the main aggregation root for the Resources domain following DDD principles.
* All resource operations should go through this class.
*/
class Resources {
constructor(workspace) {
this.cache = new Map();
this.templateClient = null;
this.workspace = workspace;
this.fsSvc = workspace;
this.urlSvc = workspace;
this.templateSvc = workspace;
this.publisher = new publisher_1.Publisher(workspace.publishFs(), workspace);
this.minifierClient = new minifier_1.MinifierClient();
this.integrityClient = new integrity_1.IntegrityClient();
this.templateClient = new template_1.TemplateClient(this.templateSvc);
}
setTemplateSvc(templateSvc) {
this.templateSvc = templateSvc;
this.templateClient = new template_1.TemplateClient(templateSvc);
}
/**
* GetResource - First primary operation: get a resource by pathname
* Supports caching for performance optimization
*/
async getResource(pathname) {
const cleanPath = path.posix.normalize(pathname);
const cacheKey = `${cleanPath}__get`;
// Check cache first
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey) || null;
}
try {
// Check if file exists in assets filesystem
const assetsFs = this.workspace.assetsFs();
const stats = await assetsFs.stat(cleanPath);
if (!stats) {
return null;
}
// Create opener function
const opener = async () => {
const file = await assetsFs.open(cleanPath);
return await this.createReadSeekCloser(file);
};
const resource = await this.buildResource(cleanPath, opener);
// Cache the result
if (resource) {
this.cache.set(cacheKey, resource);
}
return resource;
}
catch (error) {
log.errorf("❌ [Resources.getResource] Error getting resource %s, %s", cleanPath, error);
return null;
}
}
/**
* GetResourceWithOpener - Alternative way to get resource with custom opener
*/
async getResourceWithOpener(pathname, opener) {
const cleanPath = path.posix.normalize(pathname);
const cacheKey = `${cleanPath}__get_with_opener`;
// Check cache first
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey) || null;
}
try {
const resource = await this.buildResource(cleanPath, opener);
// Cache the result
if (resource) {
this.cache.set(cacheKey, resource);
}
return resource;
}
catch (error) {
log.errorf("❌ [Resources.getResourceWithOpener] Error getting resource with opener %s, %s", cleanPath, error);
return null;
}
}
/**
* ExecuteAsTemplate - Second primary operation: execute resource as template
* Supports template execution with data binding
*/
async executeAsTemplate(resource, targetPath, data) {
if (!this.templateClient) {
throw new Error('Template client not available. Please set template client first.');
}
const key = resource.key() + "-" + "template" + "-" + targetPath;
const cacheKey = this.cacheKey(key);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const result = await this.templateClient.executeAsTemplate(resource, targetPath, data);
if (result) {
this.cache.set(cacheKey, result);
}
else {
log.warnf("⚠️ [Resources.executeAsTemplate] Template execution returned null for resource %s", resource.key());
}
return result;
}
catch (error) {
log.errorf("❌ [Resources.executeAsTemplate] Error executing template %s, %s", resource.key(), error);
throw error;
}
}
/**
* Minify - Third primary operation: minify a resource
* Supports CSS, JS, HTML, JSON, SVG, and XML minification
*/
async minify(resource) {
if (!this.minifierClient) {
throw new Error('Minifier client not available. Please set minifier client first.');
}
const key = resource.key() + "-" + "minify";
const cacheKey = this.cacheKey(key);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const result = await this.minifierClient.minify(resource);
if (result) {
this.cache.set(cacheKey, result);
}
else {
log.warnf("⚠️ [Resources.minify] Minification returned null for resource %s", resource.key());
}
return result;
}
catch (error) {
log.errorf("❌ [Resources.minify] Error minifying resource %s, %s", resource.key(), error);
throw error;
}
}
/**
* Fingerprint - Fourth primary operation: add fingerprint to resource
* Supports integrity generation and filename fingerprinting
*/
async fingerprint(resource) {
const key = resource.key() + "-" + "fingerprint";
const cacheKey = this.cacheKey(key);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const res = await this.integrityClient.fingerprint(resource);
// Cache the result
if (res) {
this.cache.set(cacheKey, res);
}
else {
log.warnf("⚠️ [Resources.fingerprint] Fingerprint operation returned null for resource %s", resource.key());
}
return res;
}
catch (error) {
log.errorf("❌ [Resources.fingerprint] Error fingerprinting resource %s, %s", resource.key(), error);
throw error;
}
}
cacheKey(key) {
return (0, crypto_1.createHash)('sha256').update(key).digest('hex').substring(0, 16);
}
/**
* Client accessors for external access
*/
getMinifierClient() {
return this.minifierClient;
}
getIntegrityClient() {
return this.integrityClient;
}
setTemplateClient(client) {
this.templateClient = client;
}
/**
* Private helper methods
*/
async buildResource(pathname, opener) {
try {
// Determine media type from file extension
const ext = path.extname(pathname);
const mediaType = this.getMediaTypeFromExtension(ext);
// Create resource paths
const resourcePaths = resources_1.ResourcePaths.newResourcePaths(pathname, this.workspace);
// Create resource with publisher
const resource = new resource_1.ResourceImpl(opener, mediaType, resourcePaths, {}, this.publisher);
return resource;
}
catch (error) {
log.errorf("❌ [Resources.buildResource] Error building resource for %s, %s", pathname, error);
return null;
}
}
getMediaTypeFromExtension(ext) {
// Basic media type mapping - in a real implementation, this would use a proper media type service
const mediaTypes = {
'.js': { type: 'text/javascript', mainType: 'text', subType: 'javascript' },
'.css': { type: 'text/css', mainType: 'text', subType: 'css' },
'.html': { type: 'text/html', mainType: 'text', subType: 'html' },
'.json': { type: 'application/json', mainType: 'application', subType: 'json' },
'.svg': { type: 'image/svg+xml', mainType: 'image', subType: 'svg+xml' },
'.xml': { type: 'application/xml', mainType: 'application', subType: 'xml' },
'.txt': { type: 'text/plain', mainType: 'text', subType: 'plain' }
};
const mediaTypeInfo = mediaTypes[ext] || mediaTypes['.txt'];
return new type_1.MediaType({
type: mediaTypeInfo.type,
mainType: mediaTypeInfo.mainType,
subType: mediaTypeInfo.subType,
delimiter: '.',
firstSuffix: { suffix: ext.substring(1), fullSuffix: ext },
mimeSuffix: '',
suffixesCSV: ext.substring(1)
});
}
async createReadSeekCloser(file) {
try {
// Read all content from the File object
const chunks = [];
let totalBytesRead = 0;
try {
// Read the file in chunks until EOF
while (true) {
const buffer = new Uint8Array(8192); // 8KB buffer
const result = await file.read(buffer);
if (result.bytesRead === 0) {
break;
}
totalBytesRead += result.bytesRead;
chunks.push(buffer.slice(0, result.bytesRead));
}
}
finally {
// Always close the file after reading
await file.close();
}
// Convert chunks to content
let content = '';
if (chunks.length > 0) {
const allBytes = new Uint8Array(totalBytesRead);
let offset = 0;
for (const chunk of chunks) {
allBytes.set(chunk, offset);
offset += chunk.length;
}
content = new TextDecoder().decode(allBytes);
}
// Create ReadSeekCloser from content
return this.newReadSeekerNoOpCloserFromString(content);
}
catch (error) {
// Ensure file is closed even on error
try {
await file.close();
}
catch (closeError) {
log.warnf("❌ [Resources.createReadSeekCloser] Error closing file after read error %s, %s", file.path, closeError);
}
// Return empty stream on error
return this.newReadSeekerNoOpCloserFromString('');
}
}
/**
* Create ReadSeekCloser from string content
* This follows golang's NewReadSeekerNoOpCloserFromString pattern
*/
newReadSeekerNoOpCloserFromString(content) {
const { Readable } = require('stream');
// Create a readable stream with the content
const stream = new Readable({
read() {
// Do nothing - we'll push all content at once
}
});
// Push all content and end the stream
stream.push(content);
stream.push(null); // Signal end of stream
// Add seek and close methods to match ReadSeekCloser interface
return Object.assign(stream, {
seek: async (offset, whence) => {
// For content-based streams, seeking doesn't change anything
// as all content is already available
return 0;
},
close: async () => {
return Promise.resolve();
}
});
}
}
exports.Resources = Resources;
//# sourceMappingURL=resources.js.map