From 015bbd7638ca9b8bbac02e108b38a3d5a174ad07 Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 6 Aug 2026 10:22:27 +0800 Subject: [PATCH] refactor(fmt): reuse relative path resolver --- packages/rstack/src/fmt/discoverPaths.ts | 33 +++++-------------- packages/rstack/src/fmt/ignore.ts | 7 ++-- packages/rstack/src/fmt/relativePath.ts | 17 ++++++++++ .../rstack/tests/fmt/relativePath.test.ts | 21 ++++++++++++ 4 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 packages/rstack/src/fmt/relativePath.ts create mode 100644 packages/rstack/tests/fmt/relativePath.test.ts diff --git a/packages/rstack/src/fmt/discoverPaths.ts b/packages/rstack/src/fmt/discoverPaths.ts index afd70fe9..c8f25d9a 100644 --- a/packages/rstack/src/fmt/discoverPaths.ts +++ b/packages/rstack/src/fmt/discoverPaths.ts @@ -4,6 +4,7 @@ import ignore from 'ignore'; import isBinaryPath from 'is-binary-path'; import micromatch from 'micromatch'; import readdir, { type Dirent, type DirentLike } from 'tiny-readdir'; +import { createRelativePathResolver, type RelativePathResolver } from './relativePath.ts'; const defaultIgnoredDirNames = new Set(['.git', '.sl', '.svn', '.hg', '.jj', 'node_modules']); @@ -38,19 +39,6 @@ const isRelativePathInside = (relativePath: string): boolean => const isPathInside = (rootPath: string, filePath: string): boolean => isRelativePathInside(path.relative(rootPath, filePath)); -type RelativePathResolver = (filePath: string) => string; - -const createRelativePathResolver = (rootPath: string): RelativePathResolver => { - const rootPrefix = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`; - - return (filePath) => - filePath === rootPath - ? '' - : filePath.startsWith(rootPrefix) - ? filePath.slice(rootPrefix.length) - : path.relative(rootPath, filePath); -}; - const toPosixPath = (filePath: string): string => path.sep === '\\' ? filePath.replaceAll('\\', '/') : filePath; @@ -86,14 +74,14 @@ const findGitRoot = async (cwd: string): Promise => { class GitIgnoreMatcher { readonly #rootPath: string; - readonly #rootPrefix: string; + readonly #resolveRelativePath: RelativePathResolver; readonly #matchers = new Map>(); readonly #loads = new Map>(); readonly #ignoredDirectories = new Map(); private constructor(rootPath: string) { this.#rootPath = rootPath; - this.#rootPrefix = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`; + this.#resolveRelativePath = createRelativePathResolver(rootPath); } static async create(cwd: string): Promise { @@ -103,11 +91,11 @@ class GitIgnoreMatcher { } async loadThrough(directoryPath: string): Promise { - if (!isPathInside(this.#rootPath, directoryPath)) { + const relativePath = this.#resolveRelativePath(directoryPath); + if (!isRelativePathInside(relativePath)) { return; } - const relativePath = path.relative(this.#rootPath, directoryPath); const segments = relativePath ? relativePath.split(path.sep) : []; const loads = [this.#load(this.#rootPath)]; let currentPath = this.#rootPath; @@ -121,7 +109,7 @@ class GitIgnoreMatcher { } async load(directoryPath: string): Promise { - if (isPathInside(this.#rootPath, directoryPath)) { + if (isRelativePathInside(this.#resolveRelativePath(directoryPath))) { await this.#load(directoryPath); } } @@ -131,12 +119,7 @@ class GitIgnoreMatcher { return false; } - const relativePath = - filePath === this.#rootPath - ? '' - : filePath.startsWith(this.#rootPrefix) - ? filePath.slice(this.#rootPrefix.length) - : path.relative(this.#rootPath, filePath); + const relativePath = this.#resolveRelativePath(filePath); if (relativePath === '' || !isRelativePathInside(relativePath)) { return false; } @@ -175,7 +158,7 @@ class GitIgnoreMatcher { return cached; } - relativePath ??= path.relative(this.#rootPath, directoryPath); + relativePath ??= this.#resolveRelativePath(directoryPath); // Git cannot re-include a path below an ignored directory. const parentPath = path.dirname(directoryPath); diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index 1405de29..33daf474 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -1,6 +1,7 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; import createIgnore from 'ignore'; +import { createRelativePathResolver } from './relativePath.ts'; import type { ResolvedFmtConfig } from './types.ts'; /** @@ -28,12 +29,10 @@ const createDefaultIgnoreMatcher = (): IgnoreMatcher => { const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => { const matcher = createIgnore({ allowRelativePaths: true }).add(patterns); - const rootPrefix = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`; + const resolveRelativePath = createRelativePathResolver(rootPath); return (filePath, isDirectory = false) => { - const relativePath = filePath.startsWith(rootPrefix) - ? filePath.slice(rootPrefix.length) - : path.relative(rootPath, filePath); + const relativePath = resolveRelativePath(filePath); if (relativePath === '') { return false; } diff --git a/packages/rstack/src/fmt/relativePath.ts b/packages/rstack/src/fmt/relativePath.ts new file mode 100644 index 00000000..9e5b1d09 --- /dev/null +++ b/packages/rstack/src/fmt/relativePath.ts @@ -0,0 +1,17 @@ +import path from 'node:path'; + +type RelativePathResolver = (filePath: string) => string; + +const createRelativePathResolver = (rootPath: string): RelativePathResolver => { + const rootPrefix = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`; + + return (filePath) => + filePath === rootPath + ? '' + : filePath.startsWith(rootPrefix) + ? filePath.slice(rootPrefix.length) + : path.relative(rootPath, filePath); +}; + +export { createRelativePathResolver }; +export type { RelativePathResolver }; diff --git a/packages/rstack/tests/fmt/relativePath.test.ts b/packages/rstack/tests/fmt/relativePath.test.ts new file mode 100644 index 00000000..5b07d90a --- /dev/null +++ b/packages/rstack/tests/fmt/relativePath.test.ts @@ -0,0 +1,21 @@ +import path from 'node:path'; +import { expect, test } from 'rstack/test'; +import { createRelativePathResolver } from '../../src/fmt/relativePath.ts'; + +const rootPath = path.join(import.meta.dirname, 'project'); + +test('resolves paths relative to a fixed root', () => { + const resolveRelativePath = createRelativePathResolver(rootPath); + + expect(resolveRelativePath(rootPath)).toBe(''); + expect(resolveRelativePath(path.join(rootPath, 'src/index.ts'))).toBe( + path.join('src', 'index.ts'), + ); +}); + +test('falls back for paths outside the fixed root', () => { + const resolveRelativePath = createRelativePathResolver(rootPath); + const siblingPath = path.join(`${rootPath}-other`, 'index.ts'); + + expect(resolveRelativePath(siblingPath)).toBe(path.relative(rootPath, siblingPath)); +});