Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/axiomcode
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ case "$cmd" in
case "$which" in
java|typescript|python|javascript|csharp) run_suite "$which" "$@";;
web) echo "══ web: cli smoke + violation checks + include checks"; bash "$ROOT/graph/test/web/cli-smoke.sh" || rc=1; bash "$ROOT/graph/test/web/violations.sh" || rc=1; bash "$ROOT/graph/test/web/includes.sh" || rc=1; run_suite web "$@";;
parser) for t in java-extractor-tests typescript-tests python-tests javascript-tests csharp-tests gradle-tests services-tests web-tests web-torture web-plumbing-tests discovery-tests; do
parser) for t in java-extractor-tests typescript-tests deno-tests python-tests javascript-tests csharp-tests gradle-tests services-tests web-tests web-torture web-plumbing-tests discovery-tests; do
[ -f "$ROOT/parser/src/test/$t.ts" ] || continue; echo "══ parser: $t"; (cd "$ROOT/parser" && npx tsx "src/test/$t.ts") || rc=1; done;;
all) for l in java typescript python javascript csharp web; do [ -f "$ROOT/graph/test/$l/run-tests.sh" ] && run_suite "$l" "$@"; done; bash "$ROOT/graph/test/web/cli-smoke.sh" || rc=1; "$0" test parser || rc=1;;
*) die "test: unknown suite $which (java | typescript | python | javascript | csharp | web | parser | all)";;
Expand Down
122 changes: 122 additions & 0 deletions parser/src/deno-libraries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* The library roots a Deno project's imports reach, for `--library auto`:
*
* node dist/deno-libraries.js <repo>
*
* prints one JSON object, `{"mirrors": ["<dir>", …], "packages": ["<dir>", …]}`:
*
* mirrors each Deno project's mirror, filled by resolving every import: each
* `jsr:` and URL module the project reaches, and what those import,
* copied from Deno's cache as `vendor/` lays them out
* packages every npm package directory an `npm:` import resolved into, in
* Deno's npm cache or `node_modules` (real paths, because Deno's
* `node_modules` entries are symlinks into `node_modules/.deno`)
*
* Library discovery runs BEFORE the project is parsed, so the mirror has to be
* filled here rather than as a side effect of the parse. Resolution is the same
* code the parser uses, so the files staged are the files the imports name.
* A repository with no Deno config prints `{"mirrors": [], "packages": []}`.
*/
import * as fs from 'fs';
import * as path from 'path';

import { configIn, denoContextForDir, isDenoVendorDirectory } from '@/parsers/typescript/deno/deno-config';
import { mirrorRoot } from '@/parsers/typescript/deno/deno-cache';
import { moduleSpecifiersOf, resolveDenoSpecifier } from '@/parsers/typescript/deno/deno-resolver';

const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'coverage', '.axiomcode']);
const SOURCE = /\.(tsx?|mts|cts|jsx?|mjs|cjs)$/;

function sourceFiles(root: string): string[] {
const out: string[] = [];
const walk = (dir: string): void => {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!SKIP.has(entry.name) && !entry.name.startsWith('.') && !isDenoVendorDirectory(dir, entry.name)) {
walk(full);
}
} else if (SOURCE.test(entry.name) && !entry.name.endsWith('.d.ts')) {
out.push(full);
}
}
};
walk(root);
return out;
}

/**
* `<…>/zod/3.25.76` for a file inside it: the nearest ancestor whose package.json
* names `packageName`. Nearest alone is not enough, because a package can ship a
* stub package.json per subpath (`zod/v4/package.json`).
*/
function packageDirOf(file: string, packageName: string): string | undefined {
let dir = path.dirname(file);
for (;;) {
const manifest = path.join(dir, 'package.json');
if (fs.existsSync(manifest)) {
let name: unknown;
try {
name = (JSON.parse(fs.readFileSync(manifest, 'utf-8')) as { name?: unknown }).name;
} catch {
name = undefined;
}
if (name === packageName) {
try {
return fs.realpathSync(dir);
} catch {
return dir;
}
}
}
const parent = path.dirname(dir);
if (parent === dir) {
return undefined;
}
dir = parent;
}
}

function main(): number {
const repo = path.resolve(process.argv[2] ?? '.');
const mirrors = new Set<string>();
const packages = new Set<string>();
for (const file of sourceFiles(repo)) {
const context = denoContextForDir(path.dirname(file));
if (context === undefined) {
continue;
}
let text: string;
try {
text = fs.readFileSync(file, 'utf-8');
} catch {
continue;
}
for (const specifier of moduleSpecifiersOf(text, file)) {
const resolved = resolveDenoSpecifier(specifier, file);
if (resolved === undefined) {
continue;
}
if (resolved.kind === 'JSR' || resolved.kind === 'REMOTE') {
mirrors.add(mirrorRoot(context.root));
} else if (resolved.kind === 'NPM_CACHE' || resolved.kind === 'NODE_MODULES') {
const dir = packageDirOf(resolved.resolvedFileName, resolved.packageName);
if (dir !== undefined) {
packages.add(dir);
}
}
}
}
// A mirror exists once something was copied into it; one that stayed empty stages nothing.
const filled = [...mirrors].filter((m) => configIn(m) !== undefined).sort();
process.stdout.write(`${JSON.stringify({ mirrors: filled, packages: [...packages].sort() })}\n`);
return 0;
}

process.exit(main());
12 changes: 11 additions & 1 deletion parser/src/enums/typescript/imports/TsImportResolutionKind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
export enum TsImportResolutionKind {
/** A relative specifier that resolved to a file. */
RELATIVE_FILE = 'RELATIVE_FILE',
/** Resolved through a tsconfig `paths` mapping. */
/** Resolved through a tsconfig `paths` mapping, or a Deno import-map entry onto a project file. */
PATHS_ALIAS = 'PATHS_ALIAS',
/** Resolved to a `.d.ts` inside `node_modules`. */
NODE_MODULES_TYPES = 'NODE_MODULES_TYPES',
Expand All @@ -46,6 +46,16 @@ export enum TsImportResolutionKind {
* source its `main` / `types` / `exports` entry is built from.
*/
WORKSPACE_PACKAGE = 'WORKSPACE_PACKAGE',
/**
* A `jsr:` package (directly, or through a Deno import map), resolved to the
* version the lockfile pins and the file its `exports` names. The path is the
* module's copy in the project's Deno mirror, laid out as `vendor/` is.
*/
JSR_PACKAGE = 'JSR_PACKAGE',
/** An `npm:` package resolved inside Deno's own npm cache rather than `node_modules`. */
DENO_NPM_CACHE = 'DENO_NPM_CACHE',
/** An `https:` module a Deno project imports by URL, as downloaded (redirects and `x-typescript-types` followed). */
REMOTE_MODULE = 'REMOTE_MODULE',
/** Matched a `declare module "x"` in this analysis. No file, and none needed. */
AMBIENT_MODULE = 'AMBIENT_MODULE',
/** A Node builtin, with or without the `node:` prefix. */
Expand Down
5 changes: 5 additions & 0 deletions parser/src/language-detectors/typescript-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export class TypeScriptDetector implements LanguageDetector {
'tsconfig.json',
'tsconfig.base.json',
'jsconfig.json',
// A Deno project's config defines its module graph the way a tsconfig does, and a
// workspace root's lists members that import one another: one project, not one per member.
'deno.json',
'deno.jsonc',
];

private static readonly SKIP = new Set<string>(TS_SKIP_DIRECTORIES);
Expand All @@ -62,6 +66,7 @@ export class TypeScriptDetector implements LanguageDetector {
async detectBuildSystem(projectPath: string): Promise<string | undefined> {
try {
const files = await fs.readdir(projectPath);
if (files.includes('deno.json') || files.includes('deno.jsonc')) return 'Deno';
if (files.includes('pnpm-lock.yaml')) return 'pnpm';
if (files.includes('yarn.lock')) return 'Yarn';
if (files.includes('bun.lockb') || files.includes('bun.lock')) return 'Bun';
Expand Down
Loading
Loading