core/analyzers/asset.analyzer.ts
core/analyzers/asset.analyzer.ts is a file in Bane's Lab Build Scripts. 123 lines of code and 40 definitions.
import { REFERENCE_DELIMITERS, REFERENCE_ENDS, TEXT_EXTENSIONS } from "#configuration/constants/asset.constants";
import { extensionOf, stripCompression, toPosix } from "#core/resolvers/asset.resolver";
import { posix, resolve } from "node:path";
import { isServed } from "#core/predicates/asset.predicate";
import { readFileSync } from "node:fs";
import { walk } from "#core/loaders/asset.loader";
const SLASH = "/";
const SIBLING_STEP = "./";
const PARENT_STEP = "../";
const ASCII = 128;
const TEXT_END = -1;
const codeTable = function codeTable(characters: ReadonlySet<string>): Uint8Array {
const table = new Uint8Array(ASCII);
for (const character of characters) {
table[character.codePointAt(0) ?? 0] = 1;
}
return table;
};
const DELIMITERS = codeTable(REFERENCE_DELIMITERS);
const ENDS = codeTable(REFERENCE_ENDS);
const SLASH_CODE = SLASH.codePointAt(0) ?? 0;
const isIn = function isIn(table: Uint8Array, code: number): boolean {
return code >= 0 && code < ASCII && table[code] === 1;
};
const nameOf = function nameOf(path: string): string {
return path.slice(path.lastIndexOf(SLASH) + 1);
};
const referencesPath = function referencesPath(path: string, from: string, candidate: string): boolean {
if (path.startsWith(SIBLING_STEP) || path.startsWith(PARENT_STEP)) {
return posix.normalize(posix.join(posix.dirname(from), path)) === candidate;
}
const bare = path.startsWith(SLASH) ? path.slice(1) : path;
return bare === candidate || path.endsWith(SLASH + candidate) || candidate.endsWith(SLASH + bare);
};
interface Token {
readonly start: number;
readonly lastSlash: number;
readonly pathEnd: number;
}
const tokenFrom = function tokenFrom(start: number): Token {
return { lastSlash: TEXT_END, pathEnd: TEXT_END, start };
};
const codeAt = function codeAt(text: string, index: number): number {
return index === text.length ? TEXT_END : (text.codePointAt(index) ?? TEXT_END);
};
const tokenReferences = function tokenReferences(
text: string,
token: Token,
index: number,
target: { readonly from: string; readonly byName: ReadonlyMap<string, readonly string[]> },
): string[] {
const end = token.pathEnd === TEXT_END ? index : token.pathEnd;
const nameStart = token.lastSlash === TEXT_END ? token.start : token.lastSlash + 1;
const candidates = end > nameStart ? target.byName.get(text.slice(nameStart, end)) : undefined;
if (candidates === undefined) {
return [];
}
const path = text.slice(token.start, end);
return candidates.filter((held) => referencesPath(path, target.from, held));
};
const extended = function extended(token: Token, code: number, index: number): Token {
if (token.pathEnd !== TEXT_END) {
return token;
}
if (code === SLASH_CODE) {
return { ...token, lastSlash: index };
}
return isIn(ENDS, code) ? { ...token, pathEnd: index } : token;
};
export const referencesIn = function referencesIn(
text: string,
from: string,
byName: ReadonlyMap<string, readonly string[]>,
): Set<string> {
const found = new Set<string>();
const target = { byName, from };
let token = tokenFrom(0);
for (let index = 0; index <= text.length; index += 1) {
const code = codeAt(text, index);
if (code === TEXT_END || isIn(DELIMITERS, code)) {
for (const candidate of tokenReferences(text, token, index, target)) {
found.add(candidate);
}
token = tokenFrom(index + 1);
} else {
token = extended(token, code, index);
}
}
return found;
};
const namesOf = function namesOf(files: Iterable<string>): Map<string, string[]> {
const byName = new Map<string, string[]>();
for (const file of files) {
const name = nameOf(file);
byName.set(name, [...(byName.get(name) ?? []), file]);
}
return byName;
};
const reachable = function reachable(outDir: string, files: readonly string[]): Set<string> {
const plain = new Set(files.map(stripCompression));
const kept = new Set([...plain].filter(isServed));
const pending = new Set([...plain].filter((file) => !kept.has(file)));
const byName = namesOf(pending);
const queue = [...kept];
while (queue.length > 0 && pending.size > 0) {
const file = queue.pop();
if (file === undefined || !TEXT_EXTENSIONS.has(extensionOf(file))) {
continue;
}
for (const reference of referencesIn(readFileSync(resolve(outDir, file), "utf8"), file, byName)) {
if (pending.delete(reference)) {
kept.add(reference);
queue.push(reference);
}
}
}
return kept;
};
export const unreferencedFiles = function unreferencedFiles(outDir: string): string[] {
const files = walk(outDir, outDir).map(toPosix);
const kept = reachable(outDir, files);
return files.filter((file) => !kept.has(stripCompression(file))).sort((a, b) => a.localeCompare(b));
};