A small Val Town-native dependency analyzer inspired by
dependency-cruiser, focused
on reading and visualizing dependencies between Vals and small Val Town
projects.
Run as a Val that can read source for other Vals/projects, extract their imports, and produce a useful dependency graph plus a few simple checks.
This is not intended to port all of dependency-cruiser. Full
dependency-cruiser assumes a local Node project with filesystem crawling,
node_modules, package manifests, config loading, caches, and subprocess-based
graph rendering. Val Town projects are smaller and can use a simpler analyzer.
Current endpoint:
https://chadparker--87d29e86533511f18884ee650bb23af1.web.val.run
This endpoint is provided by api.http.ts, which re-exports the handler from
main.http.ts.
The Val exports analyzeVals(files) for direct use and also exposes an HTTP
handler.
import { analyzeVals } from "https://esm.town/v/chad/vt_dep";
const result = analyzeVals({
"main.http.ts": `import { helper } from "./helper.ts";`,
"helper.ts": `export const helper = 1;`,
});
HTTP:
POST /analyze content-type: application/json { "files": { "main.http.ts": "import { helper } from './helper.ts';", "helper.ts": "export const helper = 1;" } }
Output:
- dependency graph as JSON
- detected cycles
- unresolved imports
- parse errors
- external dependencies grouped by type
- Mermaid graph text for quick visualization
Try:
/samplefor example JSON/sample/mermaidfor example Mermaid output
Example API shape:
type SourceMap = Record<string, string>;
type DependencyKind =
| "local"
| "val"
| "npm"
| "jsr"
| "url"
| "builtin"
| "unresolved";
type Dependency = {
specifier: string;
resolved?: string;
kind: DependencyKind;
dynamic: boolean;
};
type ModuleNode = {
id: string;
dependencies: Dependency[];
};
type AnalysisResult = {
modules: ModuleNode[];
cycles: string[][];
unresolved: Dependency[];
externals: Dependency[];
mermaid: string;
};
export async function analyzeVals(files: SourceMap): Promise<AnalysisResult>;
Static ES modules:
import x from "./foo.ts";
import { helper } from "https://esm.town/v/chad/helper";
export { thing } from "./thing.ts";
Dynamic imports:
const mod = await import("./mod.ts");
Basic CommonJS, if encountered:
const lodash = require("npm:lodash");
local— relative project import like./foo.tsor../lib/bar.tsval— Val Town URL/import likehttps://esm.town/v/user/valNamenpm—npm:packageor bare npm package imports if Val Town allows/normalizes themjsr—jsr:@scope/packageurl— otherhttp:/https:importsbuiltin—node:fs,node:path, etc.unresolved— local-looking import that does not match known files
Use parser-level extraction instead of regexes.
Potential libraries:
npm:acornnpm:acorn-walknpm:acorn-jsxif JSX is needed- Possibly
npm:@typescript-eslint/typescript-estreeornpm:oxc-parserlater for better TypeScript support
Initial extraction can be small:
- Parse source as module, falling back to loose/script mode if needed.
- Walk AST.
- Collect:
ImportDeclarationExportNamedDeclaration.sourceExportAllDeclaration.sourceImportExpressionwith string literal sourceCallExpressionwhere callee isrequireand first arg is a string literal
- Resolve/classify each specifier.
- Build adjacency list.
- Run DFS/Tarjan to find cycles.
- Emit Mermaid.
Given files: Record<string, string>:
- Normalize module ids to POSIX-style paths.
- For relative imports, resolve from importer directory.
- Try exact match first.
- Then try common extensions:
.ts,.tsx,.js,.jsx,.mjs. - Then try
/index.ts,/index.tsx,/index.js, etc. - Non-relative imports are classified as external unless they match a Val Town URL.
Rendering mermaid diagram...
- Fetch files from Val Town APIs directly.
- Analyze one Val, a folder/project, or a user namespace.
- HTML response with interactive graph.
- Rule checks:
- no cycles
- no unresolved imports
- no private/external Val imports
- no npm imports outside allowlist
- no imports from specific users/namespaces
focusmode: show only neighbors around one Val/file.- Diff mode: compare dependency graph between two versions.
- Export dependency-cruiser-like JSON for compatibility with other tools.
- MVP analyzer: 1–2 days
- Useful Val Town integration and nicer output: 3–5 days
- Rule system and richer reporting: 1–2 weeks
- Which Val Town API endpoint should this use to read another Val/project's source?
- Should this analyze only the current user's Vals or public Vals too?
- Should output be a JSON API, an HTML page, or both?
- Should the Val store historical graphs for diffs?