From 0b903756a5940759e15b4cce6c254a5c8bc104b3 Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 23 Jun 2026 11:11:15 +0200 Subject: [PATCH] Improve .indexignore support -Improve ignore file handling for the index generation command. -Now has reverse lookup up the parent hierachy similar to .gitignore --- .vscode/settings.json | 2 +- .../cli/src/commands/generate-index.spec.ts | 91 +++++++++++++++++++ .../cli/src/commands/generate-index.ts | 50 +++++++++- dev-packages/cli/src/util/file-util.ts | 25 +++++ 4 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 dev-packages/cli/src/commands/generate-index.spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 1f2422f..8b52a0f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,7 @@ "**/lib": true }, "task.autoDetect": "off", - "typescript.tsdk": "node_modules/typescript/lib", + "js/ts.tsdk.path": "node_modules/typescript/lib", "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, diff --git a/dev-packages/cli/src/commands/generate-index.spec.ts b/dev-packages/cli/src/commands/generate-index.spec.ts new file mode 100644 index 0000000..e2ecc25 --- /dev/null +++ b/dev-packages/cli/src/commands/generate-index.spec.ts @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { execFileSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { initGlobby } from '../util'; +import { GenerateIndexCmdOptions, generateIndex } from './generate-index'; +import { cleanupTempDir, createTempDir } from '../../tests/helpers/test-helper'; + +const DEFAULT_OPTIONS: GenerateIndexCmdOptions = { + singleIndex: true, + forceOverwrite: true, + match: ['**/*.ts', '**/*.tsx'], + ignore: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.d.ts'], + ignoreFile: '.indexignore', + style: 'commonjs', + verbose: false +}; + +describe('generateIndex', () => { + let tempDir: string; + let srcDir: string; + const originalCwd = process.cwd(); + + beforeAll(async () => { + await initGlobby(); + }); + + beforeEach(() => { + tempDir = createTempDir(); + // the ancestor-traversal is bounded by the enclosing git repository, so make the temp dir a repo root + execFileSync('git', ['init', '-q'], { cwd: tempDir }); + srcDir = path.join(tempDir, 'src'); + fs.mkdirSync(path.join(srcDir, 'base'), { recursive: true }); + fs.mkdirSync(path.join(srcDir, 'test'), { recursive: true }); + fs.writeFileSync(path.join(srcDir, 'base', 'util.ts'), ''); + fs.writeFileSync(path.join(srcDir, 'test', 'test-util.ts'), ''); + }); + + afterEach(() => { + process.chdir(originalCwd); + cleanupTempDir(tempDir); + }); + + afterAll(() => { + process.chdir(originalCwd); + }); + + function readIndex(): string { + return fs.readFileSync(path.join(srcDir, 'index.ts'), 'utf-8'); + } + + it('should include all matching files when no ignore file is present', () => { + generateIndex(srcDir, DEFAULT_OPTIONS); + const index = readIndex(); + expect(index).toContain("export * from './base/util';"); + expect(index).toContain("export * from './test/test-util';"); + }); + + it('should honor an ignore file located in the source directory itself', () => { + fs.writeFileSync(path.join(srcDir, '.indexignore'), 'test/\n'); + generateIndex(srcDir, DEFAULT_OPTIONS); + const index = readIndex(); + expect(index).toContain("export * from './base/util';"); + expect(index).not.toContain('test/test-util'); + }); + + it('should honor an ignore file located in a parent directory (up to the git root)', () => { + // ignore file one level above the indexed source directory + fs.writeFileSync(path.join(tempDir, '.indexignore'), 'test/\n'); + generateIndex(srcDir, DEFAULT_OPTIONS); + const index = readIndex(); + expect(index).toContain("export * from './base/util';"); + expect(index).not.toContain('test/test-util'); + }); +}); diff --git a/dev-packages/cli/src/commands/generate-index.ts b/dev-packages/cli/src/commands/generate-index.ts index deaf909..c211609 100644 --- a/dev-packages/cli/src/commands/generate-index.ts +++ b/dev-packages/cli/src/commands/generate-index.ts @@ -18,7 +18,18 @@ import { createOption } from 'commander'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { GlobOptions, LOGGER, baseCommand, cd, configureLogger, globby, validateDirectory } from '../util'; +import { + GlobOptions, + LOGGER, + baseCommand, + cd, + configureLogger, + getGitRoot, + globby, + isGitRepository, + isIgnoredByIgnoreFiles, + validateDirectory +} from '../util'; export interface GenerateIndexCmdOptions { singleIndex: boolean; forceOverwrite: boolean; @@ -58,11 +69,12 @@ export function generateIndex(rootDir: string, options: GenerateIndexCmdOptions) ignore, cwd, onlyFiles: options.singleIndex, - markDirectories: true, - ignoreFiles: '**/' + options.ignoreFile + markDirectories: true }; LOGGER.debug('Search for children using the following glob options', globOptions); - const files = globby(pattern, globOptions); + + const isIgnored = createIgnoreFilter(cwd, options.ignoreFile); + const files = globby(pattern, globOptions).filter(file => !isIgnored(file)); LOGGER.debug('All children considered in the input directory', files); const relativeRootDirectory = ''; @@ -80,6 +92,36 @@ export function generateIndex(rootDir: string, options: GenerateIndexCmdOptions) } } +/** + * Creates a predicate that determines whether a globbed entry should be excluded based on the + * configured ignore files. + * + * Ignore files are resolved from the enclosing Git repository root down to the indexed directory, so + * that ignore files in parent directories are honored — matching how Git applies `.gitignore` files up + * the tree. Outside of a Git repository there is no well-defined upper boundary, so only ignore files + * within {@link rootDir} are considered. + * + * @param rootDir The absolute source directory that is being indexed (globbed entries are relative to it). + * @param ignoreFileName The name of the ignore file to honor (e.g. `.indexignore`). + * @returns A predicate that returns `true` for entries that should be excluded from indexing. + */ +export function createIgnoreFilter(rootDir: string, ignoreFileName: string): (file: string) => boolean { + const ignoreRoot = isGitRepository(rootDir) ? getGitRoot(rootDir) : rootDir; + const isIgnored = isIgnoredByIgnoreFiles('**/' + ignoreFileName, { cwd: ignoreRoot }); + return file => { + // the predicate expects POSIX paths relative to `ignoreRoot` + let relative = path.relative(ignoreRoot, path.join(rootDir, file)).split(path.sep).join('/'); + if (relative === '') { + return false; + } + // globby marks directories with a trailing slash; preserve it so directory-only patterns (e.g. `test/`) match + if (isDirectory(file)) { + relative += '/'; + } + return isIgnored(relative); + }; +} + export function isDirectChild(parent: string, child: string, childHasChildren: () => boolean): boolean { return isChildFile(parent, child) || (isChildDirectory(parent, child) && childHasChildren()); } diff --git a/dev-packages/cli/src/util/file-util.ts b/dev-packages/cli/src/util/file-util.ts index 47bd72d..1ad6608 100644 --- a/dev-packages/cli/src/util/file-util.ts +++ b/dev-packages/cli/src/util/file-util.ts @@ -134,10 +134,15 @@ export interface GlobOptions { } type GlobbySync = (patterns: string | string[], options?: GlobOptions) => string[]; +/** Predicate that reports whether a given path (relative to the configured `cwd`) is ignored. */ +export type IgnoreFilter = (path: string) => boolean; +type IsIgnoredByIgnoreFilesSync = (patterns: string | string[], options?: GlobOptions) => IgnoreFilter; let _globbySync: GlobbySync | undefined; +let _isIgnoredByIgnoreFilesSync: IsIgnoredByIgnoreFilesSync | undefined; const _globbyReady: Promise = import('globby').then(m => { _globbySync = m.globbySync as GlobbySync; + _isIgnoredByIgnoreFilesSync = m.isIgnoredByIgnoreFilesSync as IsIgnoredByIgnoreFilesSync; }); export async function initGlobby(): Promise { @@ -151,6 +156,26 @@ export function globby(patterns: string | string[], options?: GlobOptions): stri return _globbySync(patterns, options); } +/** + * Builds a predicate that reports whether a path is excluded by the gitignore-style ignore files + * matched by the given {@link patterns}. + * + * In contrast to the {@link GlobOptions.ignoreFiles} glob option, which only searches the glob `cwd` + * downwards, this also honors ignore files located in parent directories when {@link GlobOptions.cwd} + * points at an ancestor (e.g. the repository root) — mirroring how Git applies `.gitignore` files up + * the directory tree. + * + * @param patterns Glob pattern(s) used to locate the ignore files (e.g. `'**\/.indexignore'`). + * @param options Glob options; `cwd` defines the directory the returned predicate's paths are resolved against. + * @returns A predicate that returns `true` for paths (relative to `cwd`) that are ignored. + */ +export function isIgnoredByIgnoreFiles(patterns: string | string[], options?: GlobOptions): IgnoreFilter { + if (!_isIgnoredByIgnoreFilesSync) { + throw new Error('globby not initialized. Call initGlobby() before using glob functions.'); + } + return _isIgnoredByIgnoreFilesSync(patterns, options); +} + /** * Finds all files and directories matching the given pattern. * @param paths The file or directory paths to search. If a path is a directory, all