Public
Val Town-native dependency analyzer for small Val projects
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

Val Dependency Cruiser

A small Val Town-native dependency analyzer inspired by dependency-cruiser, focused on reading and visualizing dependencies between Vals and small Val Town projects.

Goal

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.

HTTP endpoint

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.

Current API

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:

  • /sample for example JSON
  • /sample/mermaid for 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>;

Import forms to support first

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");

Dependency classification

  • local — relative project import like ./foo.ts or ../lib/bar.ts
  • val — Val Town URL/import like https://esm.town/v/user/valName
  • npmnpm:package or bare npm package imports if Val Town allows/normalizes them
  • jsrjsr:@scope/package
  • url — other http:/https: imports
  • builtinnode:fs, node:path, etc.
  • unresolved — local-looking import that does not match known files

Likely implementation

Use parser-level extraction instead of regexes.

Potential libraries:

  • npm:acorn
  • npm:acorn-walk
  • npm:acorn-jsx if JSX is needed
  • Possibly npm:@typescript-eslint/typescript-estree or npm:oxc-parser later for better TypeScript support

Initial extraction can be small:

  1. Parse source as module, falling back to loose/script mode if needed.
  2. Walk AST.
  3. Collect:
    • ImportDeclaration
    • ExportNamedDeclaration.source
    • ExportAllDeclaration.source
    • ImportExpression with string literal source
    • CallExpression where callee is require and first arg is a string literal
  4. Resolve/classify each specifier.
  5. Build adjacency list.
  6. Run DFS/Tarjan to find cycles.
  7. Emit Mermaid.

Resolution rules for MVP

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.

Example Mermaid output

Rendering mermaid diagram...

Later features

  • 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
  • focus mode: 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.

Estimated effort

  • MVP analyzer: 1–2 days
  • Useful Val Town integration and nicer output: 3–5 days
  • Rule system and richer reporting: 1–2 weeks

Open questions

  • 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?