Repository: styleguidist/react-docgen-typescript (ts)

Issue:
trimFileName no longer works in a monorepo
#392 changed the behavior of trimming filenames so that it no longer works properly on types coming from `node_modules` in a monorepo, where commands may be run from a particular package but dependencies are installed at the repo root. 

For example, I might run commands from an individual package `/Users/me/my-repo/packages/foo`, but the React typings are installed at the root, `/Users/me/my-repo/node_modules/@types/react/index.d.ts`.

Previously, the parent/declaration filename for props derived from React types would come back as `my-repo/node_modules/@types/react/index.d.ts`. Then we used a `propFilter` to exclude props coming from that file.

After #392, the filename in this case is just `index.d.ts`, which breaks the filtering.

Pull request description:
Fix trimming of filenames
After #392, trimming filenames no longer works properly on types coming from node_modules in a monorepo, where commands may be run from a particular package but dependencies are installed at the repo root (see #404 for more details).

This PR restores the previous behavior of returning the path starting from the first common root folder between the filename and `process.cwd()`, in a way that should be reliable across platforms regardless of how paths are formatted.

Fixes #404

Patch:
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index ad345e8..0e90f1a 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -1,10 +1,13 @@
 name: Node CI
 
-on: [push]
+on:
+  push:
+    branches: [master]
+  pull_request:
+    branches: [master]
 
 jobs:
   build:
-
     runs-on: ubuntu-latest
 
     strategy:
@@ -12,16 +15,16 @@ jobs:
         node-version: [14.x]
 
     steps:
-    - uses: actions/checkout@v1
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v1
-      with:
-        node-version: ${{ matrix.node-version }}
-    - name: install dependencies
-      run: npm install
-      env:
-        CI: true
-    - name: test
-      run: npm test
-      env:
-        CI: true
+      - uses: actions/checkout@v1
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: install dependencies
+        run: npm install
+        env:
+          CI: true
+      - name: test
+        run: npm test
+        env:
+          CI: true
diff --git a/src/parser.ts b/src/parser.ts
index d22f26e..4dea833 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -4,7 +4,7 @@ import * as ts from 'typescript';
 
 import { buildFilter } from './buildFilter';
 import { SymbolDisplayPart } from 'typescript';
-import { trimFileName } from './utils';
+import { trimFileName } from './trimFileName';
 
 type InterfaceOrTypeAliasDeclaration =
   | ts.TypeAliasDeclaration
diff --git a/src/trimFileName.ts b/src/trimFileName.ts
new file mode 100644
index 0000000..011853e
--- /dev/null
+++ b/src/trimFileName.ts
@@ -0,0 +1,40 @@
+import * as path from 'path';
+
+const slashRegex = /[\\/]/g;
+
+export function trimFileName(
+  fileName: string,
+  cwd: string = process.cwd(),
+  platform?: 'posix' | 'win32'
+) {
+  // This allows tests to run regardless of current platform
+  const pathLib = platform ? path[platform] : path;
+
+  // Typescript formats Windows paths with forward slashes. For easier use of
+  // the path utilities, normalize to platform-standard slashes, then restore
+  // the original slashes when returning the result.
+  const originalSep = fileName.match(slashRegex)?.[0] || pathLib.sep;
+  const normalizedFileName = pathLib.normalize(fileName);
+  const root = pathLib.parse(cwd).root;
+
+  // Walk up paths from the current directory until we find a common ancestor,
+  // and return the path relative to that. This will work in either a single-
+  // package repo or a monorepo (where dependencies may be installed at the
+  // root, but commands may be run in a package folder).
+  let parent = cwd;
+  do {
+    if (normalizedFileName.startsWith(parent)) {
+      return (
+        pathLib
+          // Preserve the parent directory name to match existing behavior
+          .relative(pathLib.dirname(parent), normalizedFileName)
+          // Restore original type of slashes
+          .replace(slashRegex, originalSep)
+      );
+    }
+    parent = pathLib.dirname(parent);
+  } while (parent !== root);
+
+  // No common ancestor, so return the path as-is
+  return fileName;
+}
diff --git a/src/utils.ts b/src/utils.ts
deleted file mode 100644
index 8806bc7..0000000
--- a/src/utils.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as path from 'path';
-
-export function trimFileName(fileName: string) {
-  // We'll use the currentDirectoryName to trim parent fileNames
-  const currentDirectoryPath = process.cwd();
-  const currentDirectoryParts = currentDirectoryPath.split(path.sep);
-  const currentDirectoryName =
-    currentDirectoryParts[currentDirectoryParts.length - 1];
-
-  const fileNameParts = fileName.split('/');
-  const idx = fileNameParts.lastIndexOf(currentDirectoryName);
-  return fileNameParts.slice(idx, fileNameParts.length).join('/');
-}


Test patch:
diff --git a/src/__tests__/trimFileName.ts b/src/__tests__/trimFileName.ts
new file mode 100644
index 0000000..267e624
--- /dev/null
+++ b/src/__tests__/trimFileName.ts
@@ -0,0 +1,119 @@
+import { assert } from 'chai';
+import { trimFileName } from '../trimFileName';
+
+describe.only('trimFileName', () => {
+  describe('posix', () => {
+    const cwd = '/home/user/projects/react-docgen-typescript';
+    const platform = 'posix';
+
+    it('works with simple path', () => {
+      const input = `${cwd}/src/__tests__/data/ExportsPropTypeImport.tsx`;
+      assert.equal(
+        trimFileName(input, cwd, platform),
+        'react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx'
+      );
+    });
+
+    it('works with repeated path segments', () => {
+      const input = `${cwd}/react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx`;
+      assert.equal(
+        trimFileName(input, cwd, platform),
+        'react-docgen-typescript/react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx'
+      );
+    });
+
+    it('preserves package name from node_modules', () => {
+      const input = `${cwd}/node_modules/@types/react/index.d.ts`;
+      assert.equal(
+        trimFileName(input, cwd, platform),
+        'react-docgen-typescript/node_modules/@types/react/index.d.ts'
+      );
+    });
+
+    it('preserves package name from node_modules in monorepo', () => {
+      // This simulates running docgen in an individual package in a monorepo
+      const monorepoCwd = cwd + '/packages/foo';
+      const input = `${cwd}/node_modules/@types/react/index.d.ts`;
+      assert.equal(
+        trimFileName(input, monorepoCwd, platform),
+        'react-docgen-typescript/node_modules/@types/react/index.d.ts'
+      );
+    });
+
+    it('returns full path if there is no common root', () => {
+      const input = '/somewhere/else/foo.d.ts';
+      assert.equal(trimFileName(input, cwd, platform), input);
+    });
+
+    it('works when run at the root directory', () => {
+      // This is more of a theoretical edge case
+      const input = `/src/__tests__/data/ExportsPropTypeImport.tsx`;
+      assert.equal(
+        trimFileName(input, '/', platform),
+        'src/__tests__/data/ExportsPropTypeImport.tsx'
+      );
+    });
+  });
+
+  describe('windows', () => {
+    const cwd = 'C:\\Users\\user\\projects\\react-docgen-typescript';
+    // typescript uses forward slashes even in windows paths
+    const cwdForwardSlash = cwd.replace(/\\/g, '/');
+    const platform = 'win32';
+
+    it('works with simple path', () => {
+      const input = `${cwdForwardSlash}/src/__tests__/data/ExportsPropTypeImport.tsx`;
+      assert.equal(
+        trimFileName(input, cwd, platform),
+        'react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx'
+      );
+    });
+
+    it('works with repeated path segments', () => {
+      const input = `${cwdForwardSlash}/react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx`;
+      assert.equal(
+        trimFileName(input, cwd, platform),
+        'react-docgen-typescript/react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx'
+      );
+    });
+
+    it('preserves package name from node_modules', () => {
+      const input = `${cwdForwardSlash}/node_modules/@types/react/index.d.ts`;
+      assert.equal(
+        trimFileName(input, cwd, platform),
+        'react-docgen-typescript/node_modules/@types/react/index.d.ts'
+      );
+    });
+
+    it('preserves package name from node_modules in monorepo', () => {
+      const monorepoCwd = cwd + '\\packages\\foo';
+      const input = `${cwdForwardSlash}/node_modules/@types/react/index.d.ts`;
+      assert.equal(
+        trimFileName(input, monorepoCwd, platform),
+        'react-docgen-typescript/node_modules/@types/react/index.d.ts'
+      );
+    });
+
+    it('returns full path if there is no common root', () => {
+      const input = 'D:/somewhere/else/foo.d.ts';
+      assert.equal(trimFileName(input, cwd, platform), input);
+    });
+
+    it('works when run at the filesystem root', () => {
+      const input = `C:/src/__tests__/data/ExportsPropTypeImport.tsx`;
+      assert.equal(
+        trimFileName(input, 'C:\\', platform),
+        'src/__tests__/data/ExportsPropTypeImport.tsx'
+      );
+    });
+
+    it('works with backslashes', () => {
+      // typescript uses forward slashes in file paths, but test with backslashes too
+      const input = `${cwd}\\src\\__tests__\\data\\ExportsPropTypeImport.tsx`;
+      assert.equal(
+        trimFileName(input, cwd, platform),
+        'react-docgen-typescript\\src\\__tests__\\data\\ExportsPropTypeImport.tsx'
+      );
+    });
+  });
+});
diff --git a/src/__tests__/utils.ts b/src/__tests__/utils.ts
deleted file mode 100644
index 99fbdb4..0000000
--- a/src/__tests__/utils.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { assert } from 'chai';
-import { trimFileName } from '../utils';
-
-describe('utils', () => {
-  it('should trim file name for simple path', () => {
-    const input =
-      '/home/user/projects/react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx';
-    assert.equal(
-      trimFileName(input),
-      'react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx'
-    );
-  });
-  it('should trim file name for complex path', () => {
-    const input =
-      '/home/user/projects/react-docgen-typescript/react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx';
-    assert.equal(
-      trimFileName(input),
-      'react-docgen-typescript/src/__tests__/data/ExportsPropTypeImport.tsx'
-    );
-  });
-});