224 lines
5.1 KiB
JavaScript
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}`);
|
||
|
}
|