# core/analyzers/asset.analyzer.ts

> 123 lines of code and 40 definitions.

Tree: Bane's Lab Build Scripts
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/build-scripts#file-build-scripts-core-analyzers-asset-analyzer-ts
Source text: https://banes-lab.com/source/build-scripts/core/analyzers/asset.analyzer.ts.txt

Listed in [core/analyzers](https://banes-lab.com/api/source/build-scripts/core/analyzers.md), before [core/analyzers/graph.analyzer.ts](https://banes-lab.com/source/build-scripts/core/analyzers/graph.analyzer.ts.md).

## Definitions

- `isIn` (lexical_declaration, line 26)
- `referencesIn` (lexical_declaration, line 82, exported)
- `tokenReferences` (lexical_declaration, line 56)
- `extended` (lexical_declaration, line 72)
- `unreferencedFiles` (lexical_declaration, line 134, exported)
- `referencesPath` (lexical_declaration, line 34)
- `tokenFrom` (lexical_declaration, line 48)
- `reachable` (lexical_declaration, line 113)
- `SLASH` (lexical_declaration, line 8)
- `SIBLING_STEP` (lexical_declaration, line 9)
- `PARENT_STEP` (lexical_declaration, line 10)
- `ASCII` (lexical_declaration, line 11)
- `TEXT_END` (lexical_declaration, line 12)
- `codeTable` (lexical_declaration, line 14)
- `table` (lexical_declaration, line 15)
- `DELIMITERS` (lexical_declaration, line 22)
- `ENDS` (lexical_declaration, line 23)
- `SLASH_CODE` (lexical_declaration, line 24)
- `nameOf` (lexical_declaration, line 30)
- `bare` (lexical_declaration, line 38)
- `Token` (interface_declaration, line 42)
- `codeAt` (lexical_declaration, line 52)
- `end` (lexical_declaration, line 62)
- `nameStart` (lexical_declaration, line 63)
- `candidates` (lexical_declaration, line 64)
- `path` (lexical_declaration, line 68)
- `found` (lexical_declaration, line 87, exported)
- `target` (lexical_declaration, line 88, exported)
- `token` (lexical_declaration, line 89, exported)
- `index` (lexical_declaration, line 90, exported)
- `code` (lexical_declaration, line 91, exported)
- `namesOf` (lexical_declaration, line 104)
- `name` (lexical_declaration, line 107)
- `plain` (lexical_declaration, line 114)
- `pending` (lexical_declaration, line 116)
- `byName` (lexical_declaration, line 117)
- `queue` (lexical_declaration, line 118)
- `file` (lexical_declaration, line 120)
- `files` (lexical_declaration, line 135, exported)
- `kept` (lexical_declaration, line 136, exported)

## Contained in

- [core/analyzers](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-analyzers.md)

## Uses

- [core/resolvers/asset.resolver.ts](https://banes-lab.com/source/build-scripts/core/resolvers/asset.resolver.ts.md)

## Used by

- [core/coordinators/validation.coordinator.ts](https://banes-lab.com/source/build-scripts/core/coordinators/validation.coordinator.ts.md)

## Linked from

- [core/analyzers](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-analyzers.md)
- [core/coordinators](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-coordinators.md)
- [core/resolvers](https://banes-lab.com/anatomy/build-scripts/folder-build-scripts-core-resolvers.md)

## Source

```typescript
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));
};
```
