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
4 changes: 3 additions & 1 deletion scripts/import-cycles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
import { dirname, join, relative, resolve, sep } from "node:path";

/**
Expand Down Expand Up @@ -74,6 +74,8 @@ export function staticImports(source: string, tsx = false): string[] {
}

export function scanGraph(root: string, sourceRoot = SOURCE_ROOT): ImportGraph {
// Bun resolves imports to physical paths; graph keys must use the same root (e.g. /var on macOS).
root = realpathSync(root);
const graph: ImportGraph = new Map();
for (const abs of listSources(join(root, sourceRoot)).sort()) {
const targets = new Set<string>();
Expand Down
3 changes: 2 additions & 1 deletion structure/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ still cover the rule, which is a judgement only review makes.
request-path entrypoints sit on no cycle at all. Type-only imports and dynamic `import()` form no
edge, so deferring an edge behind `import()` is a sanctioned way to break a loop (issue #6386).
Bun parses the declarations and resolves local file and directory imports, preserving compact
syntax, erased type-only specifiers, and the runtime's TypeScript index-file precedence.
syntax, erased type-only specifiers, and the runtime's TypeScript index-file precedence. The
scanner canonicalizes the root so symlinked roots and resolved imports share graph identities.
Enforced by `tests/ci-workflows/import-cycle-ratchet.test.ts`.
- **INV-RESTORE-01** — `ocx restore` restores native Codex from the pristine catalog with
retired bare/account-qualified native rows omitted from the output; the original backup stays
Expand Down
21 changes: 20 additions & 1 deletion tests/ci-workflows/import-cycle-ratchet.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import {
Expand Down Expand Up @@ -69,6 +69,25 @@ describe("import-cycle ratchet: scanner", () => {
expect(scanGraph(root).get("src/a.ts")).toEqual([`src/nested/${resolved}`]);
});

test.each(["ts", "tsx", "mts"])("symlinked roots preserve index.%s cycle identities", extension => {
const base = mkdtempSync(join(tmpdir(), "ocx-import-symlink-"));
const physical = join(base, "physical");
const root = join(base, "alias");
try {
mkdirSync(join(physical, "src", "nested"), { recursive: true });
writeFileSync(join(physical, "src", "a.ts"), 'import { b } from "./nested"; export const a = b;');
writeFileSync(join(physical, "src", "nested", `index.${extension}`),
'import { a } from "../a"; export const b = a;' + (extension === "tsx" ? " export const view = <span/>;" : ""));
symlinkSync(physical, root, process.platform === "win32" ? "junction" : "dir");
const graph = scanGraph(root);
expect(graph.get("src/a.ts")).toEqual([`src/nested/index.${extension}`]);
expect(graph.get(`src/nested/index.${extension}`)).toEqual(["src/a.ts"]);
expect(cyclicModules(graph)).toEqual(["src/a.ts", `src/nested/index.${extension}`]);
} finally {
rmSync(base, { recursive: true, force: true });
}
});

test("only load-time value imports form edges", () => {
const source = [
'import { a } from "./a";',
Expand Down
Loading