health-code-simulator/buildrunner.mjs

224 lines
5.1 KiB
JavaScript

import { globby } from 'globby';
import micromatch from 'micromatch';
import { readFile, writeFile, copyFile, mkdir } from 'node:fs/promises';
import path from 'node:path';
import chokidar from 'chokidar';
import { createServer } from 'http-server';
const tasks = [];
const STAGED_KEEP = Symbol();
class TaskRegister {
constructor(src) {
tasks.push({ src });
this.index = tasks.length - 1;
}
transform(cb) {
tasks[this.index].transform = cb;
return this;
}
staged(val) {
tasks[this.index].staged = val || STAGED_KEEP;
return this;
}
}
export function task(src) {
return new TaskRegister(src);
}
function transformStagedFilename(staged, file) {
if(staged === STAGED_KEEP) {
return file;
} else if(typeof staged === 'string' || staged instanceof String) {
return staged.toString();
} else if(typeof staged === 'function') {
return staged(file);
}
console.error('unrecognized staged type', typeof(staged));
}
export function replaceExtension(file, newExtension) {
return path.parse(file).name + newExtension;
}
const history = new Map();
const watches = new Map();
let basePath = 'dist/';
async function getFileContent(file) {
if(history.has(file)) {
const item = history.get(file);
if(item.content != null) {
return item.content;
}
}
return await readFile(file);
}
function getHistory(file) {
if(!history.has(file)) history.set(file, {});
return history.get(file);
}
function getWatch(file) {
if(!watches.has(file)) watches.set(file, {
/**
* task indexes
*/
passes: [],
/**
* use another file as task entry
*/
entry: '',
/**
* execute passes and redirect to another file
*/
redirect: '',
});
return watches.get(file);
}
class TaskContext {
constructor(index, file) {
this.index = index;
this.file = file;
}
file() { return this.file; }
path() { return path.dirname(this.file); }
watch(file, { markAsBuilt = false } = {}) {
const files = (Array.isArray(file) ? file : files).map(f => path.relative('.', f));
for(const f of files) {
getWatch(f).entry = this.file;
getWatch(f).passes.push(this.index);
if(markAsBuilt) {
getHistory(f).built = true;
console.log(`[-] dependency ${f} marked as built`);
}
}
}
}
async function doTask(index, file, addToWatch) {
const task = tasks[index];
const { dir } = path.parse(file);
const destinationFolder = dir.replace(dir.split('/')[0], basePath);
await mkdir(destinationFolder, { recursive: true });
const outFile = path.join(destinationFolder, path.basename(file));
const isDirectCopy = !task.staged && !task.transform && getHistory(file).content == null;
if(isDirectCopy) {
await copyFile(file, outFile);
console.log(`[>] ${file}${outFile}`);
return;
}
let content = await getFileContent(file);
if(task.transform) {
const ctx = new TaskContext(index, file);
content = task.transform(content, ctx);
}
if(task.staged) {
const newFile = transformStagedFilename(task.staged, file);
getHistory(newFile).built = false;
getHistory(newFile).content = content;
addToWatch && getWatch(file).passes.push(index);
if(newFile !== file) {
// set old file built so that it won't build again
getHistory(file).built = true;
addToWatch && (getWatch(file).redirect = newFile);
console.log(`[^] ${file}${newFile} staged`);
} else {
console.log(`[^] ${file} staged`);
}
} else {
await writeFile(outFile, content);
getHistory(file).built = true;
addToWatch && getWatch(file).passes.push(index);
console.log(`[+] ${file}${outFile}`);
}
}
export async function build(outPath) {
basePath = outPath;
history.clear();
watches.clear();
for(let i = 0; i < tasks.length; i++) {
const files = [...new Set(
(await globby(tasks[i].src)) // glob files
.concat(micromatch([...history.keys()], tasks[i].src)) // staged files
.filter(file => !getHistory(file).built)
)];
await Promise.all(files.map(file => doTask(i, file, true)));
}
console.log('[✓] build completed.');
}
async function onFileChanged(file) {
let curFile = file;
while(curFile) {
const watch = watches.get(curFile);
if(watch.entry) {
curFile = watch.entry;
continue;
}
for(let pass of watch.passes) {
await doTask(pass, curFile, false);
}
if(watch.redirect) {
curFile = watch.redirect;
continue;
}
curFile = '';
}
}
export function watch() {
// all built files and dependencies
chokidar.watch([...watches.keys()], { ignoreInitial: true })
.on('change', onFileChanged);
// watch new files
for(let i = 0; i < tasks.length; i++) {
chokidar.watch(tasks[i].src, { ignoreInitial: true })
.on('add', file => doTask(i, file, true));
}
console.log(`[ ] watching file changes`);
}
export function serve({ host = '0.0.0.0', port = 3001, https } = {}) {
createServer({
root: basePath,
https,
}).listen(port, host);
console.log(`[ ] server started on ${https ? 'https' : 'http'}://${host}:${port}`);
}