Skip to main content
Module

x/lume/core/source.ts

πŸ”₯ Static site generator for Deno πŸ¦•
Very Popular
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
import { posix } from "../deps/path.ts";import { concurrent, normalizePath } from "./utils.ts";import { Components, Directory, Page, StaticFile } from "./filesystem.ts";
import type { ComponentLoader, Data, DataLoader, DirEntry, Formats, PageLoader, Reader, ScopeFilter,} from "../core.ts";
export interface Options { formats: Formats; globalData: Data; globalComponents: Components; dataLoader: DataLoader; pageLoader: PageLoader; componentLoader: ComponentLoader; reader: Reader; components: { variable: string; cssFile: string; jsFile: string; };}
/** * Scan and load files from the source folder * with the data, pages, assets and static files */export default class Source { /** The root of the src directory */ root?: Directory;
/** Filesystem reader to scan folders */ reader: Reader;
/** Global data to be assigned to the root folder */ globalData: Data;
/** Global components to be assigned to the root folder */ globalComponents: Components;
/** To load all _data files */ dataLoader: DataLoader;
/** To load all pages */ pageLoader: PageLoader;
/** To load all components */ componentLoader: ComponentLoader;
/** Info about how to handle different file formats */ formats: Formats;
/** The list of paths to ignore */ ignored = new Set<string>();
/** The path filters to ignore */ filters: ScopeFilter[] = [];
/** List of static files and folders to copy */ staticPaths = new Map< string, string | ((path: string) => string) | undefined >();
/** Extra code generated by the components */ extraCode = new Map<string, Map<string, string>>();
components: { /** File name used to output the extra CSS code generated by the components */ cssFile: string;
/** File name used to output the extra JavaScript code generated by the components */ jsFile: string;
/** Variable name used to access to the components */ variable: string; };
constructor(options: Options) { this.pageLoader = options.pageLoader; this.dataLoader = options.dataLoader; this.componentLoader = options.componentLoader; this.reader = options.reader; this.formats = options.formats; this.components = options.components; this.globalData = options.globalData; this.globalComponents = options.globalComponents; }
addIgnoredPath(path: string) { this.ignored.add(normalizePath(path)); }
addIgnoreFilter(filter: ScopeFilter) { this.filters.push(filter); }
addStaticPath(from: string, to?: string | ((path: string) => string)) { this.staticPaths.set( normalizePath(from), typeof to === "string" ? normalizePath(to) : to, ); }
/** Returns all pages found */ getPages(...filters: ((page: Page) => boolean)[]): Page[] { if (!this.root) { return []; }
return [...this.root.getPages()].filter((page) => filters.every((filter) => filter(page)) ); }
/** Returns all static files found */ getStaticFiles(...filters: ((file: StaticFile) => boolean)[]): StaticFile[] { if (!this.root) { return []; }
return [...this.root.getStaticFiles()].filter((page) => filters.every((filter) => filter(page)) ); }
/** Returns the pages with extra code generated by the components */ getComponentsExtraCode(): Page[] { const files = { css: this.components.cssFile, js: this.components.jsFile, }; const pages: Page[] = [];
for (const [type, path] of Object.entries(files)) { const code = this.extraCode.get(type);
if (code && code.size) { pages.push(Page.create(path, Array.from(code.values()).join("\n"))); } }
return pages; }
/** Load all sources */ async load() { const [root] = await this.#getOrCreateDirectory("/");
return concurrent( this.reader.readDir(root.src.path), (entry) => this.#loadEntry(root, entry), ); }
/** Update a file */ async update(file: string): Promise<void> { // Check if the file should be ignored for (const path of this.ignored) { if (file === path || file.startsWith(path + "/")) { return; } }
if (this.filters.some((filter) => filter(file))) { return; }
// It's a static file for (const entry of this.staticPaths) { const [src, dest] = entry;
if (file === src || file.startsWith(src + "/")) { const [directory] = await this.#getOrCreateDirectory( posix.dirname(src), );
// It's a static file previously copied for (const staticFile of directory.staticFiles) { if (staticFile.src === file) { delete staticFile.saved; const info = await this.reader.getInfo(file); staticFile.removed = !info; staticFile.remote = info?.remote; return; } }
// It's a new static file if (typeof dest === "string") { directory.setStaticFile({ src: file, dest: posix.join(dest, file.slice(src.length)), }); } else { const output = posix.join( directory.dest.path, file.slice(directory.src.path.length), ); directory.setStaticFile({ src: file, dest: dest ? dest(output) : output, }); }
return; } }
// It's a _data or _component file const match = file.match(/(.*)\/(_data\/|_components\/|_data\.\w+$)/);
if (match) { const [directory, created] = await this.#getOrCreateDirectory(match[1]);
if (!created) { await this.#loadDirectory(directory); } return; }
// Any path segment starting with _ or . if (file.includes("/_") || file.includes("/.")) { return; }
// Default return await this.#updateFile(file); }
/** Return the File or Directory of a path */ getFileOrDirectory(path: string): Directory | Page | undefined { let result: Directory | Page | undefined = this.root;
path.split("/").forEach((name) => { if (!name || !result) { return; }
if (result instanceof Directory) { result = result.dirs.get(name) || result.pages.get(name); } });
return result; }
/** Reloads a file */ async #updateFile(file: string) { const info = await this.reader.getInfo(file); const entry = { name: posix.basename(file), isFile: true, isDirectory: false, isSymlink: false, remote: info?.remote, }; const [directory] = await this.#getOrCreateDirectory(posix.dirname(file)); await this.#loadEntry(directory, entry); }
/** Get an existing directory or create it if it doesn't exist */ async #getOrCreateDirectory(path: string): Promise<[Directory, boolean]> { let dir: Directory; let created = false;
if (this.root) { dir = this.root; } else { dir = this.root = new Directory({ path: "/" }); await this.#loadDirectory(dir); }
for (const name of path.split("/")) { if (!name) { continue; }
if (dir.dirs.has(name)) { dir = dir.dirs.get(name)!; continue; }
dir = dir.createDirectory(name); await this.#loadDirectory(dir); created = true; }
return [dir, created]; }
/** Load an entry from a directory */ async #loadEntry(directory: Directory, entry: DirEntry) { if (entry.isSymlink) { return; }
const path = posix.join(directory.src.path, entry.name);
// It's a static file/folder if (this.staticPaths.has(path)) { await this.#loadStaticFiles(directory, entry); return; }
// Ignore .filename and _filename if (entry.name.startsWith(".") || entry.name.startsWith("_")) { return; }
// Check if the file should be ignored if (this.ignored.has(path)) { return; }
if (this.filters.some((filter) => filter(path))) { return; }
if (entry.isFile) { const format = this.formats.search(path);
if (!format) { return; }
// The file is a static file if (format.copy) { const output = posix.join(directory.dest.path, entry.name);
directory.setStaticFile({ src: path, dest: typeof format.copy === "function" ? format.copy(output) : output, remote: entry.remote, }); return; }
// The file is a page (a loadable file) if (format.pageLoader) { const page = (await this.pageLoader.load(path, format));
if (page) { directory.setPage(entry.name, page); } else { directory.unsetPage(entry.name); } } return; }
if (entry.isDirectory) { const [subDirectory] = await this.#getOrCreateDirectory(path); await concurrent( this.reader.readDir(subDirectory.src.path), (entry) => this.#loadEntry(subDirectory, entry), ); return; } }
/** Read the static files in a directory */ async #loadStaticFiles(directory: Directory, entry: DirEntry) { const src = posix.join(directory.src.path, entry.name);
if (!this.staticPaths.has(src)) { return; }
await this.#scanStaticFiles( directory, entry, src, this.staticPaths.get(src), ); }
async #scanStaticFiles( directory: Directory, entry: DirEntry, src: string, dest?: string | ((file: string) => string), ) { if (entry.isSymlink) { return; }
// It's a static file/folder if (this.staticPaths.has(src)) { dest = this.staticPaths.get(src); } else if (entry.name.startsWith(".") || entry.name.startsWith("_")) { return; }
// Check if the file should be ignored if (this.ignored.has(src)) { return; }
if (this.filters.some((filter) => filter(src))) { return; }
if (entry.isFile) { if (typeof dest === "string") { directory.setStaticFile({ src, dest, remote: entry.remote }); } else { const output = posix.join( directory.dest.path, src.slice(directory.src.path.length), ); directory.setStaticFile({ src, dest: dest ? dest(output) : output, remote: entry.remote, }); } return; }
if (entry.isDirectory) { for await (const entry of this.reader.readDir(src)) { await this.#scanStaticFiles( directory, entry, posix.join(src, entry.name), typeof dest === "string" ? posix.join(dest, entry.name) : dest, ); } } }
/** Load the _data and components inside a directory */ async #loadDirectory(directory: Directory) { const data: Data = {};
// Assign the global data and components to the root directory if (directory.src.path === "/") { Object.assign(data, this.globalData); directory.components = this.globalComponents; }
await concurrent( this.reader.readDir(directory.src.path), async (entry) => { const path = posix.join(directory.src.path, entry.name);
if (this.ignored.has(path)) { return; } if (this.filters.some((filter) => filter(path))) { return; }
// Load the _data files if (entry.name === "_data" || /^_data\.\w+$/.test(entry.name)) { const dataFile = await this.dataLoader.load(path); Object.assign(data, dataFile); }
// Load the _components files if (entry.isDirectory && entry.name === "_components") { await this.componentLoader.load(path, directory); } }, );
// Setup the components const components = directory.getComponents();
if (components?.size) { data[this.components.variable] = toProxy(components, this.extraCode); }
Object.assign(directory.baseData, data); directory.refreshCache(); }}
/** * Create and returns a proxy to use the components * as comp.name() instead of components.get("name").render() */function toProxy( components: Components, extraCode?: Map<string, Map<string, string>>,): ProxyComponents { const node = { _components: components, _proxies: new Map(), }; return new Proxy(node, { get: (target, name) => { if (typeof name !== "string" || name in target) { return; }
const key = name.toLowerCase();
if (target._proxies.has(key)) { return target._proxies.get(key); }
const component = target._components.get(key);
if (!component) { throw new Error(`Component "${name}" not found`); }
if (component instanceof Map) { const proxy = toProxy(component, extraCode); target._proxies.set(key, proxy); return proxy; }
// Save CSS & JS code for the component if (extraCode) { if (component.css) { const code = extraCode.get("css") ?? new Map(); code.set(key, component.css); extraCode.set("css", code); }
if (component.js) { const code = extraCode.get("js") ?? new Map(); code.set(key, component.js); extraCode.set("js", code); } }
// Return the function to render the component return (props: Record<string, unknown>) => component.render(props); }, }) as unknown as ProxyComponents;}
export type ComponentFunction = (props: Record<string, unknown>) => string;
export interface ProxyComponents { [key: string]: ComponentFunction | ProxyComponents;}