akasharender
Version:
Rendering support for generating static HTML websites or EPUB eBooks
1,338 lines • 276 kB
JavaScript
/**
*
* Copyright 2014-2025 David Herron
*
* This file is part of AkashaCMS (http://akashacms.com/).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _BaseFileCache_instances, _BaseFileCache_config, _BaseFileCache_name, _BaseFileCache_dirs, _BaseFileCache_is_ready, _BaseFileCache_cache_content, _BaseFileCache_map_renderpath, _BaseFileCache_dao, _BaseFileCache_watcher, _BaseFileCache_queue, _BaseFileCache_fExistsInDir;
import { DirsWatcher } from '@akashacms/stacked-dirs';
import path from 'node:path';
import util from 'node:util';
import FS from 'fs';
import EventEmitter from 'events';
import micromatch from 'micromatch';
import { field, id, index, table, schema, BaseDAO } from 'sqlite3orm';
import { sqdb } from '../sqdb.js';
import fastq from 'fastq';
///////////// Assets table
let Asset = class Asset {
};
__decorate([
id({
name: 'vpath', dbtype: 'TEXT'
}),
index('asset_vpath'),
__metadata("design:type", String)
], Asset.prototype, "vpath", void 0);
__decorate([
field({
name: 'mime', dbtype: 'TEXT'
}),
__metadata("design:type", String)
], Asset.prototype, "mime", void 0);
__decorate([
field({
name: 'mounted', dbtype: 'TEXT'
}),
index('asset_mounted'),
__metadata("design:type", String)
], Asset.prototype, "mounted", void 0);
__decorate([
field({
name: 'mountPoint', dbtype: 'TEXT'
}),
index('asset_mountPoint'),
__metadata("design:type", String)
], Asset.prototype, "mountPoint", void 0);
__decorate([
field({
name: 'pathInMounted', dbtype: 'TEXT'
}),
index('asset_pathInMounted'),
__metadata("design:type", String)
], Asset.prototype, "pathInMounted", void 0);
__decorate([
field({
name: 'fspath', dbtype: 'TEXT'
}),
index('asset_fspath'),
__metadata("design:type", String)
], Asset.prototype, "fspath", void 0);
__decorate([
field({
name: 'renderPath', dbtype: 'TEXT'
}),
index('asset_renderPath'),
__metadata("design:type", String)
], Asset.prototype, "renderPath", void 0);
__decorate([
field({
name: 'mtimeMs',
dbtype: "TEXT DEFAULT(datetime('now') || 'Z')"
}),
__metadata("design:type", String)
], Asset.prototype, "mtimeMs", void 0);
__decorate([
field({
name: 'info', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Asset.prototype, "info", void 0);
Asset = __decorate([
table({
name: 'ASSETS',
withoutRowId: true,
})
], Asset);
export { Asset };
await schema().createTable(sqdb, 'ASSETS');
export const assetsDAO = new BaseDAO(Asset, sqdb);
await assetsDAO.createIndex('asset_vpath');
await assetsDAO.createIndex('asset_mounted');
await assetsDAO.createIndex('asset_mountPoint');
await assetsDAO.createIndex('asset_pathInMounted');
await assetsDAO.createIndex('asset_fspath');
await assetsDAO.createIndex('asset_renderPath');
//////////// Partials Table
let Partial = class Partial {
};
__decorate([
id({
name: 'vpath', dbtype: 'TEXT'
}),
index('partial_vpath'),
__metadata("design:type", String)
], Partial.prototype, "vpath", void 0);
__decorate([
field({
name: 'mime', dbtype: 'TEXT'
}),
__metadata("design:type", String)
], Partial.prototype, "mime", void 0);
__decorate([
field({
name: 'mounted', dbtype: 'TEXT'
}),
index('partial_mounted'),
__metadata("design:type", String)
], Partial.prototype, "mounted", void 0);
__decorate([
field({
name: 'mountPoint', dbtype: 'TEXT'
}),
index('partial_mountPoint'),
__metadata("design:type", String)
], Partial.prototype, "mountPoint", void 0);
__decorate([
field({
name: 'pathInMounted', dbtype: 'TEXT'
}),
index('partial_pathInMounted'),
__metadata("design:type", String)
], Partial.prototype, "pathInMounted", void 0);
__decorate([
field({
name: 'fspath', dbtype: 'TEXT'
}),
index('partial_fspath'),
__metadata("design:type", String)
], Partial.prototype, "fspath", void 0);
__decorate([
field({
name: 'renderPath', dbtype: 'TEXT'
}),
index('partial_renderPath'),
__metadata("design:type", String)
], Partial.prototype, "renderPath", void 0);
__decorate([
field({
name: 'mtimeMs',
dbtype: "TEXT DEFAULT(datetime('now') || 'Z')"
}),
__metadata("design:type", String)
], Partial.prototype, "mtimeMs", void 0);
__decorate([
field({
name: 'docMetadata', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Partial.prototype, "docMetadata", void 0);
__decorate([
field({
name: 'docContent', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Partial.prototype, "docContent", void 0);
__decorate([
field({
name: 'docBody', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Partial.prototype, "docBody", void 0);
__decorate([
field({
name: 'metadata', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Partial.prototype, "metadata", void 0);
__decorate([
field({
name: 'info', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Partial.prototype, "info", void 0);
Partial = __decorate([
table({
name: 'PARTIALS',
withoutRowId: true,
})
], Partial);
export { Partial };
await schema().createTable(sqdb, 'PARTIALS');
export const partialsDAO = new BaseDAO(Partial, sqdb);
await partialsDAO.createIndex('partial_vpath');
await partialsDAO.createIndex('partial_mounted');
await partialsDAO.createIndex('partial_mountPoint');
await partialsDAO.createIndex('partial_pathInMounted');
await partialsDAO.createIndex('partial_fspath');
await partialsDAO.createIndex('partial_renderPath');
///////////////// Layouts Table
let Layout = class Layout {
};
__decorate([
id({
name: 'vpath', dbtype: 'TEXT'
}),
index('layout_vpath'),
__metadata("design:type", String)
], Layout.prototype, "vpath", void 0);
__decorate([
field({
name: 'mime', dbtype: 'TEXT'
}),
__metadata("design:type", String)
], Layout.prototype, "mime", void 0);
__decorate([
field({
name: 'mounted', dbtype: 'TEXT'
}),
index('layout_mounted'),
__metadata("design:type", String)
], Layout.prototype, "mounted", void 0);
__decorate([
field({
name: 'mountPoint', dbtype: 'TEXT'
}),
index('layout_mountPoint'),
__metadata("design:type", String)
], Layout.prototype, "mountPoint", void 0);
__decorate([
field({
name: 'pathInMounted', dbtype: 'TEXT'
}),
index('layout_pathInMounted'),
__metadata("design:type", String)
], Layout.prototype, "pathInMounted", void 0);
__decorate([
field({
name: 'fspath', dbtype: 'TEXT'
}),
index('layout_fspath'),
__metadata("design:type", String)
], Layout.prototype, "fspath", void 0);
__decorate([
field({
name: 'renderPath', dbtype: 'TEXT'
}),
index('layout_renderPath'),
__metadata("design:type", String)
], Layout.prototype, "renderPath", void 0);
__decorate([
field({
name: 'mtimeMs',
dbtype: "TEXT DEFAULT(datetime('now') || 'Z')"
}),
__metadata("design:type", String)
], Layout.prototype, "mtimeMs", void 0);
__decorate([
field({
name: 'docMetadata', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Layout.prototype, "docMetadata", void 0);
__decorate([
field({
name: 'docContent', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Layout.prototype, "docContent", void 0);
__decorate([
field({
name: 'docBody', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Layout.prototype, "docBody", void 0);
__decorate([
field({
name: 'metadata', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Layout.prototype, "metadata", void 0);
__decorate([
field({
name: 'info', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Layout.prototype, "info", void 0);
Layout = __decorate([
table({
name: 'LAYOUTS',
withoutRowId: true,
})
], Layout);
export { Layout };
await schema().createTable(sqdb, 'LAYOUTS');
export const layoutsDAO = new BaseDAO(Layout, sqdb);
await layoutsDAO.createIndex('layout_vpath');
await layoutsDAO.createIndex('layout_mounted');
await layoutsDAO.createIndex('layout_mountPoint');
await layoutsDAO.createIndex('layout_pathInMounted');
await layoutsDAO.createIndex('layout_fspath');
await layoutsDAO.createIndex('layout_renderPath');
/////////////// Documents Table
let Document = class Document {
};
__decorate([
id({
name: 'vpath', dbtype: 'TEXT'
}),
index('docs_vpath'),
__metadata("design:type", String)
], Document.prototype, "vpath", void 0);
__decorate([
field({
name: 'mime', dbtype: 'TEXT'
}),
__metadata("design:type", String)
], Document.prototype, "mime", void 0);
__decorate([
field({
name: 'mounted', dbtype: 'TEXT'
}),
index('docs_mounted'),
__metadata("design:type", String)
], Document.prototype, "mounted", void 0);
__decorate([
field({
name: 'mountPoint', dbtype: 'TEXT'
}),
index('docs_mountPoint'),
__metadata("design:type", String)
], Document.prototype, "mountPoint", void 0);
__decorate([
field({
name: 'pathInMounted', dbtype: 'TEXT'
}),
index('docs_pathInMounted'),
__metadata("design:type", String)
], Document.prototype, "pathInMounted", void 0);
__decorate([
field({
name: 'fspath', dbtype: 'TEXT'
}),
index('docs_fspath'),
__metadata("design:type", String)
], Document.prototype, "fspath", void 0);
__decorate([
field({
name: 'renderPath', dbtype: 'TEXT'
}),
index('docs_renderPath'),
__metadata("design:type", String)
], Document.prototype, "renderPath", void 0);
__decorate([
field({
name: 'rendersToHTML', dbtype: 'INTEGER'
}),
index('docs_rendersToHTML'),
__metadata("design:type", Boolean)
], Document.prototype, "rendersToHTML", void 0);
__decorate([
field({
name: 'dirname', dbtype: 'TEXT'
}),
index('docs_dirname'),
__metadata("design:type", String)
], Document.prototype, "dirname", void 0);
__decorate([
field({
name: 'parentDir', dbtype: 'TEXT'
}),
index('docs_parentDir'),
__metadata("design:type", String)
], Document.prototype, "parentDir", void 0);
__decorate([
field({
name: 'mtimeMs',
dbtype: "TEXT DEFAULT(datetime('now') || 'Z')"
}),
__metadata("design:type", String)
], Document.prototype, "mtimeMs", void 0);
__decorate([
field({
name: 'docMetadata', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Document.prototype, "docMetadata", void 0);
__decorate([
field({
name: 'docContent', dbtype: 'TEXT', isJson: false
}),
__metadata("design:type", String)
], Document.prototype, "docContent", void 0);
__decorate([
field({
name: 'docBody', dbtype: 'TEXT', isJson: false
}),
__metadata("design:type", String)
], Document.prototype, "docBody", void 0);
__decorate([
field({
name: 'metadata', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Document.prototype, "metadata", void 0);
__decorate([
field({
name: 'tags', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Document.prototype, "tags", void 0);
__decorate([
field({
name: 'layout', dbtype: 'TEXT', isJson: false
}),
index('docs_layout'),
__metadata("design:type", String)
], Document.prototype, "layout", void 0);
__decorate([
field({
name: 'blogtag', dbtype: 'TEXT', isJson: false
}),
index('docs_blogtag'),
__metadata("design:type", String)
], Document.prototype, "blogtag", void 0);
__decorate([
field({
name: 'info', dbtype: 'TEXT', isJson: true
}),
__metadata("design:type", Object)
], Document.prototype, "info", void 0);
Document = __decorate([
table({
name: 'DOCUMENTS',
withoutRowId: true,
})
], Document);
export { Document };
await schema().createTable(sqdb, 'DOCUMENTS');
export const documentsDAO = new BaseDAO(Document, sqdb);
await documentsDAO.createIndex('docs_vpath');
await documentsDAO.createIndex('docs_mounted');
await documentsDAO.createIndex('docs_mountPoint');
await documentsDAO.createIndex('docs_pathInMounted');
await documentsDAO.createIndex('docs_fspath');
await documentsDAO.createIndex('docs_renderPath');
await documentsDAO.createIndex('docs_rendersToHTML');
await documentsDAO.createIndex('docs_dirname');
await documentsDAO.createIndex('docs_parentDir');
await documentsDAO.createIndex('docs_blogtag');
let TagGlue = class TagGlue {
};
__decorate([
field({ name: 'docvpath', dbtype: 'TEXT' })
// @fk('tag_docvpath', 'DOCUMENTS', 'vpath')
,
index('tagglue_vpath'),
__metadata("design:type", String)
], TagGlue.prototype, "docvpath", void 0);
__decorate([
field({ name: 'tagName', dbtype: 'TEXT' })
// @fk('tag_slug', 'TAGS', 'slug')
,
index('tagglue_name'),
__metadata("design:type", String)
], TagGlue.prototype, "tagName", void 0);
TagGlue = __decorate([
table({ name: 'TAGGLUE' })
], TagGlue);
await schema().createTable(sqdb, 'TAGGLUE');
export const tagGlueDAO = new BaseDAO(TagGlue, sqdb);
await tagGlueDAO.createIndex('tagglue_vpath');
await tagGlueDAO.createIndex('tagglue_name');
// @table({ name: 'TAGS' })
// class Tag {
// @field({
// name: 'tagname',
// dbtype: 'TEXT'
// })
// tagname: string;
// @id({
// name: 'slug', dbtype: 'TEXT'
// })
// @index('tag_slug')
// slug: string;
// @field({
// name: 'description', dbtype: 'TEXT'
// })
// description?: string;
// }
// await schema().createTable(sqdb, 'TAGS');
// const tagsDAO = new BaseDAO<Tag>(Tag, sqdb);
// Convert AkashaCMS mount points into the mountpoint
// used by DirsWatcher
const remapdirs = (dirz) => {
return dirz.map(dir => {
// console.log('document dir ', dir);
if (typeof dir === 'string') {
return {
mounted: dir,
mountPoint: '/',
baseMetadata: {}
};
}
else {
if (!dir.dest) {
throw new Error(`remapdirs invalid mount specification ${util.inspect(dir)}`);
}
return {
mounted: dir.src,
mountPoint: dir.dest,
baseMetadata: dir.baseMetadata,
ignore: dir.ignore
};
}
});
};
export class BaseFileCache extends EventEmitter {
/**
* @param config AkashaRender Configuration object
* @param dirs array of directories and mount points to watch
* @param name string giving the name for this watcher name
* @param dao The SQLITE3ORM DAO instance to use
*/
constructor(config, name, dirs, dao // BaseDAO<T>
) {
super();
_BaseFileCache_instances.add(this);
_BaseFileCache_config.set(this, void 0);
_BaseFileCache_name.set(this, void 0);
_BaseFileCache_dirs.set(this, void 0);
_BaseFileCache_is_ready.set(this, false);
_BaseFileCache_cache_content.set(this, void 0);
_BaseFileCache_map_renderpath.set(this, void 0);
_BaseFileCache_dao.set(this, void 0); // BaseDAO<T>;
// SKIP: getDynamicView
_BaseFileCache_watcher.set(this, void 0);
_BaseFileCache_queue.set(this, void 0);
// console.log(`BaseFileCache ${name} constructor dirs=${util.inspect(dirs)}`);
__classPrivateFieldSet(this, _BaseFileCache_config, config, "f");
__classPrivateFieldSet(this, _BaseFileCache_name, name, "f");
__classPrivateFieldSet(this, _BaseFileCache_dirs, dirs, "f");
__classPrivateFieldSet(this, _BaseFileCache_is_ready, false, "f");
__classPrivateFieldSet(this, _BaseFileCache_cache_content, false, "f");
__classPrivateFieldSet(this, _BaseFileCache_map_renderpath, false, "f");
__classPrivateFieldSet(this, _BaseFileCache_dao, dao, "f");
}
get config() { return __classPrivateFieldGet(this, _BaseFileCache_config, "f"); }
get name() { return __classPrivateFieldGet(this, _BaseFileCache_name, "f"); }
get dirs() { return __classPrivateFieldGet(this, _BaseFileCache_dirs, "f"); }
set cacheContent(doit) { __classPrivateFieldSet(this, _BaseFileCache_cache_content, doit, "f"); }
get gacheContent() { return __classPrivateFieldGet(this, _BaseFileCache_cache_content, "f"); }
set mapRenderPath(doit) { __classPrivateFieldSet(this, _BaseFileCache_map_renderpath, doit, "f"); }
get mapRenderPath() { return __classPrivateFieldGet(this, _BaseFileCache_map_renderpath, "f"); }
get dao() { return __classPrivateFieldGet(this, _BaseFileCache_dao, "f"); }
async close() {
if (__classPrivateFieldGet(this, _BaseFileCache_queue, "f")) {
__classPrivateFieldGet(this, _BaseFileCache_queue, "f").killAndDrain();
__classPrivateFieldSet(this, _BaseFileCache_queue, undefined, "f");
}
if (__classPrivateFieldGet(this, _BaseFileCache_watcher, "f")) {
// console.log(`CLOSING ${this.name}`);
await __classPrivateFieldGet(this, _BaseFileCache_watcher, "f").close();
__classPrivateFieldSet(this, _BaseFileCache_watcher, undefined, "f");
}
this.removeAllListeners('changed');
this.removeAllListeners('added');
this.removeAllListeners('unlinked');
this.removeAllListeners('ready');
await sqdb.close();
}
/**
* Set up receiving events from DirsWatcher, and dispatching to
* the handler methods.
*/
async setup() {
const fcache = this;
if (__classPrivateFieldGet(this, _BaseFileCache_watcher, "f")) {
await __classPrivateFieldGet(this, _BaseFileCache_watcher, "f").close();
}
__classPrivateFieldSet(this, _BaseFileCache_queue, fastq.promise(async function (event) {
if (event.code === 'changed') {
try {
// console.log(`change ${event.name} ${event.info.vpath}`);
await fcache.handleChanged(event.name, event.info);
fcache.emit('change', event.name, event.info);
}
catch (e) {
fcache.emit('error', {
code: event.code,
name: event.name,
vpath: event.info.vpath,
error: e
});
}
}
else if (event.code === 'added') {
try {
// console.log(`add ${event.name} ${event.info.vpath}`);
await fcache.handleAdded(event.name, event.info);
fcache.emit('add', event.name, event.info);
}
catch (e) {
fcache.emit('error', {
code: event.code,
name: event.name,
vpath: event.info.vpath,
error: e
});
}
}
else if (event.code === 'unlinked') {
try {
// console.log(`unlink ${event.name} ${event.info.vpath}`, event.info);
await fcache.handleUnlinked(event.name, event.info);
fcache.emit('unlink', event.name, event.info);
}
catch (e) {
fcache.emit('error', {
code: event.code,
name: event.name,
vpath: event.info.vpath,
error: e
});
}
/* } else if (event.code === 'error') {
await fcache.handleError(event.name) */
}
else if (event.code === 'ready') {
await fcache.handleReady(event.name);
fcache.emit('ready', event.name);
}
}, 10), "f");
__classPrivateFieldSet(this, _BaseFileCache_watcher, new DirsWatcher(this.name), "f");
__classPrivateFieldGet(this, _BaseFileCache_watcher, "f").on('change', async (name, info) => {
// console.log(`${name} changed ${info.mountPoint} ${info.vpath}`);
try {
if (!this.ignoreFile(info)) {
// console.log(`PUSH ${name} changed ${info.mountPoint} ${info.vpath}`);
__classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({
code: 'changed',
name, info
});
}
else {
console.log(`Ignored 'change' for ${info.vpath}`);
}
}
catch (err) {
console.error(`FAIL change ${info.vpath} because ${err.stack}`);
}
})
.on('add', async (name, info) => {
try {
// console.log(`${name} add ${info.mountPoint} ${info.vpath}`);
if (!this.ignoreFile(info)) {
// console.log(`PUSH ${name} add ${info.mountPoint} ${info.vpath}`);
__classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({
code: 'added',
name, info
});
}
else {
console.log(`Ignored 'add' for ${info.vpath}`);
}
}
catch (err) {
console.error(`FAIL add ${info.vpath} because ${err.stack}`);
}
})
.on('unlink', async (name, info) => {
// console.log(`unlink ${name} ${info.vpath}`);
try {
if (!this.ignoreFile(info)) {
__classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({
code: 'unlinked',
name, info
});
}
else {
console.log(`Ignored 'unlink' for ${info.vpath}`);
}
}
catch (err) {
console.error(`FAIL unlink ${info.vpath} because ${err.stack}`);
}
})
.on('ready', async (name) => {
// console.log(`${name} ready`);
__classPrivateFieldGet(this, _BaseFileCache_queue, "f").push({
code: 'ready',
name
});
});
const mapped = remapdirs(this.dirs);
// console.log(`setup ${this.#name} watch ${util.inspect(this.#dirs)} ==> ${util.inspect(mapped)}`);
await __classPrivateFieldGet(this, _BaseFileCache_watcher, "f").watch(mapped);
// console.log(`DAO ${this.dao.table.name} ${util.inspect(this.dao.table.fields)}`);
}
gatherInfoData(info) {
// Placeholder which some subclasses
// are expected to override
info.renderPath = info.vpath;
}
async handleChanged(name, info) {
// console.log(`PROCESS ${name} handleChanged`, info.vpath);
if (this.ignoreFile(info)) {
// console.log(`OOOOOOOOGA!!! Received a file that should be ingored `, info);
return;
}
if (name !== this.name) {
throw new Error(`handleChanged event for wrong name; got ${name}, expected ${this.name}`);
}
// console.log(`handleChanged ${info.vpath} ${info.metadata && info.metadata.publicationDate ? info.metadata.publicationDate : '???'}`);
this.gatherInfoData(info);
info.stack = undefined;
const result = await this.dao.selectAll({
vpath: { eq: info.vpath },
mounted: { eq: info.mounted }
});
if (!Array.isArray(result)
|| result.length <= 0) {
// It wasn't found in the database. Hence
// we should add it.
return this.handleAdded(name, info);
}
info.stack = undefined;
await this.updateDocInDB(info);
await this.config.hookFileChanged(name, info);
}
async updateDocInDB(info) {
await __classPrivateFieldGet(this, _BaseFileCache_dao, "f").update({
vpath: info.vpath,
mime: info.mime,
mounted: info.mounted,
mountPoint: info.mountPoint,
pathInMounted: info.pathInMounted,
mtimeMs: new Date(info.statsMtime).toISOString(),
fspath: path.join(info.mounted, info.pathInMounted),
renderPath: info.renderPath,
rendersToHTML: info.rendersToHTML,
dirname: path.dirname(info.renderPath),
docMetadata: info.docMetadata,
// docContent: info.docContent,
// docBody: info.docBody,
metadata: info.metadata,
info,
});
}
/**
* We receive this:
*
* {
* fspath: fspath,
* vpath: vpath,
* mime: mime.getType(fspath),
* mounted: dir.mounted,
* mountPoint: dir.mountPoint,
* pathInMounted: computed relative path
* stack: [ array of these instances ]
* }
*
* Need to add:
* renderPath
* And for HTML render files, add the baseMetadata and docMetadata
*
* Should remove the stack, since it's likely not useful to us.
*/
async handleAdded(name, info) {
// console.log(`PROCESS ${name} handleAdded`, info.vpath);
if (this.ignoreFile(info)) {
// console.log(`OOOOOOOOGA!!! Received a file that should be ingored `, info);
return;
}
if (name !== this.name) {
throw new Error(`handleAdded event for wrong name; got ${name}, expected ${this.name}`);
}
this.gatherInfoData(info);
info.stack = undefined;
await this.insertDocToDB(info);
await this.config.hookFileAdded(name, info);
}
async insertDocToDB(info) {
await __classPrivateFieldGet(this, _BaseFileCache_dao, "f").insert({
vpath: info.vpath,
mime: info.mime,
mounted: info.mounted,
mountPoint: info.mountPoint,
pathInMounted: info.pathInMounted,
mtimeMs: new Date(info.statsMtime).toISOString(),
fspath: path.join(info.mounted, info.pathInMounted),
renderPath: info.renderPath,
rendersToHTML: info.rendersToHTML,
dirname: path.dirname(info.renderPath),
docMetadata: info.docMetadata,
// docContent: info.docContent,
// docBody: info.docBody,
metadata: info.metadata,
info,
});
}
async handleUnlinked(name, info) {
// console.log(`PROCESS ${name} handleUnlinked`, info.vpath);
if (name !== this.name) {
throw new Error(`handleUnlinked event for wrong name; got ${name}, expected ${this.name}`);
}
await this.config.hookFileUnlinked(name, info);
await __classPrivateFieldGet(this, _BaseFileCache_dao, "f").deleteAll({
vpath: { eq: info.vpath },
mounted: { eq: info.mounted }
});
}
async handleReady(name) {
// console.log(`PROCESS ${name} handleReady`);
if (name !== this.name) {
throw new Error(`handleReady event for wrong name; got ${name}, expected ${this.name}`);
}
__classPrivateFieldSet(this, _BaseFileCache_is_ready, true, "f");
this.emit('ready', name);
}
/**
* Find the directory mount corresponding to the file.
*
* @param {*} info
* @returns
*/
fileDirMount(info) {
const mapped = remapdirs(this.dirs);
for (const dir of mapped) {
// console.log(`dirMount for ${info.vpath} -- ${util.inspect(info)} === ${util.inspect(dir)}`);
if (info.mountPoint === dir.mountPoint) {
return dir;
}
}
return undefined;
}
/**
* Should this file be ignored, based on the `ignore` field
* in the matching `dir` mount entry.
*
* @param {*} info
* @returns
*/
ignoreFile(info) {
// console.log(`ignoreFile ${info.vpath}`);
const dirMount = this.fileDirMount(info);
// console.log(`ignoreFile ${info.vpath} dirMount ${util.inspect(dirMount)}`);
let ignore = false;
if (dirMount) {
let ignores;
if (typeof dirMount.ignore === 'string') {
ignores = [dirMount.ignore];
}
else if (Array.isArray(dirMount.ignore)) {
ignores = dirMount.ignore;
}
else {
ignores = [];
}
for (const i of ignores) {
if (micromatch.isMatch(info.vpath, i))
ignore = true;
// console.log(`dirMount.ignore ${fspath} ${i} => ${ignore}`);
}
// if (ignore) console.log(`MUST ignore File ${info.vpath}`);
// console.log(`ignoreFile for ${info.vpath} ==> ${ignore}`);
return ignore;
}
else {
// no mount? that means something strange
console.error(`No dirMount found for ${info.vpath} / ${info.dirMountedOn}`);
return true;
}
}
/**
* Allow a caller to wait until the <em>ready</em> event has
* been sent from the DirsWatcher instance. This event means the
* initial indexing has happened.
*/
async isReady() {
// If there's no directories, there won't be any files
// to load, and no need to wait
while (__classPrivateFieldGet(this, _BaseFileCache_dirs, "f").length > 0 && !__classPrivateFieldGet(this, _BaseFileCache_is_ready, "f")) {
// This does a 100ms pause
// That lets us check is_ready every 100ms
// at very little cost
// console.log(`!isReady ${this.name} ${this[_symb_dirs].length} ${this[_symb_is_ready]}`);
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(undefined);
}, 100);
});
}
return true;
}
async paths(rootPath) {
const fcache = this;
let rootP = rootPath?.startsWith('/')
? rootPath?.substring(1)
: rootPath;
// This is copied from the older version
// (LokiJS version) of this function. It
// seems meant to eliminate duplicates.
const vpathsSeen = new Set();
const selector = {
order: { mtimeMs: true }
};
if (typeof rootP === 'string'
&& rootP.length >= 1) {
selector.renderPath = {
isLike: `${rootP}%`
// sql: ` renderPath regexp '^${rootP}' `
};
}
// console.log(`paths ${util.inspect(selector)}`);
const result = await this.dao.selectAll(selector);
const result2 = result.filter(item => {
// console.log(`paths ?ignore? ${item.vpath}`);
if (fcache.ignoreFile(item)) {
return false;
}
if (vpathsSeen.has(item.vpath)) {
return false;
}
else {
vpathsSeen.add(item.vpath);
return true;
}
});
// const result3 = result2.sort((a, b) => {
// // We need these to be one of the concrete
// // types so that the mtimeMs field is
// // recognized by TypeScript. The Asset
// // class is a good substitute for the base
// // class of cached files.
// const aa = <Asset>a;
// const bb = <Asset>b;
// if (aa.mtimeMs < bb.mtimeMs) return 1;
// if (aa.mtimeMs === bb.mtimeMs) return 0;
// if (aa.mtimeMs > bb.mtimeMs) return -1;
// });
// This stage converts the items
// received by this function into
// what is required from
// the paths method.
// const result4
// = new Array<PathsReturnType>();
// for (const item of result3) {
// result4.push(<PathsReturnType>{
// vpath: item.vpath,
// mime: item.mime,
// mounted: item.mounted,
// mountPoint: item.mountPoint,
// pathInMounted: item.pathInMounted,
// mtimeMs: item.mtimeMs,
// info: item.info,
// fspath: path.join(item.mounted, item.pathInMounted),
// renderPath: item.vpath
// });
// }
// console.log(result2/*.map(item => {
// return {
// vpath: item.vpath,
// mtimeMs: item.mtimeMs
// };
// }) */);
return result2;
}
/**
* Find the file within the cache.
*
* @param _fpath The vpath or renderPath to look for
* @returns boolean true if found, false otherwise
*/
async find(_fpath) {
if (typeof _fpath !== 'string') {
throw new Error(`find parameter not string ${typeof _fpath}`);
}
const fpath = _fpath.startsWith('/')
? _fpath.substring(1)
: _fpath;
const fcache = this;
const result1 = await this.dao.selectAll({
or: [
{ vpath: { eq: fpath } },
{ renderPath: { eq: fpath } }
]
});
// console.log(`find ${_fpath} ${fpath} ==> result1 ${util.inspect(result1)} `);
const result2 = result1.filter(item => {
return !(fcache.ignoreFile(item));
});
// console.log(`find ${_fpath} ${fpath} ==> result2 ${util.inspect(result2)} `);
let ret;
if (Array.isArray(result2) && result2.length > 0) {
ret = result2[0];
}
else if (Array.isArray(result2) && result2.length <= 0) {
ret = undefined;
}
else {
ret = result2;
}
return ret;
}
/**
* Fulfills the "find" operation not by
* looking in the database, but by scanning
* the filesystem using synchronous calls.
*
* @param _fpath
* @returns
*/
findSync(_fpath) {
if (typeof _fpath !== 'string') {
throw new Error(`find parameter not string ${typeof _fpath}`);
}
const fpath = _fpath.startsWith('/')
? _fpath.substring(1)
: _fpath;
const fcache = this;
const mapped = remapdirs(this.dirs);
// console.log(`findSync looking for ${fpath} in ${util.inspect(mapped)}`);
for (const dir of mapped) {
if (!(dir?.mountPoint)) {
console.warn(`findSync bad dirs in ${util.inspect(this.dirs)}`);
}
const found = __classPrivateFieldGet(this, _BaseFileCache_instances, "m", _BaseFileCache_fExistsInDir).call(this, fpath, dir);
if (found) {
// console.log(`findSync ${fpath} found`, found);
return found;
}
}
return undefined;
}
async findAll() {
const fcache = this;
const result1 = await this.dao.selectAll({});
const result2 = result1.filter(item => {
// console.log(`findAll ?ignore? ${item.vpath}`);
return !(fcache.ignoreFile(item));
});
return result2;
}
}
_BaseFileCache_config = new WeakMap(), _BaseFileCache_name = new WeakMap(), _BaseFileCache_dirs = new WeakMap(), _BaseFileCache_is_ready = new WeakMap(), _BaseFileCache_cache_content = new WeakMap(), _BaseFileCache_map_renderpath = new WeakMap(), _BaseFileCache_dao = new WeakMap(), _BaseFileCache_watcher = new WeakMap(), _BaseFileCache_queue = new WeakMap(), _BaseFileCache_instances = new WeakSet(), _BaseFileCache_fExistsInDir = function _BaseFileCache_fExistsInDir(fpath, dir) {
// console.log(`#fExistsInDir ${fpath} ${util.inspect(dir)}`);
if (dir.mountPoint === '/') {
const fspath = path.join(dir.mounted, fpath);
let fsexists = FS.existsSync(fspath);
if (fsexists) {
let stats = FS.statSync(fspath);
return {
vpath: fpath,
renderPath: fpath,
fspath: fspath,
mime: undefined,
mounted: dir.mounted,
mountPoint: dir.mountPoint,
pathInMounted: fpath,
statsMtime: stats.mtimeMs
};
}
else {
return undefined;
}
}
let mp = dir.mountPoint.startsWith('/')
? dir.mountPoint.substring(1)
: dir.mountPoint;
mp = mp.endsWith('/')
? mp
: (mp + '/');
if (fpath.startsWith(mp)) {
let pathInMounted = fpath.replace(dir.mountPoint, '');
let fspath = path.join(dir.mounted, pathInMounted);
// console.log(`Checking exist for ${dir.mountPoint} ${dir.mounted} ${pathInMounted} ${fspath}`);
let fsexists = FS.existsSync(fspath);
if (fsexists) {
let stats = FS.statSync(fspath);
return {
vpath: fpath,
renderPath: fpath,
fspath: fspath,
mime: undefined,
mounted: dir.mounted,
mountPoint: dir.mountPoint,
pathInMounted: pathInMounted,
statsMtime: stats.mtimeMs
};
}
}
return undefined;
};
export class TemplatesFileCache extends BaseFileCache {
constructor(config, name, dirs, dao) {
super(config, name, dirs, dao);
}
/**
* Gather the additional data suitable
* for Partial and Layout templates. The
* full data set required for Documents is
* not suitable for the templates.
*
* @param info
*/
gatherInfoData(info) {
info.renderPath = info.vpath;
info.dirname = path.dirname(info.vpath);
if (info.dirname === '.')
info.dirname = '/';
let renderer = this.config.findRendererPath(info.vpath);
info.renderer = renderer;
if (renderer) {
if (renderer.parseMetadata) {
// Using <any> here covers over
// that parseMetadata requires
// a RenderingContext which
// in turn requires a
// metadata object.
const rc = renderer.parseMetadata({
fspath: info.fspath,
content: FS.readFileSync(info.fspath, 'utf-8')
});
// docMetadata is the unmodified metadata/frontmatter
// in the document
info.docMetadata = rc.metadata;
// docContent is the unparsed original content
// including any frontmatter
info.docContent = rc.content;
// docBody is the parsed body -- e.g. following the frontmatter
info.docBody = rc.body;
// This is the computed metadata that includes data from
// several sources
info.metadata = {};
if (!info.docMetadata)
info.docMetadata = {};
for (let yprop in info.baseMetadata) {
// console.log(`initMetadata ${basedir} ${fpath} baseMetadata ${baseMetadata[yprop]}`);
info.metadata[yprop] = info.baseMetadata[yprop];
}
}
}
// console.log(`TemplatesFileCache after gatherInfoData `, info);
}
async updateDocInDB(info) {
await this.dao.update({
vpath: info.vpath,
mime: info.mime,
mounted: info.mounted,
mountPoint: info.mountPoint,
pathInMounted: info.pathInMounted,
mtimeMs: new Date(info.statsMtime).toISOString(),
fspath: path.join(info.mounted, info.pathInMounted),
renderPath: info.renderPath,
rendersToHTML: info.rendersToHTML,
dirname: path.dirname(info.renderPath),
docMetadata: info.docMetadata,
docContent: info.docContent,
docBody: info.docBody,
metadata: info.metadata,
info,
});
}
async insertDocToDB(info) {
await this.dao.insert({
vpath: info.vpath,
mime: info.mime,
mounted: info.mounted,
mountPoint: info.mountPoint,
pathInMounted: info.pathInMounted,
mtimeMs: new Date(info.statsMtime).toISOString(),
fspath: path.join(info.mounted, info.pathInMounted),
renderPath: info.renderPath,
rendersToHTML: info.rendersToHTML,
dirname: path.dirname(info.renderPath),
docMetadata: info.docMetadata,
docContent: info.docContent,
docBody: info.docBody,
metadata: info.metadata,
info,
});
}
}
export class DocumentsFileCache extends BaseFileCache {
constructor(config, name, dirs) {
super(config, name, dirs, documentsDAO);
}
gatherInfoData(info) {
info.renderPath = info.vpath;
info.dirname = path.dirname(info.vpath);
if (info.dirname === '.')
info.dirname = '/';
info.parentDir = path.dirname(info.dirname);
// find the mounted directory,
// get the baseMetadata
for (let dir of remapdirs(this.dirs)) {
if (dir.mounted === info.mounted) {
if (dir.baseMetadata) {
info.baseMetadata = dir.baseMetadata;
}
break;
}
}
// set publicationDate somehow
let renderer = this.config.findRendererPath(info.vpath);
info.renderer = renderer;
if (renderer) {
info.renderPath
= renderer.filePath(info.vpath);
// This was in the LokiJS code, but
// was not in use.
// info.rendername = path.basename(
// info.renderPath
// );
info.rendersToHTML = micromatch.isMatch(info.renderPath, '**/*.html')
? true : false;
if (renderer.parseMetadata) {
// Using <any> here covers over
// that parseMetadata requires
// a RenderingContext which
// in turn requires a
// metadata object.
const rc = renderer.parseMetadata({
fspath: info.fspath,
content: FS.readFileSync(info.fspath, 'utf-8')
});
// docMetadata is the unmodified metadata/frontmatter
// in the document
info.docMetadata = rc.metadata;
// docContent is the unparsed original content
// including any frontmatter
info.docContent = rc.content;
// docBody is the parsed body -- e.g. following the frontmatter
info.docBody = rc.body;
// This is the computed metadata that includes data from
// several sources
info.metadata = {};
if (!info.docMetadata)
info.docMetadata = {};
// The rest of this is adapted from the old function
// HTMLRenderer.newInitMetadata
// For starters the metadata is collected from several sources.
// 1) the metadata specified in the directory mount where
// this document was found
// 2) metadata in the project configuration
// 3) the metadata in the document, as captured in docMetadata
for (let yprop in info.baseMetadata) {
// console.log(`initMetadata ${basedir} ${fpath} baseMetadata ${baseMetadata[yprop]}`);
info.metadata[yprop] = info.baseMetadata[yprop];
}
for (let yprop in this.config.metadata) {
info.metadata[yprop] = this.config.metadata[yprop];
}
let fmmcount = 0;
for (let yprop in info.docMetadata) {
info.metadata[yprop] = info.docMetadata[yprop];
fmmcount++;
}
// The rendered version of the content lands here
info.metadata.content = "";
// The document object has been useful for
// communicating the file path and other data.
info.metadata.document = {};
info.metadata.document.basedir = info.mountPoint;
info.metadata.document.relpath = info.pathInMounted;
info.metadata.document.relrender = renderer.filePath(info.pathInMounted);
info.metadata.document.path = info.vpath;
info.metadata.document.renderTo = info.renderPath;
// Ensure the <em>tags</em> field is an array
if (!(info.metadata.tags)) {
info.metadata.tags = [];
}
else if (typeof (info.metadata.tags) === 'string') {
let taglist = [];
const re = /\s*,\s*/;
info.metadata.tags.split(re).forEach(tag => {
taglist.push(tag.trim());
});
info.metadata.tags = taglist;
}
else if (!Array.isArray(info.metadata.tags)) {
throw new Error(`FORMAT ERROR - ${info.vpath} has badly formatted tags `, info.metadata.tags);
}
info.docMetadata.tags = info.metadata.tags;
// The root URL for the project
info.metadata.root_url = this.config.root_url;
// Compute the URL this document will render to
if (this.config.root_url) {
let uRootUrl = new URL(this.config.root_url, 'http://example.com');
uRootUrl.pathname = path.normalize(path.join(uRootUrl.pathname, info.metadata.document.renderTo));
info.metadata.rendered_url = uRootUrl.toString();
}
else {
info.metadata.rendered_url = info.metadata.document.renderTo;
}
// info.metadata.rendered_date = info.stats.mtime;
const parsePublDate = (date) => {
const parsed = Date.parse(date);
if (!isNaN(parsed)) {
info.metadata.publicationDate = new Date(parsed);
info.publicationDate = info.metadata.publicationDate;
info.publicationTime = info.publicationDate.getTime();
}
};
if (info.docMetadata
&& typeof info.docMetadata.publDate