could not get tsarch working

 npm uninstall tsarch

inspired by https://stackoverflow.com/a/69210603/820837

hacked together my own (in a very rough state)

src/architecture.spec.ts

import { describe, expect, it } from 'vitest';  
import { verifyArchitecture } from './archtest';  
  
describe('Architecture test', () => {  
  it('services should not depend on db', async () => {  
   expect(await verifyArchitecture({  
    filesFromFolder: 'src/lib/server/services',  
    notDependOnFolder: 'src/lib/server/db'  
   })).toEqual([]);  
  });
  
  it('repositories should not depend on services', async () => {  
   expect(await verifyArchitecture({  
    filesFromFolder: 'src/lib/server/repositories',  
    notDependOnFolder: 'src/lib/server/services'  
   })).toEqual([]);  
  });
  
  it('db should not depend on services', async () => {  
   expect(await verifyArchitecture({  
    filesFromFolder: 'src/lib/server/db',  
    notDependOnFolder: 'src/lib/server/services'  
   })).toEqual([]);  
  });
});

src/archtest/index.ts

import { readdir, readFile } from 'node:fs/promises';  
import * as ts from 'typescript';  
  
async function getFiles(folder: string) {  
  if (folder.endsWith('/')) throw new Error('folder cannot end in /');  
  if (folder === '') throw new Error('folder cannot be empty');  
  
  const files = await readdir(folder, { recursive: true });  
  return files.map(file => `${folder}/${file}`);  
}  
  
type FileDependencies = {  
  file: string,  
  dependencies: Dependency[]  
}  
  
async function getDependenciesFromFile(file: string): Promise<FileDependencies | null> {  
  try {  
   const source = await readFile(file);  
   const rootNode = ts.createSourceFile(  
    file,  
    source.toString(),  
    ts.ScriptTarget.Latest,  
    /*setParentNodes */ true  
   );  
  
   return {  
    file: file,  
    dependencies: rootNode.getChildren().flatMap(c => getDependenciesFromNode(file, c))  
   };  
  } catch (e: unknown) {  
   if(e.code === 'EISDIR') return null  
   throw new Error(`failed to get dependencies from file '${file}': ${e}`);  
  }  
}  
  
function unLib(dependency: Dependency) {  
  // todo: relative depending on tsconfig.json  
  return {  
   ...dependency,  
   referencedSpecifier: dependency.referencedSpecifier.replace(/^\$lib\//, 'src/lib/')  
  };  
}  
  
const specifierRelativeFile = /^\..*$/;  
const specifierNodeModule = /^[^.]/;  
  
type Dependency = {  
  typeOnly: boolean  
  relativePathReference: boolean  
  referencingPath: string  
  referencedSpecifier: string  
}  
  
function getDependenciesFromNode(path: string, node: ts.Node): Dependency[] {  
  switch (node.kind) {  
   case ts.SyntaxKind.ExportDeclaration: {  
    console.log('skipping ExportDeclaration');  
    return [];  
   }  
   case ts.SyntaxKind.ImportDeclaration: {  
    const importDeclaration = node as ts.ImportDeclaration;  
    const importClause = importDeclaration.importClause;  
    const specifier = (importDeclaration.moduleSpecifier as ts.StringLiteral).text;  
  
    if (!specifier) {  
     console.log('no specifier');  
     return [];  
    }  
  
    if (specifierRelativeFile.test(specifier)) {  
     return [{  
      typeOnly: (!!importClause && !importClause.isTypeOnly),  
      relativePathReference: true,  
      referencingPath: path,  
      referencedSpecifier: specifier  
     }];  
    } else if (specifierNodeModule.test(specifier)) {  
     return [{  
      typeOnly: (!!importClause && !importClause.isTypeOnly),  
      relativePathReference: false,  
      referencingPath: path,  
      referencedSpecifier: specifier  
     }];  
    } else {  
     console.log('specifier neither relative nor module', specifier);  
     return [];  
    }  
   }  
   case ts.SyntaxKind.CallExpression: {  
    const callExpression = node as ts.CallExpression;  
  
    if (!((callExpression.expression.kind === ts.SyntaxKind.ImportKeyword ||  
      (callExpression.expression.kind === ts.SyntaxKind.Identifier &&  
       callExpression.expression.getText() === 'require')) &&  
     callExpression.arguments[0]?.kind === ts.SyntaxKind.StringLiteral)) {  
     return node.getChildren().flatMap(c => getDependenciesFromNode(path, c));  
    }  
  
    const specifier = (callExpression.arguments[0] as ts.StringLiteral).text;  
  
    if (specifierRelativeFile.test(specifier)) {  
     return [{  
      typeOnly: false,  
      relativePathReference: true,  
      referencingPath: path,  
      referencedSpecifier: specifier  
     }];  
    } else if (specifierNodeModule.test(specifier)) {  
     return [{  
      typeOnly: false,  
      relativePathReference: false,  
      referencingPath: path,  
      referencedSpecifier: specifier  
     }];  
    } else {  
     return node.getChildren().flatMap(c => getDependenciesFromNode(path, c));  
    }  
   }  
   default: {  
    return node.getChildren().flatMap(c => getDependenciesFromNode(path, c));  
   }  
  }  
}  
  
export type ArchitectureSpec = {  
  filesFromFolder: string  
  notDependOnFolder: string  
}  
  
export type Violation = {  
  file: string,  
  message: string  
}  
  
export async function verifyArchitecture(spec: ArchitectureSpec): Promise<Violation[]> {  
  const codeFileExtensions = ['.ts', '.js']  
  const filesFromFolder = (await getFiles(spec.filesFromFolder))  
   .filter(file => codeFileExtensions.filter(ext => file.endsWith(ext)).length );  
  const parsedFileDependencies = await Promise.all(filesFromFolder.map(getDependenciesFromFile));  
  
  const dependenciesFromFolder = parsedFileDependencies  
   .filter(d => d!==null)  
   .map(f => ({  
    ...f, dependencies: f.dependencies  
     .map(unLib)  
     // unRelative  
     .map(d => d.referencedSpecifier)  
   }))  
  ;  
  
  const violations: Violation[] = dependenciesFromFolder  
   .map(f => {  
    const notAllowed = f.dependencies.filter(d => d.startsWith(spec.notDependOnFolder));  
    if (notAllowed.length) {  
     return {  
      file: f.file,  
      message: `should not depend on folder ${spec.notDependOnFolder}`  
     };  
    }  
    return null;  
   })  
   .filter(v => v !== null);  
  return violations;  
}