115 lines
3.3 KiB
JavaScript
115 lines
3.3 KiB
JavaScript
import {createHash} from 'node:crypto';
|
|
import {readFile, realpath} from 'node:fs/promises';
|
|
|
|
import chokidar from 'chokidar';
|
|
import {glob} from 'glob';
|
|
|
|
import {singleton} from './singleton.js';
|
|
|
|
const {
|
|
RESOURCES_PATH = [process.cwd(), 'resources'].join('/'),
|
|
} = process.env;
|
|
|
|
const resources = singleton('resources', {});
|
|
const manifest = singleton('manifest', {});
|
|
const RESOURCES_PATH_REAL = await realpath(RESOURCES_PATH);
|
|
const RESOURCES_GLOB = [RESOURCES_PATH_REAL, '**', '*'].join('/');
|
|
|
|
async function computeAsset(path) {
|
|
const buffer = await readFile(path);
|
|
return {
|
|
asset: buffer,
|
|
hash: createHash('sha1').update(buffer).digest(),
|
|
path: path.slice(RESOURCES_PATH_REAL.length + 1),
|
|
};
|
|
}
|
|
|
|
async function assetPaths() {
|
|
return glob(RESOURCES_GLOB, {nodir: true});
|
|
}
|
|
|
|
export async function createManifest() {
|
|
const paths = await assetPaths();
|
|
const entries = await Promise.all(paths.map(async (path) => {
|
|
const asset = await computeAsset(path);
|
|
return [asset.path, asset.hash.toString('hex')];
|
|
}))
|
|
return Object.fromEntries(
|
|
entries.filter(Boolean),
|
|
);
|
|
}
|
|
|
|
export async function loadManifest() {
|
|
emptyCheck: {
|
|
for (const path in manifest) { // eslint-disable-line no-unused-vars
|
|
break emptyCheck;
|
|
}
|
|
const created = await createManifest();
|
|
for (const path in created) {
|
|
manifest[path] = created[path];
|
|
}
|
|
const watcher = chokidar.watch(RESOURCES_GLOB);
|
|
watcher.on('add', async (path) => {
|
|
const asset = await computeAsset(path);
|
|
manifest[asset.path] = asset.hash.toString('hex');
|
|
resources[asset.path] = {asset: asset.asset.buffer, hash: asset.hash.buffer};
|
|
});
|
|
watcher.on('change', async (path) => {
|
|
const asset = await computeAsset(path);
|
|
manifest[asset.path] = asset.hash.toString('hex');
|
|
resources[asset.path] = {asset: asset.asset.buffer, hash: asset.hash.buffer};
|
|
});
|
|
watcher.on('unlink', async (path) => {
|
|
const assetPath = path.slice(RESOURCES_PATH_REAL.length + 1);
|
|
delete resources[assetPath];
|
|
delete manifest[assetPath];
|
|
});
|
|
}
|
|
return manifest;
|
|
}
|
|
|
|
export async function encodeResources(resources) {
|
|
let byteLength = 4;
|
|
for (const {asset} of resources) {
|
|
byteLength += 20;
|
|
byteLength += 4;
|
|
byteLength += asset.byteLength;
|
|
}
|
|
const buffer = new ArrayBuffer(byteLength);
|
|
const view = new DataView(buffer);
|
|
let caret = 0;
|
|
view.setUint32(caret, resources.length);
|
|
caret += 4;
|
|
for (const {asset, hash} of resources) {
|
|
const hashView = new DataView(hash);
|
|
for (let j = 0; j < 20; ++j) {
|
|
view.setUint8(caret, hashView.getUint8(j));
|
|
caret += 1;
|
|
}
|
|
view.setUint32(caret, asset.byteLength);
|
|
caret += 4;
|
|
const assetView = new DataView(asset);
|
|
for (let j = 0; j < asset.byteLength; ++j) {
|
|
view.setUint8(caret, assetView.getUint8(j));
|
|
caret += 1;
|
|
}
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
export async function loadResources() {
|
|
emptyCheck: {
|
|
for (const path in resources) { // eslint-disable-line no-unused-vars
|
|
break emptyCheck;
|
|
}
|
|
const paths = await assetPaths();
|
|
await Promise.all(
|
|
paths.map(async (path) => {
|
|
const {asset, hash, path: assetPath} = await computeAsset(path);
|
|
resources[assetPath] = {asset: asset.buffer, hash: hash.buffer};
|
|
}),
|
|
);
|
|
}
|
|
return resources;
|
|
}
|