/** * A tsconfig `paths` entry keyed `"*"` matches every specifier, so treating * "an alias pattern matches" as "the import is the project's" made every * package import in such a project look local. The out-of-repo guard never * fired there: on cord-field (`"baseUrl": "src"`, `"*": ["./typings/*"]`), * 169 imports of `Typography` from `@mui/material` landed on a project * `Typography`, and `Form` from `react-final-form` on the project's form. * * An alias counts only when it maps the specifier to a project file. A * package the importing file's package.json declares, which no alias maps to * a file, is outside the repository, catch-all or not. A catch-all that does * find a file (a local `.d.ts` for an untyped package, or `"*": ["src/*"]` * for the project's own folders) still makes that import the project's. */ import { describe, it, expect, afterAll } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; const roots: string[] = []; afterAll(() => { for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true }); }); async function indexProject(files: Record): Promise { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-catch-all-alias-')); roots.push(root); for (const [rel, content] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), content); } return CodeGraph.init(root, { index: true }); } /** * ` :` for every edge leaving * `file`'s nodes. JSX tags are left out: a separate pass links `; } `, 'src/components/Card.tsx': `export function Card(props: { children?: unknown }) { return
{String(props.children)}
; } `, 'src/components/form/Form.tsx': `export function Form() { return
; } export function useForm() { return { valid: true }; } `, 'src/common/DateTime.ts': `export class DateTime { static now(): DateTime { return new DateTime(); } } `, // The catch-all does answer this package: its types live in the project. 'src/typings/legacy-widgets.d.ts': `export interface WidgetOptions { name: string } export declare class Widget { constructor(options: WidgetOptions); render(): void; } `, 'src/scenes/Page.tsx': `import { Button, type ButtonProps } from '@mui/material'; import { Form, useForm } from 'react-final-form'; import { DateTime } from 'luxon'; import { Widget, type WidgetOptions } from 'legacy-widgets'; import { Card } from '~/components/Card'; const options: WidgetOptions = { name: 'page' }; export function Page(props: ButtonProps) { const form = useForm(); const when = DateTime.now(); new Widget(options).render(); return ; } `, }); try { const fromPage = edgesFrom(cg, 'src/scenes/Page.tsx'); // No name the page imports from a package lands on a project namesake. expect(fromPage.filter((e) => /src\/components\/(?:Button|form\/Form)\.tsx|src\/common\/DateTime\.ts/.test(e))).toEqual([]); // `~/…` still reaches the project, and so does a package whose types the // catch-all finds in the project. expect(fromPage).toContain('imports src/components/Card.tsx:Card'); expect(fromPage).toContain('imports src/typings/legacy-widgets.d.ts:Widget'); expect(fromPage).toContain('imports src/typings/legacy-widgets.d.ts:WidgetOptions'); } finally { cg.close(); } }); it('still makes an import the project’s when it maps the import to a project file', async () => { const cg = await indexProject({ 'package.json': JSON.stringify({ name: 'shop', dependencies: { '@mui/material': '^5.15.0', react: '^18.2.0' } }), 'tsconfig.json': JSON.stringify({ compilerOptions: { baseUrl: '.', jsx: 'react-jsx', paths: { '*': ['src/*'] } } }), 'src/components/Button.tsx': `export function Button() { return