# rules/eslint/no-inline-asset-path.eslint.rule.ts

> 103 lines of code and 18 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-no-inline-asset-path-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/no-inline-asset-path.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/no-hardcoded-exclusions.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/no-hardcoded-exclusions.eslint.rule.ts.md) and before [rules/eslint/no-inline-icon-options.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/no-inline-icon-options.eslint.rule.ts.md).

## Definitions

- `isEnvBaseUrl` (lexical_declaration, line 55)
- `isImportMeta` (lexical_declaration, line 48)
- `isAllowedFile` (lexical_declaration, line 34)
- `create` (method_definition, line 67, exported)
- `literal` (method_definition, line 72, exported)
- `memberExpression` (method_definition, line 79, exported)
- `templateElement` (method_definition, line 84, exported)
- `CORE_CONTAINER` (lexical_declaration, line 26)
- `ASSET_MODULE_PREFIX` (lexical_declaration, line 27)
- `REGISTRY_SUFFIX` (lexical_declaration, line 28)
- `ENV_BASE_URL` (lexical_declaration, line 30)
- `ENV_NAMESPACE` (lexical_declaration, line 31)
- `BASE_URL_PROPERTY` (lexical_declaration, line 32)
- `norm` (lexical_declaration, line 35)
- `pathHintIn` (lexical_declaration, line 44)
- `object` (lexical_declaration, line 59)
- `value` (lexical_declaration, line 73, exported)
- `hint` (lexical_declaration, line 85, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Single Source of Truth](https://banes-lab.com/records/architecture/single-source-of-truth.md)
- [Hardcoded Configuration](https://banes-lab.com/records/architecture/hardcoded-configuration.md)

## Source

```typescript
import { ASSET_MODULE_NAME, ASSET_PATH_HINTS } from "../../shared/manifests/asset.manifest.ts";
import {
    BUILD_SCRIPT_ROOT_SEGMENT,
    GOVERNED_ROOT,
    SCRIPT_ROOT_SEGMENT,
    basenameOf,
    normalizePath,
} from "../../shared/resolvers/anchor.resolver.ts";
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import {
    isType,
    literalString,
    locOf,
    nameOf,
    nodeAt,
    recordAt,
    stringIn,
} from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { concernSuffix } from "../../shared/manifests/taxonomy.manifest.ts";
import { containerPath } from "../../shared/resolvers/container.resolver.ts";
import { defineCheck } from "@govlab/context/check";
import { isModuleSpecifier } from "../../shared/predicates/location.predicate.ts";
import { listener } from "../../shared/factories/listener.factory.ts";

const CORE_CONTAINER = containerPath("core", GOVERNED_ROOT);
const ASSET_MODULE_PREFIX = `/${CORE_CONTAINER.slice(CORE_CONTAINER.indexOf("/") + 1)}${ASSET_MODULE_NAME}/`;
const REGISTRY_SUFFIX = concernSuffix("manifest");

const ENV_BASE_URL = "import.meta.env.BASE_URL";
const ENV_NAMESPACE = "env";
const BASE_URL_PROPERTY = "BASE_URL";

const isAllowedFile = function isAllowedFile(filename: string): boolean {
    const norm = normalizePath(filename);
    return (
        norm.includes(ASSET_MODULE_PREFIX) ||
        norm.includes(SCRIPT_ROOT_SEGMENT) ||
        norm.includes(BUILD_SCRIPT_ROOT_SEGMENT) ||
        basenameOf(norm).endsWith(REGISTRY_SUFFIX)
    );
};

const pathHintIn = function pathHintIn(value: string): string | null {
    return ASSET_PATH_HINTS.find((hint) => value.includes(hint)) ?? null;
};

const isImportMeta = function isImportMeta(node: AstNode | null): boolean {
    if (!isType(node, "MetaProperty")) {
        return false;
    }
    return nameOf(nodeAt(node, "meta")) === "import" && nameOf(nodeAt(node, "property")) === "meta";
};

const isEnvBaseUrl = function isEnvBaseUrl(node: AstNode): boolean {
    if (node.type !== "MemberExpression" || nameOf(nodeAt(node, "property")) !== BASE_URL_PROPERTY) {
        return false;
    }
    const object = nodeAt(node, "object");
    if (!isType(object, "MemberExpression") || nameOf(nodeAt(object, "property")) !== ENV_NAMESPACE) {
        return false;
    }
    return isImportMeta(nodeAt(object, "object"));
};

export default {
    create(context: RuleContext): RuleListener {
        if (isAllowedFile(context.filename)) {
            return {};
        }
        return listener({
            literal(view) {
                const value = literalString(view);
                const hint = value === null || isModuleSpecifier(view) ? null : pathHintIn(value);
                if (hint !== null) {
                    context.report({ data: { hint }, loc: locOf(view), messageId: "pathLiteral" });
                }
            },
            memberExpression(view, node) {
                if (isEnvBaseUrl(view)) {
                    context.report({ data: { expr: ENV_BASE_URL }, messageId: "envBaseUrl", node });
                }
            },
            templateElement(view) {
                const hint = pathHintIn(stringIn(recordAt(view, "value"), "cooked"));
                if (hint !== null) {
                    context.report({ data: { hint }, loc: locOf(view), messageId: "pathLiteral" });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({
                detects: ["architecture:hardcoded-configuration"],
                enforces: ["architecture:single-source-of-truth"],
            }),
            description:
                "An asset location, an environment import, a URL or a route written as a literal at a call site is banned; each is declared once in the asset catalog and reached through its accessor. These are deployment facts, not code facts — they change on a different schedule than the code that consumes them, and a literal ties the two together. The location shapes the rule looks for are data in the asset manifest, and a registry module is exempt because it is where such a shape is declared.",
        },
        messages: {
            envBaseUrl:
                "Direct `{{expr}}` access is banned outside the asset catalog. The catalog's accessors apply the deployment base internally, so reaching for the environment here re-derives a fact the catalog already owns.",
            pathLiteral:
                "Inline location '{{hint}}' is banned outside the asset catalog. Declare it there once and reach it through the catalog's accessor — a deployment fact spelled at a call site changes on a different schedule than the code around it.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
