From 784442bc2675ceffa00ad59b9dfa53ed68e0424b Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sat, 11 Apr 2026 09:57:02 +0900 Subject: [PATCH 01/15] fix(windows): normalize path separators for Windows compatibility - fix IncludeContainer.isInclude and ExcludeContainer.isExclude to normalize backslashes before map lookup so Windows paths (C:\project\src\foo.ts) correctly match posix keys stored in the map (C:/project/src/foo.ts) - fix getCorrectCasedPath to always return posix-separated paths via replaceSepToPosix, preventing backslash-contaminated paths from propagating to all downstream callers on Windows - add Debugger singleton with --verbose/-v CLI flag to emit diagnostic logs to stderr for path resolution tracing on Windows - add regression tests for Windows-style backslash paths in both IncludeContainer and ExcludeContainer (absolute, relative, inline) Co-Authored-By: Claude Sonnet 4.6 --- src/cli/builders/setProjectOptions.ts | 7 ++ src/cli/commands/buildCommand.ts | 2 + src/cli/commands/removeCommand.ts | 2 + src/cli/ux/Debugger.ts | 104 ++++++++++++++++++ src/configs/castConfig.ts | 1 + src/configs/interfaces/IProjectOptions.ts | 11 ++ src/configs/transforms/createBuildOptions.ts | 1 + src/configs/transforms/createRemoveOptions.ts | 1 + src/modules/path/getCorrectCasedPath.ts | 7 +- src/modules/scope/ExcludeContainer.ts | 31 +++++- src/modules/scope/IncludeContainer.ts | 41 ++++++- .../scope/__tests__/exclude.container.test.ts | 83 ++++++++++++++ .../scope/__tests__/include.container.test.ts | 61 ++++++++++ 13 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 src/cli/ux/Debugger.ts diff --git a/src/cli/builders/setProjectOptions.ts b/src/cli/builders/setProjectOptions.ts index cb77fd3..3fd1e3c 100644 --- a/src/cli/builders/setProjectOptions.ts +++ b/src/cli/builders/setProjectOptions.ts @@ -30,6 +30,13 @@ export function setProjectOptions>(args: Argv ${String(value)}`); + } + } + + close() { + if (this.#stream != null) { + this.#stream.end(); + this.#stream = undefined; + } + } +} + +Debugger.bootstrap(); diff --git a/src/configs/castConfig.ts b/src/configs/castConfig.ts index ca98d72..0b5beaa 100644 --- a/src/configs/castConfig.ts +++ b/src/configs/castConfig.ts @@ -48,6 +48,7 @@ export function castConfig( 'progress-stream': 'stderr', reasonerStream: 'stderr', 'reasoner-stream': 'stderr', + verbose: false, } as IProjectOptions; } } diff --git a/src/configs/interfaces/IProjectOptions.ts b/src/configs/interfaces/IProjectOptions.ts index 03b3de5..108ad2f 100644 --- a/src/configs/interfaces/IProjectOptions.ts +++ b/src/configs/interfaces/IProjectOptions.ts @@ -39,4 +39,15 @@ export interface IProjectOptions { * @default stderr */ reasonerStream: TStreamType; + + /** + * Enable verbose debug logging to diagnose path resolution and include/exclude issues. + * Outputs diagnostic information to stderr. + * + * @command build, remove + * @mode bundle, create + * + * @default false + */ + verbose: boolean; } diff --git a/src/configs/transforms/createBuildOptions.ts b/src/configs/transforms/createBuildOptions.ts index d6b1fe4..392c1c5 100644 --- a/src/configs/transforms/createBuildOptions.ts +++ b/src/configs/transforms/createBuildOptions.ts @@ -31,6 +31,7 @@ export async function createBuildOptions( spinnerStream: argv.spinnerStream, progressStream: argv.progressStream, reasonerStream: argv.reasonerStream, + verbose: argv.verbose ?? false, options: [], }; diff --git a/src/configs/transforms/createRemoveOptions.ts b/src/configs/transforms/createRemoveOptions.ts index a8c74f9..98bd84c 100644 --- a/src/configs/transforms/createRemoveOptions.ts +++ b/src/configs/transforms/createRemoveOptions.ts @@ -17,6 +17,7 @@ export function createRemoveOptions( spinnerStream: argv.spinnerStream, progressStream: argv.progressStream, reasonerStream: argv.reasonerStream, + verbose: argv.verbose ?? false, removeBackup: argv.removeBackup, exportFilename: argv.exportFilename ?? CE_CTIX_DEFAULT_VALUE.EXPORT_FILENAME, forceYes: argv.forceYes, diff --git a/src/modules/path/getCorrectCasedPath.ts b/src/modules/path/getCorrectCasedPath.ts index c48403a..c8a7cfe 100644 --- a/src/modules/path/getCorrectCasedPath.ts +++ b/src/modules/path/getCorrectCasedPath.ts @@ -1,6 +1,7 @@ /* eslint-disable no-continue, no-await-in-loop */ import { getSep } from '#/modules/path/getSep'; import { atOrThrow, orThrow } from 'my-easy-fp'; +import { replaceSepToPosix } from 'my-node-fp'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -65,9 +66,11 @@ export async function getCorrectCasedPath(inputPath: string): Promise { } } - return correctedPath; + // Normalize to posix separators so callers always receive forward-slash paths + // regardless of the platform (Windows uses backslashes with path.join). + return replaceSepToPosix(correctedPath); } catch { // If any error occurs, return the original path - return inputPath; + return replaceSepToPosix(inputPath); } } diff --git a/src/modules/scope/ExcludeContainer.ts b/src/modules/scope/ExcludeContainer.ts index ff15bab..a0eba7a 100644 --- a/src/modules/scope/ExcludeContainer.ts +++ b/src/modules/scope/ExcludeContainer.ts @@ -1,3 +1,4 @@ +import { Debugger } from '#/cli/ux/Debugger'; import type { IInlineCommentInfo } from '#/comments/interfaces/IInlineCommentInfo'; import type { IModeGenerateOptions } from '#/configs/interfaces/IModeGenerateOptions'; import { getGlobFiles } from '#/modules/file/getGlobFiles'; @@ -7,6 +8,11 @@ import { Glob, type GlobOptions } from 'glob'; import { replaceSepToPosix } from 'my-node-fp'; import path from 'node:path'; +/** Replaces all backslashes with forward slashes regardless of the current platform. */ +function normalizeToPosix(filePath: string): string { + return filePath.replace(/\\/g, '/'); +} + export class ExcludeContainer { #globs: Glob[]; @@ -52,16 +58,33 @@ export class ExcludeContainer { isExclude(filePath: string): boolean { if (this.#map.size <= 0 && this.#inline.size <= 0) { + Debugger.it.log(`isExclude("${filePath}"): map and inline are empty => false`); return false; } if (path.isAbsolute(filePath)) { - return this.#map.get(filePath) != null || this.#inline.get(filePath) != null; + // Normalize backslashes to forward slashes so Windows paths (C:\foo\bar.ts) + // match the posix-normalized keys stored in the map (C:/foo/bar.ts). + const normalizedPath = normalizeToPosix(filePath); + const result = + this.#map.get(normalizedPath) != null || this.#inline.get(normalizedPath) != null; + + Debugger.it.log( + `isExclude("${filePath}"): isAbsolute=true, normalized="${normalizedPath}" => ${result}`, + ); + + return result; } - return ( - this.#map.get(posixResolve(filePath)) != null || - this.#inline.get(posixResolve(filePath)) != null + // Normalize backslashes before resolving so relative Windows-style paths + // (src\cli\foo.ts) are correctly resolved and matched against the map. + const resolved = posixResolve(normalizeToPosix(filePath)); + const result = this.#map.get(resolved) != null || this.#inline.get(resolved) != null; + + Debugger.it.log( + `isExclude("${filePath}"): isAbsolute=false, resolved="${resolved}" => ${result}`, ); + + return result; } } diff --git a/src/modules/scope/IncludeContainer.ts b/src/modules/scope/IncludeContainer.ts index 23abd68..bd6c67c 100644 --- a/src/modules/scope/IncludeContainer.ts +++ b/src/modules/scope/IncludeContainer.ts @@ -1,3 +1,4 @@ +import { Debugger } from '#/cli/ux/Debugger'; import type { IModeGenerateOptions } from '#/configs/interfaces/IModeGenerateOptions'; import { getGlobFiles } from '#/modules/file/getGlobFiles'; import { posixResolve } from '#/modules/path/modules/posixResolve'; @@ -5,6 +6,11 @@ import { defaultExclude } from '#/modules/scope/defaultExclude'; import { Glob, type GlobOptions } from 'glob'; import path from 'node:path'; +/** Replaces all backslashes with forward slashes regardless of the current platform. */ +function normalizeToPosix(filePath: string): string { + return filePath.replace(/\\/g, '/'); +} + export class IncludeContainer { #globs: Glob[]; @@ -21,6 +27,16 @@ export class IncludeContainer { const files = getGlobFiles(globs).map((filePath): [string, boolean] => [filePath, true]); this.#map = new Map(files); this.#globs = [globs]; + + Debugger.it.log( + `IncludeContainer: cwd="${params.cwd}", patterns=${JSON.stringify(params.config.include)}`, + ); + Debugger.it.log(`IncludeContainer: ${files.length} files resolved into map`); + + if (files.length > 0) { + Debugger.it.log(`IncludeContainer: sample map keys (first 5):`); + files.slice(0, 5).forEach(([key]) => Debugger.it.log(` map key: "${key}"`)); + } } get globs(): Readonly[]> { @@ -33,15 +49,34 @@ export class IncludeContainer { isInclude(filePath: string): boolean { if (this.#map.size <= 0) { + Debugger.it.log(`isInclude("${filePath}"): map is empty => false`); return false; } if (path.isAbsolute(filePath)) { - const isExists = this.#map.get(filePath); - return isExists ?? false; + // Normalize backslashes to forward slashes so Windows paths (C:\foo\bar.ts) + // match the posix-normalized keys stored in the map (C:/foo/bar.ts). + const normalizedPath = normalizeToPosix(filePath); + const isExists = this.#map.get(normalizedPath); + const result = isExists ?? false; + + Debugger.it.log( + `isInclude("${filePath}"): isAbsolute=true, normalized="${normalizedPath}" => ${result}`, + ); + + return result; } - return this.#map.get(posixResolve(filePath)) != null; + // Normalize backslashes before resolving so relative Windows-style paths + // (src\cli\foo.ts) are correctly resolved and matched against the map. + const resolved = posixResolve(normalizeToPosix(filePath)); + const result = this.#map.get(resolved) != null; + + Debugger.it.log( + `isInclude("${filePath}"): isAbsolute=false, resolved="${resolved}" => ${result}`, + ); + + return result; } files() { diff --git a/src/modules/scope/__tests__/exclude.container.test.ts b/src/modules/scope/__tests__/exclude.container.test.ts index 5ae193e..c9dd4a8 100644 --- a/src/modules/scope/__tests__/exclude.container.test.ts +++ b/src/modules/scope/__tests__/exclude.container.test.ts @@ -3,6 +3,15 @@ import { posixResolve } from '#/modules/path/modules/posixResolve'; import { ExcludeContainer } from '#/modules/scope/ExcludeContainer'; import { describe, expect, it } from 'vitest'; +/** + * Converts a posix absolute path to a Windows-style path with backslashes. + * e.g. /Users/foo/bar.ts => \Users\foo\bar.ts + * Used to simulate paths that ts-morph returns on Windows. + */ +function toWindowsPath(posixAbsolutePath: string): string { + return posixAbsolutePath.replace(/\//g, '\\'); +} + describe('ExcludeContainer', () => { it('getter', () => { const container = new ExcludeContainer({ @@ -99,4 +108,78 @@ describe('ExcludeContainer', () => { expect(r04).toBeTruthy(); expect(r05).toBeFalsy(); }); + + it('isExclude - Windows-style backslash absolute path should be recognized', () => { + // Regression test for Windows path separator bug. + // On Windows, ts-morph returns paths like C:\project\src\foo.ts (backslashes). + // ExcludeContainer stores posix paths after replaceSepToPosix. + // isExclude must normalize backslashes before map lookup; otherwise it always returns false. + const container = new ExcludeContainer({ + config: { exclude: ['src/cli/**/*.ts'] }, + inlineExcludeds: [], + cwd: process.cwd(), + }); + + // Pick a real file that IS in the exclude map (posix separator) + const posixPath = Array.from(container.map.keys()).at(0); + expect(posixPath).toBeDefined(); + + // Simulate the Windows path that ts-morph would return on a Windows machine + const windowsStylePath = toWindowsPath(posixPath!); + + // isExclude must return true — the file is excluded regardless of separator style + expect(container.isExclude(windowsStylePath)).toBe(true); + }); + + it('isExclude - Windows-style backslash path must not match a non-excluded file', () => { + // Even after normalizing separators, a file outside the exclude glob must not match. + const container = new ExcludeContainer({ + config: { exclude: ['src/cli/**/*.ts'] }, + inlineExcludeds: [], + cwd: process.cwd(), + }); + + // A path that is NOT in the exclude pattern (src/modules, not src/cli) + const posixPathNotExcluded = posixJoin(process.cwd(), 'src/modules/scope/ExcludeContainer.ts'); + const windowsStylePath = toWindowsPath(posixPathNotExcluded); + + expect(container.isExclude(windowsStylePath)).toBe(false); + }); + + it('isExclude - relative Windows-style backslash path should be recognized', () => { + // Regression test for relative paths with backslashes on Windows. + const container = new ExcludeContainer({ + config: { exclude: ['src/cli/**/*.ts'] }, + inlineExcludeds: [], + cwd: process.cwd(), + }); + + // Simulate a relative Windows-style path (backslashes instead of forward slashes) + const windowsRelativePath = 'src\\cli\\builders\\setModeBundleOptions.ts'; + + expect(container.isExclude(windowsRelativePath)).toBe(true); + }); + + it('isExclude - Windows-style backslash inline excluded path should be recognized', () => { + // Regression test: inline excludeds stored with posix paths must also match + // Windows-style paths passed to isExclude. + const inlineFilePath = posixJoin(process.cwd(), 'examples/type03/ComparisonCls.tsx'); + const container = new ExcludeContainer({ + config: { exclude: [] }, + inlineExcludeds: [ + { + commentCode: 'inline exclude test', + tag: 'ctix-exclude', + pos: { line: 1, start: 1, column: 1 }, + filePath: inlineFilePath, + }, + ], + cwd: process.cwd(), + }); + + // Simulate the Windows path version of the same inline-excluded file + const windowsStylePath = toWindowsPath(inlineFilePath); + + expect(container.isExclude(windowsStylePath)).toBe(true); + }); }); diff --git a/src/modules/scope/__tests__/include.container.test.ts b/src/modules/scope/__tests__/include.container.test.ts index 3b4e2de..34d2826 100644 --- a/src/modules/scope/__tests__/include.container.test.ts +++ b/src/modules/scope/__tests__/include.container.test.ts @@ -5,6 +5,15 @@ import { defaultExclude } from '#/modules/scope/defaultExclude'; import { Glob } from 'glob'; import { describe, expect, it } from 'vitest'; +/** + * Converts a posix absolute path to a Windows-style path with backslashes. + * e.g. /Users/foo/bar.ts => \Users\foo\bar.ts + * Used to simulate paths that ts-morph returns on Windows. + */ +function toWindowsPath(posixAbsolutePath: string): string { + return posixAbsolutePath.replace(/\//g, '\\'); +} + describe('IncludeContainer', () => { it('getter', () => { const container = new IncludeContainer({ @@ -75,6 +84,58 @@ describe('IncludeContainer', () => { expect(r05).toBeFalsy(); }); + it('isInclude - Windows-style backslash absolute path should be recognized', () => { + // Regression test for Windows path separator bug. + // On Windows, ts-morph returns paths like C:\project\src\foo.ts (backslashes). + // IncludeContainer stores posix paths (C:/project/src/foo.ts) after replaceSepToPosix. + // isInclude must normalize backslashes before map lookup; otherwise it always returns false. + const container = new IncludeContainer({ + config: { include: ['src/cli/**/*.ts'] }, + cwd: process.cwd(), + }); + + // Pick a real file that IS in the map (posix separator) + const posixPath = Array.from(container.map.keys()).at(0); + expect(posixPath).toBeDefined(); + + // Simulate the Windows path that ts-morph would return on a Windows machine + const windowsStylePath = toWindowsPath(posixPath!); + + // isInclude must return true — the file is included regardless of separator style + expect(container.isInclude(windowsStylePath)).toBe(true); + }); + + it('isInclude - Windows-style backslash path must not match an excluded file', () => { + // Even after normalizing separators, a file outside the include glob must not match. + const container = new IncludeContainer({ + config: { include: ['src/cli/**/*.ts'] }, + cwd: process.cwd(), + }); + + // A path that is NOT in the include pattern (src/modules, not src/cli) + const posixPathNotIncluded = posixJoin(process.cwd(), 'src/modules/scope/IncludeContainer.ts'); + const windowsStylePath = toWindowsPath(posixPathNotIncluded); + + expect(container.isInclude(windowsStylePath)).toBe(false); + }); + + it('isInclude - relative Windows-style backslash path should be recognized', () => { + // Regression test for relative paths with backslashes on Windows. + // When filePath is relative (not absolute), isInclude calls posixResolve. + // On Windows, path.resolve('src\\cli\\foo.ts') returns a proper Windows absolute path, + // but posixResolve must then also normalize separators for the map lookup to succeed. + const container = new IncludeContainer({ + config: { include: ['src/cli/**/*.ts'] }, + cwd: process.cwd(), + }); + + // Simulate a relative Windows-style path (backslashes instead of forward slashes) + const windowsRelativePath = 'src\\cli\\builders\\setModeBundleOptions.ts'; + + // isInclude should resolve and normalize the path to match the map key + expect(container.isInclude(windowsRelativePath)).toBe(true); + }); + it('files - string path', () => { const expactation = getGlobFiles( new Glob('examples/type03/**/*.ts', { From cf2061701fa6679f69d2314b95b9405b36bd86ee Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 12 Apr 2026 16:46:03 +0900 Subject: [PATCH 02/15] feat(debug): add verbose list logging for include/exclude pipeline - add logList() method to Debugger for printing labeled file lists - log ts-morph source files, include/exclude map contents, filtered results at each stage in creating, bundling, and moduling commands - replace IncludeContainer constructor sample log (first 5) with full file list via logList - add ExcludeContainer constructor log showing patterns, resolved map files, and inline excludeds usage: ctix build --verbose 2> ctix-debug.log Co-Authored-By: Claude Sonnet 4.6 --- src/cli/ux/Debugger.ts | 16 ++++++++++++++++ src/modules/commands/bundling.ts | 15 ++++++++++++--- src/modules/commands/creating.ts | 15 ++++++++++++--- src/modules/commands/moduling.ts | 13 ++++++++++++- src/modules/scope/ExcludeContainer.ts | 8 ++++++++ src/modules/scope/IncludeContainer.ts | 13 +++++-------- 6 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/cli/ux/Debugger.ts b/src/cli/ux/Debugger.ts index 4957bb2..5212e8e 100644 --- a/src/cli/ux/Debugger.ts +++ b/src/cli/ux/Debugger.ts @@ -81,6 +81,22 @@ export class Debugger { } } + logList(label: string, items: string[]) { + if (!this.#enable) { + return; + } + + this.log(`${label} (${items.length}):`); + + if (items.length === 0) { + this.log(' (empty)'); + } else { + for (const item of items) { + this.log(` - ${item}`); + } + } + } + table(label: string, entries: [string, unknown][]) { if (!this.#enable) { return; diff --git a/src/modules/commands/bundling.ts b/src/modules/commands/bundling.ts index 3aeb621..5b3329a 100644 --- a/src/modules/commands/bundling.ts +++ b/src/modules/commands/bundling.ts @@ -1,3 +1,4 @@ +import { Debugger } from '#/cli/ux/Debugger'; import { ProgressBar } from '#/cli/ux/ProgressBar'; import { Reasoner } from '#/cli/ux/Reasoner'; import { Spinner } from '#/cli/ux/Spinner'; @@ -64,6 +65,10 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: .map((sourceFile) => getCorrectCasedPath(sourceFile.getFilePath().toString())), ); + Debugger.it.log(`[bundle] project: ${bundleOption.project}`); + Debugger.it.log(`[bundle] projectDirPath: ${extendOptions.resolved.projectDirPath}`); + Debugger.it.logList('[bundle] ts-morph source files', filePaths); + const include = new IncludeContainer({ config: { include: getTsIncludeFiles({ config: bundleOption, extend: extendOptions }) }, cwd: extendOptions.resolved.projectDirPath, @@ -103,9 +108,13 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: return isDeclarationFile(sourceFile); }); - const filenames = filePaths - .filter((filename) => include.isInclude(filename)) - .filter((filename) => !exclude.isExclude(filename)); + const includedFiles = filePaths.filter((filename) => include.isInclude(filename)); + const excludedByExclude = includedFiles.filter((filename) => exclude.isExclude(filename)); + const filenames = includedFiles.filter((filename) => !exclude.isExclude(filename)); + + Debugger.it.logList('[bundle] files passed include filter', includedFiles); + Debugger.it.logList('[bundle] files removed by exclude filter', excludedByExclude); + Debugger.it.logList('[bundle] final target files', filenames); Spinner.it.succeed('analysis export statements completed!'); Spinner.it.stop(); diff --git a/src/modules/commands/creating.ts b/src/modules/commands/creating.ts index d3ad3de..5b3f29b 100644 --- a/src/modules/commands/creating.ts +++ b/src/modules/commands/creating.ts @@ -1,3 +1,4 @@ +import { Debugger } from '#/cli/ux/Debugger'; import { ProgressBar } from '#/cli/ux/ProgressBar'; import { Reasoner } from '#/cli/ux/Reasoner'; import { Spinner } from '#/cli/ux/Spinner'; @@ -60,6 +61,10 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption .map((sourceFile) => getCorrectCasedPath(sourceFile.getFilePath().toString())), ); + Debugger.it.log(`[create] project: ${createOption.project}`); + Debugger.it.log(`[create] projectDirPath: ${extendOptions.resolved.projectDirPath}`); + Debugger.it.logList('[create] ts-morph source files', filePaths); + const include = new IncludeContainer({ config: { include: getTsIncludeFiles({ config: createOption, extend: extendOptions }) }, cwd: extendOptions.resolved.projectDirPath, @@ -93,9 +98,13 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption inlineExcludeds, }); - const filenames = filePaths - .filter((filename) => include.isInclude(filename)) - .filter((filename) => !exclude.isExclude(filename)); + const includedFiles = filePaths.filter((filename) => include.isInclude(filename)); + const excludedByExclude = includedFiles.filter((filename) => exclude.isExclude(filename)); + const filenames = includedFiles.filter((filename) => !exclude.isExclude(filename)); + + Debugger.it.logList('[create] files passed include filter', includedFiles); + Debugger.it.logList('[create] files removed by exclude filter', excludedByExclude); + Debugger.it.logList('[create] final target files', filenames); Spinner.it.succeed('analysis export statements completed!'); Spinner.it.stop(); diff --git a/src/modules/commands/moduling.ts b/src/modules/commands/moduling.ts index b34e517..3ed354c 100644 --- a/src/modules/commands/moduling.ts +++ b/src/modules/commands/moduling.ts @@ -1,3 +1,4 @@ +import { Debugger } from '#/cli/ux/Debugger'; import { ProgressBar } from '#/cli/ux/ProgressBar'; import { Reasoner } from '#/cli/ux/Reasoner'; import { Spinner } from '#/cli/ux/Spinner'; @@ -36,6 +37,10 @@ export async function moduling(_buildOptions: TCommandBuildOptions, moduleOption const output = posixResolve(posixJoin(moduleOption.output, moduleOption.exportFilename)); + Debugger.it.log(`[module] project: ${moduleOption.project}`); + Debugger.it.log(`[module] projectDirPath: ${extendOptions.resolved.projectDirPath}`); + Debugger.it.logList('[module] tsconfig fileNames', extendOptions.tsconfig.fileNames); + const include = new IncludeContainer({ config: { include: getTsIncludeFiles({ config: moduleOption, extend: extendOptions }) }, cwd: extendOptions.resolved.projectDirPath, @@ -59,7 +64,13 @@ export async function moduling(_buildOptions: TCommandBuildOptions, moduleOption cwd: extendOptions.resolved.projectDirPath, }); - const filenames = include.files().filter((filename) => !exclude.isExclude(filename)); + const allIncludedFiles = include.files(); + const excludedByExclude = allIncludedFiles.filter((filename) => exclude.isExclude(filename)); + const filenames = allIncludedFiles.filter((filename) => !exclude.isExclude(filename)); + + Debugger.it.logList('[module] files from include.files()', allIncludedFiles); + Debugger.it.logList('[module] files removed by exclude filter', excludedByExclude); + Debugger.it.logList('[module] final target files', filenames); Spinner.it.succeed('analysis export statements completed!'); Spinner.it.stop(); diff --git a/src/modules/scope/ExcludeContainer.ts b/src/modules/scope/ExcludeContainer.ts index a0eba7a..06b293a 100644 --- a/src/modules/scope/ExcludeContainer.ts +++ b/src/modules/scope/ExcludeContainer.ts @@ -46,6 +46,14 @@ export class ExcludeContainer { : posixResolve(inlineExcluded.filePath); this.#inline.set(filePath, inlineExcluded); }); + + Debugger.it.log(`ExcludeContainer: cwd="${params.cwd}"`); + Debugger.it.logList('ExcludeContainer: patterns', params.config.exclude); + Debugger.it.logList( + 'ExcludeContainer: resolved files in map', + files.map(([key]) => key), + ); + Debugger.it.logList('ExcludeContainer: inline excludeds', Array.from(this.#inline.keys())); } get globs(): Readonly[]> { diff --git a/src/modules/scope/IncludeContainer.ts b/src/modules/scope/IncludeContainer.ts index bd6c67c..0db12db 100644 --- a/src/modules/scope/IncludeContainer.ts +++ b/src/modules/scope/IncludeContainer.ts @@ -28,15 +28,12 @@ export class IncludeContainer { this.#map = new Map(files); this.#globs = [globs]; - Debugger.it.log( - `IncludeContainer: cwd="${params.cwd}", patterns=${JSON.stringify(params.config.include)}`, + Debugger.it.log(`IncludeContainer: cwd="${params.cwd}"`); + Debugger.it.logList('IncludeContainer: patterns', params.config.include); + Debugger.it.logList( + 'IncludeContainer: resolved files in map', + files.map(([key]) => key), ); - Debugger.it.log(`IncludeContainer: ${files.length} files resolved into map`); - - if (files.length > 0) { - Debugger.it.log(`IncludeContainer: sample map keys (first 5):`); - files.slice(0, 5).forEach(([key]) => Debugger.it.log(` map key: "${key}"`)); - } } get globs(): Readonly[]> { From faa0770e19aea905a6e7410d81d8ba7327fd7bcd Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 12 Apr 2026 19:59:20 +0900 Subject: [PATCH 03/15] fix(include): treat empty include patterns as no filter When no include patterns are specified (neither via --include flag, .ctirc config, nor tsconfig.json include field), IncludeContainer was building an empty map and returning false for every file, causing 'Cannot find target files' on projects whose tsconfig.json omits the include field (e.g. examples/type04, examples/type05). - IncludeContainer.isInclude(): return true when map is empty so that an absent include configuration includes all source files, matching TypeScript compiler's default behaviour - Add a verbose debug log when no patterns are specified to aid diagnosis - Update the corresponding unit test to assert the corrected behaviour Co-Authored-By: Claude Sonnet 4.6 --- src/modules/scope/IncludeContainer.ts | 10 ++++++++-- src/modules/scope/__tests__/include.container.test.ts | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/modules/scope/IncludeContainer.ts b/src/modules/scope/IncludeContainer.ts index 0db12db..e273083 100644 --- a/src/modules/scope/IncludeContainer.ts +++ b/src/modules/scope/IncludeContainer.ts @@ -30,6 +30,11 @@ export class IncludeContainer { Debugger.it.log(`IncludeContainer: cwd="${params.cwd}"`); Debugger.it.logList('IncludeContainer: patterns', params.config.include); + + if (params.config.include.length === 0) { + Debugger.it.log('IncludeContainer: no patterns specified — all files will be included'); + } + Debugger.it.logList( 'IncludeContainer: resolved files in map', files.map(([key]) => key), @@ -46,8 +51,9 @@ export class IncludeContainer { isInclude(filePath: string): boolean { if (this.#map.size <= 0) { - Debugger.it.log(`isInclude("${filePath}"): map is empty => false`); - return false; + // No include patterns were specified — treat as "include everything" + Debugger.it.log(`isInclude("${filePath}"): map is empty => true (no include filter)`); + return true; } if (path.isAbsolute(filePath)) { diff --git a/src/modules/scope/__tests__/include.container.test.ts b/src/modules/scope/__tests__/include.container.test.ts index 34d2826..a4bd048 100644 --- a/src/modules/scope/__tests__/include.container.test.ts +++ b/src/modules/scope/__tests__/include.container.test.ts @@ -25,14 +25,19 @@ describe('IncludeContainer', () => { expect(container.map).toBeDefined(); }); - it('isInclude - no glob files', () => { + it('isInclude - no include patterns means include all files', () => { + // When no include patterns are specified (empty array), all files should pass the + // include check. This matches the TypeScript compiler default behaviour where an + // absent `include` field means "include everything". const container = new IncludeContainer({ config: { include: [] }, cwd: process.cwd(), }); const r01 = container.isInclude('src/files/IncludeContainer.ts'); - expect(r01).toBeFalsy(); + const r02 = container.isInclude('src/modules/scope/IncludeContainer.ts'); + expect(r01).toBeTruthy(); + expect(r02).toBeTruthy(); }); it('isInclude', () => { From 119ec7c11ab3cd635da0ebe460be5e75f5a5c9df Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 12 Apr 2026 20:10:44 +0900 Subject: [PATCH 04/15] fix(include): default to cwd-scoped glob when include is empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix returned true for all files when no include patterns were specified, which caused ctix to process every TypeScript file that ts-morph had loaded — including files from unrelated workspace packages outside the target directory. Instead, fall back to ['**/*.ts', '**/*.tsx'] resolved against the project cwd when include is empty. This keeps the scope limited to the project directory while preserving the TypeScript compiler's default behaviour of including all source files when no include field is declared. - IncludeContainer: derive default patterns from cwd rather than bypassing the map lookup entirely - Update unit test to assert the map is populated and a real project file passes the include check Co-Authored-By: Claude Sonnet 4.6 --- src/modules/scope/IncludeContainer.ts | 23 ++++++++++++++----- .../scope/__tests__/include.container.test.ts | 18 +++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/modules/scope/IncludeContainer.ts b/src/modules/scope/IncludeContainer.ts index e273083..7798ba4 100644 --- a/src/modules/scope/IncludeContainer.ts +++ b/src/modules/scope/IncludeContainer.ts @@ -17,7 +17,14 @@ export class IncludeContainer { #map: Map; constructor(params: { config: Pick; cwd: string }) { - const globs = new Glob(params.config.include, { + // When no include patterns are specified, fall back to all TypeScript source files + // under the project directory. This matches the TypeScript compiler's default behaviour + // (an absent `include` means "include everything") while keeping the scope limited + // to the project cwd so unrelated workspace packages are not pulled in. + const patterns = + params.config.include.length > 0 ? params.config.include : ['**/*.ts', '**/*.tsx']; + + const globs = new Glob(patterns, { absolute: true, ignore: defaultExclude, cwd: params.cwd, @@ -29,10 +36,15 @@ export class IncludeContainer { this.#globs = [globs]; Debugger.it.log(`IncludeContainer: cwd="${params.cwd}"`); - Debugger.it.logList('IncludeContainer: patterns', params.config.include); + Debugger.it.logList( + 'IncludeContainer: patterns', + params.config.include.length > 0 ? params.config.include : ['**/*.ts', '**/*.tsx (default)'], + ); if (params.config.include.length === 0) { - Debugger.it.log('IncludeContainer: no patterns specified — all files will be included'); + Debugger.it.log( + 'IncludeContainer: no patterns specified — defaulting to **/*.ts, **/*.tsx within cwd', + ); } Debugger.it.logList( @@ -51,9 +63,8 @@ export class IncludeContainer { isInclude(filePath: string): boolean { if (this.#map.size <= 0) { - // No include patterns were specified — treat as "include everything" - Debugger.it.log(`isInclude("${filePath}"): map is empty => true (no include filter)`); - return true; + Debugger.it.log(`isInclude("${filePath}"): map is empty => false`); + return false; } if (path.isAbsolute(filePath)) { diff --git a/src/modules/scope/__tests__/include.container.test.ts b/src/modules/scope/__tests__/include.container.test.ts index a4bd048..64d4946 100644 --- a/src/modules/scope/__tests__/include.container.test.ts +++ b/src/modules/scope/__tests__/include.container.test.ts @@ -25,19 +25,23 @@ describe('IncludeContainer', () => { expect(container.map).toBeDefined(); }); - it('isInclude - no include patterns means include all files', () => { - // When no include patterns are specified (empty array), all files should pass the - // include check. This matches the TypeScript compiler default behaviour where an - // absent `include` field means "include everything". + it('isInclude - no include patterns defaults to all ts/tsx under cwd', () => { + // When no include patterns are specified (empty array), IncludeContainer falls back to + // **/*.ts and **/*.tsx within the project cwd. Files that exist under cwd pass; + // the map is never empty so unrelated workspace packages are not pulled in. const container = new IncludeContainer({ config: { include: [] }, cwd: process.cwd(), }); - const r01 = container.isInclude('src/files/IncludeContainer.ts'); - const r02 = container.isInclude('src/modules/scope/IncludeContainer.ts'); + // map must not be empty — default patterns should have matched real project files + expect(container.map.size).toBeGreaterThan(0); + + // a real file that exists under cwd must be included + const r01 = container.isInclude( + posixJoin(process.cwd(), 'src/modules/scope/IncludeContainer.ts'), + ); expect(r01).toBeTruthy(); - expect(r02).toBeTruthy(); }); it('isInclude', () => { From cb19aecdb4e18dc0d2c91f142275b262539e1fbb Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 12 Apr 2026 20:26:02 +0900 Subject: [PATCH 05/15] feat(cwd): honour USE_INIT_CWD / INIT_CWD in getCwd() When ctix runs as an npm/pnpm script from a parent directory the script runner changes process.cwd() to the package root. pnpm sets INIT_CWD to the directory where the user originally invoked the command, and USE_INIT_CWD=true signals that ctix should use that value instead. - getCwd(): return process.env.INIT_CWD when USE_INIT_CWD=true so that all relative path resolution (config file lookup, project path, output path) is anchored to the user's intended directory - Replace every direct process.cwd() call in production source files with getCwd() so the override is applied consistently: getConfigFilePath, readConfigFromPackageJson, getDefaultInitAnswer, askInitOptions, askRemoveFiles, removing Co-Authored-By: Claude Sonnet 4.6 --- src/cli/questions/askInitOptions.ts | 5 +++-- src/cli/questions/askRemoveFiles.ts | 3 ++- src/configs/modules/getConfigFilePath.ts | 3 ++- src/configs/modules/getDefaultInitAnswer.ts | 5 +++-- src/configs/modules/readConfigFromPackageJson.ts | 3 ++- src/modules/commands/removing.ts | 11 ++++------- src/modules/path/getCwd.ts | 9 +++++++++ 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/cli/questions/askInitOptions.ts b/src/cli/questions/askInitOptions.ts index 5d4d64f..15acdaf 100644 --- a/src/cli/questions/askInitOptions.ts +++ b/src/cli/questions/askInitOptions.ts @@ -3,6 +3,7 @@ import { CE_CTIX_BUILD_MODE } from '#/configs/const-enum/CE_CTIX_BUILD_MODE'; import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE'; import { getTsconfigComparer } from '#/configs/modules/getTsconfigComparer'; import { getGlobFiles } from '#/modules/file/getGlobFiles'; +import { getCwd } from '#/modules/path/getCwd'; import { defaultExclude } from '#/modules/scope/defaultExclude'; import chalk from 'chalk'; import { exists } from 'find-up'; @@ -11,7 +12,7 @@ import inquirer from 'inquirer'; import pathe from 'pathe'; export async function askInitOptions(): Promise { - const cwd = process.cwd(); + const cwd = getCwd(); const cwdAnswer = await inquirer.prompt>([ { @@ -117,7 +118,7 @@ export async function askInitOptions(): Promise { overwirte: overwriteAnswer.overwirte, tsconfig: [], addEveryOptions: false, - packageJson: pathe.join(process.cwd(), CE_CTIX_DEFAULT_VALUE.PACKAGE_JSON_FILENAME), + packageJson: pathe.join(getCwd(), CE_CTIX_DEFAULT_VALUE.PACKAGE_JSON_FILENAME), mode: CE_CTIX_BUILD_MODE.BUNDLE_MODE, configPosition: '.ctirc', configComment: true, diff --git a/src/cli/questions/askRemoveFiles.ts b/src/cli/questions/askRemoveFiles.ts index 38f21c4..b1c0304 100644 --- a/src/cli/questions/askRemoveFiles.ts +++ b/src/cli/questions/askRemoveFiles.ts @@ -1,6 +1,7 @@ import type { IChoiceTypeItem } from '#/cli/interfaces/IChoiceTypeItem'; import { getRatioNumber } from '#/cli/modules/getRatioNumber'; import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE'; +import { getCwd } from '#/modules/path/getCwd'; import { posixRelative } from '#/modules/path/modules/posixRelative'; import Fuse from 'fuse.js'; import inquirer from 'inquirer'; @@ -12,7 +13,7 @@ export async function askRemoveFiles(filePaths: string[]) { const choiceAbleTypes = filePaths.map((filePath) => { return { filePath, - name: posixRelative(process.cwd(), filePath), + name: posixRelative(getCwd(), filePath), value: filePath, } satisfies IChoiceTypeItem; }); diff --git a/src/configs/modules/getConfigFilePath.ts b/src/configs/modules/getConfigFilePath.ts index 698b4fb..30785e0 100644 --- a/src/configs/modules/getConfigFilePath.ts +++ b/src/configs/modules/getConfigFilePath.ts @@ -1,3 +1,4 @@ +import { getCwd } from '#/modules/path/getCwd'; import { exists } from 'my-node-fp'; import pathe from 'pathe'; @@ -6,7 +7,7 @@ export async function getConfigFilePath(fileName: string, configFilePath?: strin return configFilePath; } - const cwdConfigFilePath = pathe.join(process.cwd(), fileName); + const cwdConfigFilePath = pathe.join(getCwd(), fileName); if (await exists(cwdConfigFilePath)) { return cwdConfigFilePath; diff --git a/src/configs/modules/getDefaultInitAnswer.ts b/src/configs/modules/getDefaultInitAnswer.ts index 1275ff0..d699bc7 100644 --- a/src/configs/modules/getDefaultInitAnswer.ts +++ b/src/configs/modules/getDefaultInitAnswer.ts @@ -3,12 +3,13 @@ import { CE_CTIX_BUILD_MODE } from '#/configs/const-enum/CE_CTIX_BUILD_MODE'; import { CE_CTIX_DEFAULT_VALUE } from '#/configs/const-enum/CE_CTIX_DEFAULT_VALUE'; import { getTsconfigComparer } from '#/configs/modules/getTsconfigComparer'; import { getGlobFiles } from '#/modules/file/getGlobFiles'; +import { getCwd } from '#/modules/path/getCwd'; import { defaultExclude } from '#/modules/scope/defaultExclude'; import { Glob } from 'glob'; import pathe from 'pathe'; export async function getDefaultInitAnswer(): Promise { - const cwd = process.cwd(); + const cwd = getCwd(); const glob = new Glob(['**/tsconfig.json', '**/tsconfig.*.json'], { cwd, ignore: defaultExclude, @@ -24,7 +25,7 @@ export async function getDefaultInitAnswer(): Promise { const answer: IInitQuestionAnswer = { cwd, tsconfig: [tsconfigPath], - packageJson: pathe.join(process.cwd(), CE_CTIX_DEFAULT_VALUE.PACKAGE_JSON_FILENAME), + packageJson: pathe.join(getCwd(), CE_CTIX_DEFAULT_VALUE.PACKAGE_JSON_FILENAME), mode: CE_CTIX_BUILD_MODE.BUNDLE_MODE, exportFilename: CE_CTIX_DEFAULT_VALUE.EXPORT_FILENAME, addEveryOptions: false, diff --git a/src/configs/modules/readConfigFromPackageJson.ts b/src/configs/modules/readConfigFromPackageJson.ts index 6ca301b..c87d92d 100644 --- a/src/configs/modules/readConfigFromPackageJson.ts +++ b/src/configs/modules/readConfigFromPackageJson.ts @@ -1,3 +1,4 @@ +import { getCwd } from '#/modules/path/getCwd'; import fs from 'fs'; import { isError } from 'my-easy-fp'; import { type PassFailEither, fail, pass } from 'my-only-either'; @@ -8,7 +9,7 @@ export async function readConfigFromPackageJson(): Promise< PassFailEither> > { try { - const packageJsonFilePath = pathe.join(process.cwd(), 'package.json'); + const packageJsonFilePath = pathe.join(getCwd(), 'package.json'); const buf = await fs.promises.readFile(packageJsonFilePath); const packageJson = JSON.parse(buf.toString()) as PackageJson; diff --git a/src/modules/commands/removing.ts b/src/modules/commands/removing.ts index d9c123a..e5e3bfd 100644 --- a/src/modules/commands/removing.ts +++ b/src/modules/commands/removing.ts @@ -5,6 +5,7 @@ import type { TCommandBuildOptions } from '#/configs/interfaces/TCommandBuildOpt import type { TCommandRemoveOptions } from '#/configs/interfaces/TCommandRemoveOptions'; import { getRemoveFileGlobPattern } from '#/modules/file/getRemoveFileGlobPattern'; import { unlinks } from '#/modules/file/unlinks'; +import { getCwd } from '#/modules/path/getCwd'; import { posixRelative } from '#/modules/path/modules/posixRelative'; import { IncludeContainer } from '#/modules/scope/IncludeContainer'; import chalk from 'chalk'; @@ -18,7 +19,7 @@ export async function removing( const include = new IncludeContainer({ config: { include: patterns.map((projectDir) => projectDir.pattern) }, - cwd: process.cwd(), + cwd: getCwd(), }); const filePaths = include.files(); @@ -36,9 +37,7 @@ export async function removing( await filePaths.reduce(async (prevHandle: Promise, filePath: string) => { const handle = async () => { - Spinner.it.succeed( - `${chalk.redBright('removed:')} ${posixRelative(process.cwd(), filePath)}`, - ); + Spinner.it.succeed(`${chalk.redBright('removed:')} ${posixRelative(getCwd(), filePath)}`); }; await prevHandle; @@ -61,9 +60,7 @@ export async function removing( await filePaths.reduce(async (prevHandle: Promise, filePath: string) => { const handle = async () => { - Spinner.it.succeed( - `${chalk.redBright('removed:')} ${posixRelative(process.cwd(), filePath)}`, - ); + Spinner.it.succeed(`${chalk.redBright('removed:')} ${posixRelative(getCwd(), filePath)}`); }; await prevHandle; diff --git a/src/modules/path/getCwd.ts b/src/modules/path/getCwd.ts index 3230134..f2cbad7 100644 --- a/src/modules/path/getCwd.ts +++ b/src/modules/path/getCwd.ts @@ -1,4 +1,13 @@ // for safety testing export function getCwd() { + // When running as an npm/pnpm script from a parent directory, the script runner + // changes process.cwd() to the package root. pnpm/npm sets INIT_CWD to the + // directory where the user originally invoked the command. + // USE_INIT_CWD=true tells ctix to resolve paths against INIT_CWD instead of + // the package root so that relative paths work as the user expects. + if (process.env.USE_INIT_CWD === 'true' && process.env.INIT_CWD != null) { + return process.env.INIT_CWD; + } + return process.cwd(); } From f88f1e906751758d9a3c772a7d46ef541b214e5d Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 12 Apr 2026 20:30:40 +0900 Subject: [PATCH 06/15] fix(cwd): use getCwd() as base in posixResolve and createBuildOptions getCwd() already returns INIT_CWD when USE_INIT_CWD=true, but path.resolve() calls throughout the codebase were still anchored to the real process.cwd() (the package root when running via pnpm scripts). - posixResolve: pass getCwd() as the base directory when resolving relative paths so all callers automatically respect USE_INIT_CWD - createBuildOptions: replace bare path.resolve(project) with path.resolve(getCwd(), project) for the same reason Co-Authored-By: Claude Sonnet 4.6 --- src/configs/transforms/createBuildOptions.ts | 9 +++++---- src/modules/path/modules/posixResolve.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/configs/transforms/createBuildOptions.ts b/src/configs/transforms/createBuildOptions.ts index 392c1c5..3f70866 100644 --- a/src/configs/transforms/createBuildOptions.ts +++ b/src/configs/transforms/createBuildOptions.ts @@ -14,6 +14,7 @@ import { transformCreateMode } from '#/configs/transforms/transformCreateMode'; import { transformModuleMode } from '#/configs/transforms/transformModuleMode'; import { getTsExcludeFiles } from '#/modules/file/getTsExcludeFiles'; import { getTsIncludeFiles } from '#/modules/file/getTsIncludeFiles'; +import { getCwd } from '#/modules/path/getCwd'; import { toArray } from 'my-easy-fp'; import path from 'node:path'; import type { ArgumentsCamelCase } from 'yargs'; @@ -51,7 +52,7 @@ export async function createBuildOptions( options.options = await Promise.all( options.options.map(async (option) => { if (option.mode === CE_CTIX_BUILD_MODE.MODULE_MODE) { - const projectPath = path.resolve(option.project); + const projectPath = path.resolve(getCwd(), option.project); const tsconfig = getTypeScriptConfig(projectPath); const moduleMode = await transformModuleMode( @@ -85,7 +86,7 @@ export async function createBuildOptions( } if (option.mode === CE_CTIX_BUILD_MODE.CREATE_MODE) { - const projectPath = path.resolve(option.project); + const projectPath = path.resolve(getCwd(), option.project); const tsconfig = getTypeScriptConfig(projectPath); const createMode = await transformCreateMode( @@ -118,7 +119,7 @@ export async function createBuildOptions( return createMode; } - const projectPath = path.resolve(option.project); + const projectPath = path.resolve(getCwd(), option.project); const tsconfig = getTypeScriptConfig(projectPath); const bundleMode = transformBundleMode( @@ -154,7 +155,7 @@ export async function createBuildOptions( return options; } - const projectPath = path.resolve(argv.project); + const projectPath = path.resolve(getCwd(), argv.project); const tsconfig = getTypeScriptConfig(projectPath); const include = diff --git a/src/modules/path/modules/posixResolve.ts b/src/modules/path/modules/posixResolve.ts index 139fa28..b25b269 100644 --- a/src/modules/path/modules/posixResolve.ts +++ b/src/modules/path/modules/posixResolve.ts @@ -1,6 +1,14 @@ +import { getCwd } from '#/modules/path/getCwd'; import { replaceSepToPosix } from 'my-node-fp'; import * as path from 'node:path'; export function posixResolve(targetPath: string): string { - return replaceSepToPosix(path.resolve(targetPath)); + // Use getCwd() as the base for relative paths so that USE_INIT_CWD / INIT_CWD + // is respected throughout the codebase. Absolute paths are returned as-is + // (after separator normalisation) because they have their own explicit root. + if (path.isAbsolute(targetPath)) { + return replaceSepToPosix(targetPath); + } + + return replaceSepToPosix(path.resolve(getCwd(), targetPath)); } From 15e66568a620526cda91667561c0932b31adb6f0 Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 12 Apr 2026 20:39:36 +0900 Subject: [PATCH 07/15] fix(cwd): propagate resolved project path to transform functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ctix resolves the project path with getCwd() as the base, the resulting absolute path was not forwarded to transformBundleMode, transformCreateMode, or transformModuleMode. Each transform kept the original relative string (e.g. 'tsconfig.json') as bundleOption.project, which ProjectContainer then re-resolved against process.cwd() (the package root) — loading the wrong tsconfig and pulling in hundreds of unrelated source files. Build a resolvedArgv with the absolute project path and pass it to all three transform calls in the non-config-file code path. Co-Authored-By: Claude Sonnet 4.6 --- src/configs/transforms/createBuildOptions.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/configs/transforms/createBuildOptions.ts b/src/configs/transforms/createBuildOptions.ts index 3f70866..1ab1686 100644 --- a/src/configs/transforms/createBuildOptions.ts +++ b/src/configs/transforms/createBuildOptions.ts @@ -182,10 +182,15 @@ export async function createBuildOptions( const mode = argv.mode ?? CE_CTIX_BUILD_MODE.BUNDLE_MODE; + // Pass the resolved absolute projectPath so that downstream functions + // (ProjectContainer, getExtendOptions, etc.) do not re-resolve the relative + // path against process.cwd() instead of getCwd(). + const resolvedArgv = { ...argv, project: projectPath }; + if (mode === CE_CTIX_BUILD_MODE.CREATE_MODE) { options.options = [ - await transformCreateMode(argv, { - ...argv, + await transformCreateMode(resolvedArgv, { + ...resolvedArgv, mode: CE_CTIX_BUILD_MODE.CREATE_MODE, include, exclude, @@ -195,12 +200,12 @@ export async function createBuildOptions( return options; } - const output = getOutputValue(argv, { output: argv.output }); + const output = getOutputValue(resolvedArgv, { output: argv.output }); if (mode === CE_CTIX_BUILD_MODE.MODULE_MODE) { options.options = [ - await transformModuleMode(argv, { - ...argv, + await transformModuleMode(resolvedArgv, { + ...resolvedArgv, mode: CE_CTIX_BUILD_MODE.MODULE_MODE, include, exclude, @@ -211,8 +216,8 @@ export async function createBuildOptions( } options.options = [ - transformBundleMode(argv, { - ...argv, + transformBundleMode(resolvedArgv, { + ...resolvedArgv, mode: CE_CTIX_BUILD_MODE.BUNDLE_MODE, output, include, From d683a64bab2adf60cb1745d1306504276a0c6159 Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sun, 12 Apr 2026 22:21:34 +0900 Subject: [PATCH 08/15] fix(exclude): convert absolute output path to relative before passing to ExcludeContainer Glob patterns with absolute paths (especially Windows drive-letter paths like C:/path/to/index.ts) are not reliably matched when a cwd is also provided. Convert the output file path to a relative path from projectDirPath using posixRelative() before adding it to the ExcludeContainer pattern list, so glob can resolve it correctly on all platforms. Co-Authored-By: Claude Sonnet 4.6 --- src/modules/commands/bundling.ts | 6 +++++- src/modules/commands/creating.ts | 4 +++- src/modules/commands/moduling.ts | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/modules/commands/bundling.ts b/src/modules/commands/bundling.ts index 5b3329a..af6498e 100644 --- a/src/modules/commands/bundling.ts +++ b/src/modules/commands/bundling.ts @@ -20,6 +20,7 @@ import { getTsExcludeFiles } from '#/modules/file/getTsExcludeFiles'; import { getTsIncludeFiles } from '#/modules/file/getTsIncludeFiles'; import { getCorrectCasedPath } from '#/modules/path/getCorrectCasedPath'; import { posixJoin } from '#/modules/path/modules/posixJoin'; +import { posixRelative } from '#/modules/path/modules/posixRelative'; import { posixResolve } from '#/modules/path/modules/posixResolve'; import { ExcludeContainer } from '#/modules/scope/ExcludeContainer'; import { IncludeContainer } from '#/modules/scope/IncludeContainer'; @@ -86,7 +87,10 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: */ const exclude = new ExcludeContainer({ config: { - exclude: [...getTsExcludeFiles({ config: bundleOption, extend: extendOptions }), ...[output]], + exclude: [ + ...getTsExcludeFiles({ config: bundleOption, extend: extendOptions }), + posixRelative(extendOptions.resolved.projectDirPath, output), + ], }, inlineExcludeds, cwd: extendOptions.resolved.projectDirPath, diff --git a/src/modules/commands/creating.ts b/src/modules/commands/creating.ts index 5b3f29b..0b2504d 100644 --- a/src/modules/commands/creating.ts +++ b/src/modules/commands/creating.ts @@ -91,7 +91,9 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption config: { exclude: [ ...getTsExcludeFiles({ config: createOption, extend: extendOptions }), - ...outputExcludeds, + ...outputExcludeds.map((absPath) => + posixRelative(extendOptions.resolved.projectDirPath, absPath), + ), ], }, cwd: extendOptions.resolved.projectDirPath, diff --git a/src/modules/commands/moduling.ts b/src/modules/commands/moduling.ts index 3ed354c..f31635b 100644 --- a/src/modules/commands/moduling.ts +++ b/src/modules/commands/moduling.ts @@ -12,6 +12,7 @@ import { checkOutputFile } from '#/modules/file/checkOutputFile'; import { getTsExcludeFiles } from '#/modules/file/getTsExcludeFiles'; import { getTsIncludeFiles } from '#/modules/file/getTsIncludeFiles'; import { posixJoin } from '#/modules/path/modules/posixJoin'; +import { posixRelative } from '#/modules/path/modules/posixRelative'; import { posixResolve } from '#/modules/path/modules/posixResolve'; import { ExcludeContainer } from '#/modules/scope/ExcludeContainer'; import { IncludeContainer } from '#/modules/scope/IncludeContainer'; @@ -58,7 +59,10 @@ export async function moduling(_buildOptions: TCommandBuildOptions, moduleOption */ const exclude = new ExcludeContainer({ config: { - exclude: [...getTsExcludeFiles({ config: moduleOption, extend: extendOptions }), ...[output]], + exclude: [ + ...getTsExcludeFiles({ config: moduleOption, extend: extendOptions }), + posixRelative(extendOptions.resolved.projectDirPath, output), + ], }, inlineExcludeds, cwd: extendOptions.resolved.projectDirPath, From bcc81cb2422e3f103da598b18cd16c3328169f08 Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Thu, 16 Apr 2026 00:23:24 +0900 Subject: [PATCH 09/15] fix(regression): restore 2.7.2 compatibility for Windows and empty output path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three regressions introduced after 2.7.2 are fixed: 1. posixResolve: revert getCwd() usage back to path.resolve() so that resolving relative paths is always anchored to the real process cwd, not a potentially empty/unexpected INIT_CWD value that caused path.resolve("", "./index.ts") → "/index.ts" (EROFS on macOS, wrong root on Windows). 2. bundling / moduling: guard against empty output option before joining with exportFilename. posixJoin("", "index.ts") produces "/index.ts" on every platform; fall back to projectDirPath when output is empty. 3. getTsIncludeFiles: restore the tsconfig.fileNames fallback that was removed in 3b60d38. Without it, projects that specify no explicit include in their tsconfig resolve to [] and nothing runs — the exact Windows "0 applicable files" regression reported against 2.7.5. Co-Authored-By: Claude Sonnet 4.6 --- src/modules/commands/bundling.ts | 5 ++++- src/modules/commands/moduling.ts | 5 ++++- src/modules/file/getTsIncludeFiles.ts | 15 ++++++++++++++- src/modules/path/modules/posixResolve.ts | 10 +--------- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/modules/commands/bundling.ts b/src/modules/commands/bundling.ts index af6498e..d9f04f6 100644 --- a/src/modules/commands/bundling.ts +++ b/src/modules/commands/bundling.ts @@ -59,7 +59,10 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: Spinner.it.succeed(`[${bundleOption.project}] loading complete!`); Spinner.it.update('include, exclude config'); - const output = posixResolve(posixJoin(bundleOption.output, bundleOption.exportFilename)); + // Guard: if bundleOption.output is empty, fall back to the project directory. + // posixJoin("", "x") produces "/x" (root) on all platforms, which is wrong. + const outputDir = bundleOption.output || extendOptions.resolved.projectDirPath; + const output = posixResolve(posixJoin(outputDir, bundleOption.exportFilename)); const filePaths = await Promise.all( project .getSourceFiles() diff --git a/src/modules/commands/moduling.ts b/src/modules/commands/moduling.ts index f31635b..54bf634 100644 --- a/src/modules/commands/moduling.ts +++ b/src/modules/commands/moduling.ts @@ -36,7 +36,10 @@ export async function moduling(_buildOptions: TCommandBuildOptions, moduleOption Spinner.it.succeed(`[${moduleOption.project}] loading complete!`); Spinner.it.update('include, exclude config'); - const output = posixResolve(posixJoin(moduleOption.output, moduleOption.exportFilename)); + // Guard: if moduleOption.output is empty, fall back to the project directory. + // posixJoin("", "x") produces "/x" (root) on all platforms, which is wrong. + const outputDir = moduleOption.output || extendOptions.resolved.projectDirPath; + const output = posixResolve(posixJoin(outputDir, moduleOption.exportFilename)); Debugger.it.log(`[module] project: ${moduleOption.project}`); Debugger.it.log(`[module] projectDirPath: ${extendOptions.resolved.projectDirPath}`); diff --git a/src/modules/file/getTsIncludeFiles.ts b/src/modules/file/getTsIncludeFiles.ts index f104435..6a46a32 100644 --- a/src/modules/file/getTsIncludeFiles.ts +++ b/src/modules/file/getTsIncludeFiles.ts @@ -1,6 +1,7 @@ import { getInheritedFileScope } from '#/compilers/getInheritedFileScope'; import type { IExtendOptions } from '#/configs/interfaces/IExtendOptions'; import type { IModeGenerateOptions } from '#/configs/interfaces/IModeGenerateOptions'; +import { isDescendant } from 'my-node-fp'; export function getTsIncludeFiles(config: { config: Pick; @@ -14,5 +15,17 @@ export function getTsIncludeFiles(config: { const { include } = getInheritedFileScope(config.extend.resolved.projectFilePath); - return include; + if (include.length > 0) { + return include; + } + + // Fallback: use the TypeScript compiler's resolved file list filtered to files + // that are descendants of the project directory. This matches 2.7.2 behaviour + // and works reliably on both Windows and macOS because tsconfig.fileNames + // contains the actual absolute paths the compiler already resolved. + const filePaths = config.extend.tsconfig.fileNames.filter((filePath) => + isDescendant(config.extend.resolved.projectDirPath, filePath), + ); + + return filePaths; } diff --git a/src/modules/path/modules/posixResolve.ts b/src/modules/path/modules/posixResolve.ts index b25b269..139fa28 100644 --- a/src/modules/path/modules/posixResolve.ts +++ b/src/modules/path/modules/posixResolve.ts @@ -1,14 +1,6 @@ -import { getCwd } from '#/modules/path/getCwd'; import { replaceSepToPosix } from 'my-node-fp'; import * as path from 'node:path'; export function posixResolve(targetPath: string): string { - // Use getCwd() as the base for relative paths so that USE_INIT_CWD / INIT_CWD - // is respected throughout the codebase. Absolute paths are returned as-is - // (after separator normalisation) because they have their own explicit root. - if (path.isAbsolute(targetPath)) { - return replaceSepToPosix(targetPath); - } - - return replaceSepToPosix(path.resolve(getCwd(), targetPath)); + return replaceSepToPosix(path.resolve(targetPath)); } From 35c99362b543cda7957a347d70608d4983503a3b Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Thu, 16 Apr 2026 11:41:33 +0900 Subject: [PATCH 10/15] feat(path): batch-correct source file casing with buildCorrectCasePathMap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the per-file getCorrectCasedPath O(N*D) readdir approach with a batched buildCorrectCasePathMap that groups paths by parent directory and reads each directory exactly once via Promise.all, reducing I/O from O(N*D) to O(unique dirs). Three matching problems that would arise after case-correction are fixed: 1. project.getSourceFile(correctedPath) can return null because ts-morph registers files under their original (possibly wrong-cased) path. Fix: build a correctedPath → SourceFile map from the caseMap result and use it instead of project.getSourceFile() in bundling/creating. 2. ExcludeContainer inline map key mismatch: getInlineCommentedFiles returns filePath via sourceFile.getFilePath() (original path), so the ExcludeContainer #inline map had original-cased keys while isExclude() was called with corrected paths. Fix: pass rawFilePaths to getInlineCommentedFiles and remap returned filePath values through caseMap before passing to ExcludeContainer. 3. inlineDeclarations filePath mismatch: same root cause as (2) — the include.isInclude / exclude.isExclude filters received original-cased paths that did not match corrected-path keys in both containers. Fix: apply caseMap remapping before the filter chain. Co-Authored-By: Claude Sonnet 4.6 --- src/modules/commands/bundling.ts | 35 +++-- src/modules/commands/creating.ts | 30 ++-- .../build.correct.case.path.map.test.ts | 128 ++++++++++++++++++ src/modules/path/buildCorrectCasePathMap.ts | 127 +++++++++++++++++ 4 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 src/modules/path/__tests__/build.correct.case.path.map.test.ts create mode 100644 src/modules/path/buildCorrectCasePathMap.ts diff --git a/src/modules/commands/bundling.ts b/src/modules/commands/bundling.ts index d9f04f6..a7af285 100644 --- a/src/modules/commands/bundling.ts +++ b/src/modules/commands/bundling.ts @@ -18,7 +18,7 @@ import { ProjectContainer } from '#/modules/file/ProjectContainer'; import { checkOutputFile } from '#/modules/file/checkOutputFile'; import { getTsExcludeFiles } from '#/modules/file/getTsExcludeFiles'; import { getTsIncludeFiles } from '#/modules/file/getTsIncludeFiles'; -import { getCorrectCasedPath } from '#/modules/path/getCorrectCasedPath'; +import { buildCorrectCasePathMap } from '#/modules/path/buildCorrectCasePathMap'; import { posixJoin } from '#/modules/path/modules/posixJoin'; import { posixRelative } from '#/modules/path/modules/posixRelative'; import { posixResolve } from '#/modules/path/modules/posixResolve'; @@ -63,10 +63,21 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: // posixJoin("", "x") produces "/x" (root) on all platforms, which is wrong. const outputDir = bundleOption.output || extendOptions.resolved.projectDirPath; const output = posixResolve(posixJoin(outputDir, bundleOption.exportFilename)); - const filePaths = await Promise.all( - project - .getSourceFiles() - .map((sourceFile) => getCorrectCasedPath(sourceFile.getFilePath().toString())), + const rawFilePaths = project + .getSourceFiles() + .map((sourceFile) => sourceFile.getFilePath().toString()); + const caseMap = await buildCorrectCasePathMap(rawFilePaths); + const filePaths = rawFilePaths.map((p) => caseMap.get(p) ?? p); + + // Build correctedPath → SourceFile map so lookups remain correct after case + // correction. ts-morph registers files by their original (possibly wrong-cased) + // path, so project.getSourceFile(correctedPath) can return null on + // case-sensitive systems or strict ts-morph builds. + const sourceFileMap = new Map( + project.getSourceFiles().map((sf) => { + const original = sf.getFilePath().toString(); + return [caseMap.get(original) ?? original, sf]; + }), ); Debugger.it.log(`[bundle] project: ${bundleOption.project}`); @@ -78,11 +89,14 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: cwd: extendOptions.resolved.projectDirPath, }); + // Pass rawFilePaths so project.getSourceFile() inside uses original registered + // paths. Remap the returned filePath values through caseMap so that + // ExcludeContainer stores corrected-path keys and isExclude() matches correctly. const inlineExcludeds = getInlineCommentedFiles({ project, - filePaths, + filePaths: rawFilePaths, keyword: CE_INLINE_COMMENT_KEYWORD.FILE_EXCLUDE_KEYWORD, - }); + }).map((item) => ({ ...item, filePath: caseMap.get(item.filePath) ?? item.filePath })); /** * SourceCode를 읽어서 inline file exclude 된 파일을 별도로 전달한다. 이렇게 하는 이유는, 이 파일은 왜 포함되지 @@ -101,13 +115,14 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: const inlineDeclarations = getInlineCommentedFiles({ project, - filePaths, + filePaths: rawFilePaths, keyword: CE_INLINE_COMMENT_KEYWORD.FILE_DECLARATION_KEYWORD, }) + .map((item) => ({ ...item, filePath: caseMap.get(item.filePath) ?? item.filePath })) .filter((declaration) => include.isInclude(declaration.filePath)) .filter((declaration) => !exclude.isExclude(declaration.filePath)) .filter((declaration) => { - const sourceFile = project.getSourceFile(declaration.filePath); + const sourceFile = sourceFileMap.get(declaration.filePath); if (sourceFile == null) { return false; } @@ -140,7 +155,7 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: const statementEithers = ( await Promise.all( filenames - .map((filename) => project.getSourceFile(filename)) + .map((filename) => sourceFileMap.get(filename)) .filter((sourceFile): sourceFile is tsm.SourceFile => sourceFile != null) .map(async (sourceFile) => { ProgressBar.it.increment(); diff --git a/src/modules/commands/creating.ts b/src/modules/commands/creating.ts index 0b2504d..a3274ad 100644 --- a/src/modules/commands/creating.ts +++ b/src/modules/commands/creating.ts @@ -23,7 +23,7 @@ import { getTsExcludeFiles } from '#/modules/file/getTsExcludeFiles'; import { getTsIncludeFiles } from '#/modules/file/getTsIncludeFiles'; import { processSkipEmptyDirOnFileTree } from '#/modules/file/processSkipEmptyDirOnFileTree'; import { addCurrentDirPrefix } from '#/modules/path/addCurrentDirPrefix'; -import { getCorrectCasedPath } from '#/modules/path/getCorrectCasedPath'; +import { buildCorrectCasePathMap } from '#/modules/path/buildCorrectCasePathMap'; import { getImportStatementExtname } from '#/modules/path/getImportStatementExtname'; import { posixJoin } from '#/modules/path/modules/posixJoin'; import { posixRelative } from '#/modules/path/modules/posixRelative'; @@ -55,10 +55,21 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption Spinner.it.succeed(`${createOption.project} loading complete!`); Spinner.it.update('include, exclude config'); - const filePaths = await Promise.all( - project - .getSourceFiles() - .map((sourceFile) => getCorrectCasedPath(sourceFile.getFilePath().toString())), + const rawFilePaths = project + .getSourceFiles() + .map((sourceFile) => sourceFile.getFilePath().toString()); + const caseMap = await buildCorrectCasePathMap(rawFilePaths); + const filePaths = rawFilePaths.map((p) => caseMap.get(p) ?? p); + + // Build correctedPath → SourceFile map so lookups remain correct after case + // correction. ts-morph registers files by their original (possibly wrong-cased) + // path, so project.getSourceFile(correctedPath) can return null on + // case-sensitive systems or strict ts-morph builds. + const sourceFileMap = new Map( + project.getSourceFiles().map((sf) => { + const original = sf.getFilePath().toString(); + return [caseMap.get(original) ?? original, sf]; + }), ); Debugger.it.log(`[create] project: ${createOption.project}`); @@ -70,11 +81,14 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption cwd: extendOptions.resolved.projectDirPath, }); + // Pass rawFilePaths so project.getSourceFile() inside uses original registered + // paths. Remap the returned filePath values through caseMap so that + // ExcludeContainer stores corrected-path keys and isExclude() matches correctly. const inlineExcludeds = getInlineCommentedFiles({ project, - filePaths, + filePaths: rawFilePaths, keyword: CE_INLINE_COMMENT_KEYWORD.FILE_EXCLUDE_KEYWORD, - }); + }).map((item) => ({ ...item, filePath: caseMap.get(item.filePath) ?? item.filePath })); const outputExcludeds = await getOutputExcludedFiles({ project, @@ -124,7 +138,7 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption const statementEithers = await Promise.all( filenames - .map((filename) => project.getSourceFile(filename)) + .map((filename) => sourceFileMap.get(filename)) .filter((sourceFile): sourceFile is tsm.SourceFile => sourceFile != null) .map(async (sourceFile) => { ProgressBar.it.increment(); diff --git a/src/modules/path/__tests__/build.correct.case.path.map.test.ts b/src/modules/path/__tests__/build.correct.case.path.map.test.ts new file mode 100644 index 0000000..b61ee1e --- /dev/null +++ b/src/modules/path/__tests__/build.correct.case.path.map.test.ts @@ -0,0 +1,128 @@ +import { buildCorrectCasePathMap } from '#/modules/path/buildCorrectCasePathMap'; +import fs from 'node:fs'; +import os from 'node:os'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('node:fs', () => ({ + default: { + promises: { + readdir: vi.fn(), + }, + }, +})); + +describe('buildCorrectCasePathMap', () => { + const mockReaddir = vi.mocked(fs.promises.readdir); + + beforeEach(() => { + mockReaddir.mockReset(); + }); + + it('should return a map with posix paths for files in a single directory', async () => { + mockReaddir.mockResolvedValueOnce(['MyFile.ts', 'OtherFile.ts'] as any); + + const inputs = ['/project/src/myfile.ts', '/project/src/otherfile.ts']; + const result = await buildCorrectCasePathMap(inputs); + + expect(result.get('/project/src/myfile.ts')).toBe('/project/src/MyFile.ts'); + expect(result.get('/project/src/otherfile.ts')).toBe('/project/src/OtherFile.ts'); + }); + + it('should read each unique directory only once', async () => { + mockReaddir + .mockResolvedValueOnce(['Alpha.ts', 'Beta.ts'] as any) + .mockResolvedValueOnce(['Gamma.ts'] as any); + + const inputs = ['/project/src/alpha.ts', '/project/src/beta.ts', '/project/lib/gamma.ts']; + await buildCorrectCasePathMap(inputs); + + expect(mockReaddir).toHaveBeenCalledTimes(2); + }); + + it('should keep original path when basename is not found in directory', async () => { + mockReaddir.mockResolvedValueOnce(['OtherFile.ts'] as any); + + const inputs = ['/project/src/missing.ts']; + const result = await buildCorrectCasePathMap(inputs); + + expect(result.get('/project/src/missing.ts')).toBe('/project/src/missing.ts'); + }); + + it('should keep original path when directory is not readable', async () => { + mockReaddir.mockRejectedValueOnce(new Error('ENOENT: no such file or directory')); + + const inputs = ['/nonexistent/src/file.ts']; + const result = await buildCorrectCasePathMap(inputs); + + expect(result.get('/nonexistent/src/file.ts')).toBe('/nonexistent/src/file.ts'); + }); + + it('should return posix-style paths (forward slashes only)', async () => { + mockReaddir.mockResolvedValueOnce(['MyComponent.ts'] as any); + + const inputs = ['/project/src/mycomponent.ts']; + const result = await buildCorrectCasePathMap(inputs); + + const value = result.get('/project/src/mycomponent.ts'); + expect(value).toBeDefined(); + expect(value).not.toContain('\\'); + expect(value).toBe('/project/src/MyComponent.ts'); + }); + + it('should handle empty input array', async () => { + const result = await buildCorrectCasePathMap([]); + + expect(result.size).toBe(0); + expect(mockReaddir).not.toHaveBeenCalled(); + }); + + it('should detect case-conflicting TypeScript files on case-insensitive platforms and exit', async () => { + const platformSpy = vi.spyOn(os, 'platform').mockReturnValue('darwin'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + // Two files that differ only in case → conflict + mockReaddir.mockResolvedValueOnce(['Aa.ts', 'aA.ts'] as any); + + const inputs = ['/project/src/Aa.ts', '/project/src/aA.ts']; + await buildCorrectCasePathMap(inputs); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(consoleSpy).toHaveBeenCalled(); + + platformSpy.mockRestore(); + exitSpy.mockRestore(); + consoleSpy.mockRestore(); + }); + + it('should not report conflict for non-TypeScript files with same lower-case name', async () => { + const platformSpy = vi.spyOn(os, 'platform').mockReturnValue('darwin'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + + // image.PNG and image.png are not TypeScript files → no conflict + mockReaddir.mockResolvedValueOnce(['MyFile.ts', 'image.PNG', 'image.png'] as any); + + const inputs = ['/project/src/myfile.ts']; + await buildCorrectCasePathMap(inputs); + + expect(exitSpy).not.toHaveBeenCalled(); + + platformSpy.mockRestore(); + exitSpy.mockRestore(); + }); + + it('should not exit on case-sensitive platforms even with same-lower-case filenames', async () => { + const platformSpy = vi.spyOn(os, 'platform').mockReturnValue('linux'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + + mockReaddir.mockResolvedValueOnce(['Aa.ts', 'aA.ts'] as any); + + const inputs = ['/project/src/Aa.ts']; + await buildCorrectCasePathMap(inputs); + + expect(exitSpy).not.toHaveBeenCalled(); + + platformSpy.mockRestore(); + exitSpy.mockRestore(); + }); +}); diff --git a/src/modules/path/buildCorrectCasePathMap.ts b/src/modules/path/buildCorrectCasePathMap.ts new file mode 100644 index 0000000..3fc741c --- /dev/null +++ b/src/modules/path/buildCorrectCasePathMap.ts @@ -0,0 +1,127 @@ +/* eslint-disable no-continue */ +import chalk from 'chalk'; +import { replaceSepToPosix } from 'my-node-fp'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +/** TypeScript source file extensions subject to case-conflict detection. */ +const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']); + +export interface ICaseConflict { + dir: string; + files: string[]; +} + +/** + * Reads every parent directory that appears in inputPaths exactly once, builds a + * map of original path → correctly-cased filesystem path, and exits the process + * with an error on case-insensitive platforms (macOS / Windows) when two TypeScript + * source files in the same directory differ only by letter case (e.g. Aa.ts / aA.ts). + * + * @param inputPaths - Absolute file paths from the TypeScript compiler (e.g. tsconfig + * fileNames or ts-morph SourceFile.getFilePath()). These may carry incorrect casing + * on case-insensitive file systems. + * @returns A Map whose keys are the original input paths and whose values are the + * correctly-cased absolute posix paths as reported by the real filesystem. + */ +export async function buildCorrectCasePathMap(inputPaths: string[]): Promise> { + // Group input paths by their parent directory so each directory is read only once. + const dirToInputs = new Map(); + for (const inputPath of inputPaths) { + const dir = path.dirname(inputPath); + const list = dirToInputs.get(dir); + if (list == null) { + dirToInputs.set(dir, [inputPath]); + } else { + list.push(inputPath); + } + } + + const platform = os.platform(); + const isCaseInsensitive = platform === 'darwin' || platform === 'win32'; + const conflicts: ICaseConflict[] = []; + const resultMap = new Map(); + + // Read all directories in parallel to avoid no-await-in-loop. + const dirEntries = await Promise.all( + Array.from(dirToInputs.keys()).map(async (dir) => { + try { + const entries = await fs.promises.readdir(dir); + return { dir, entries }; + } catch { + return { dir, entries: null }; + } + }), + ); + + for (const { dir, entries } of dirEntries) { + const pathsInDir = dirToInputs.get(dir) ?? []; + + if (entries == null) { + // Directory is not readable; keep the original paths unchanged. + for (const p of pathsInDir) { + resultMap.set(p, replaceSepToPosix(p)); + } + continue; + } + + // On case-insensitive platforms, detect TypeScript source files whose names + // are identical when compared case-insensitively. + if (isCaseInsensitive) { + const lowerToActual = new Map(); + for (const entry of entries) { + if (!TS_EXTENSIONS.has(path.extname(entry))) { + continue; + } + const lower = entry.toLowerCase(); + const existing = lowerToActual.get(lower); + if (existing == null) { + lowerToActual.set(lower, [entry]); + } else { + existing.push(entry); + } + } + + for (const conflicting of lowerToActual.values()) { + if (conflicting.length > 1) { + conflicts.push({ dir, files: conflicting }); + } + } + } + + // Map each input path to its correctly-cased filesystem counterpart. + for (const inputPath of pathsInDir) { + const basename = path.basename(inputPath); + const correctEntry = entries.find((entry) => entry.toLowerCase() === basename.toLowerCase()); + + const correctedPath = correctEntry != null ? path.join(dir, correctEntry) : inputPath; + + resultMap.set(inputPath, replaceSepToPosix(correctedPath)); + } + } + + if (isCaseInsensitive && conflicts.length > 0) { + const lines = conflicts.map( + ({ dir: conflictDir, files }) => + ` ${chalk.cyan(conflictDir)}: ${files.map((f) => chalk.yellow(f)).join(', ')}`, + ); + + const platformLabel = platform === 'darwin' ? 'macOS' : 'Windows'; + // eslint-disable-next-line no-console + console.error( + [ + chalk.red( + 'ERROR: Case-conflicting TypeScript files detected on a case-insensitive filesystem.', + ), + 'Files in the same directory differ only by letter case:', + ...lines, + `Rename the conflicting files to avoid ambiguity on ${platformLabel}.`, + ].join('\n'), + ); + + process.exit(1); + } + + return resultMap; +} From 135c7e53d161937ab6a815b22d6ac240a7fde08a Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sat, 2 May 2026 00:01:35 +0900 Subject: [PATCH 11/15] docs(comments): add English translations alongside Korean comments - Convert all Korean-only comments to bilingual format (English first, Korean below) - Applies to both inline (//) and JSDoc (/** */) comments across 13 source files - Improves accessibility for international contributors - Also adds variable naming guideline to PROJECT.md Co-Authored-By: Claude Sonnet 4.6 --- .claude/PROJECT.md | 7 +++++ src/comments/interfaces/IInlineCommentInfo.ts | 9 ++++-- .../interfaces/IInlineGenerationStyleInfo.ts | 18 +++++++---- src/comments/interfaces/IStatementComments.ts | 9 ++++-- src/compilers/getExportStatement.ts | 4 +++ src/compilers/interfaces/IExportStatement.ts | 6 ++-- src/configs/getExtendOptions.ts | 2 ++ src/configs/transforms/createBuildOptions.ts | 2 +- src/modules/commands/initializing.ts | 1 + src/modules/commands/moduling.ts | 1 + src/modules/file/getCreateModeFileTree.ts | 3 ++ src/modules/path/getCorrectCasedPath.ts | 3 ++ src/templates/interfaces/IIndexRenderData.ts | 30 ++++++++++++------- src/templates/modules/getAutoRenderCase.ts | 4 +++ 14 files changed, 74 insertions(+), 25 deletions(-) diff --git a/.claude/PROJECT.md b/.claude/PROJECT.md index b5c35f1..7a32827 100644 --- a/.claude/PROJECT.md +++ b/.claude/PROJECT.md @@ -132,6 +132,13 @@ Test projects for various scenarios. Contains examples with different structures - Export management for React, Vue.js component libraries - Preparation for webpack, rollup.js bundling +## Coding Guide line + +- Don't use one alphabet variable name + - Not use: `const filePaths = rawFilePaths.map((p) => caseMap.get(p) ?? p);` + - use singular name + - `const filePaths = rawFilePaths.map((rawFilePath) => caseMap.get(rawFilePaths) ?? rawFilePaths);` + ### Commit Log - Use Conventional Commit format diff --git a/src/comments/interfaces/IInlineCommentInfo.ts b/src/comments/interfaces/IInlineCommentInfo.ts index ba0169d..17dd8cb 100644 --- a/src/comments/interfaces/IInlineCommentInfo.ts +++ b/src/comments/interfaces/IInlineCommentInfo.ts @@ -9,11 +9,14 @@ export interface IInlineCommentInfo { tag: string; pos: { - /** inline exclude 키워드가 몇 번째 line에 있는가 */ + /** Which line number the inline exclude keyword is on + * inline exclude 키워드가 몇 번째 line에 있는가 */ line: number; - /** inline exclude 키워드가 몇 번째 col에 있는가 */ + /** Which column number the inline exclude keyword is on + * inline exclude 키워드가 몇 번째 col에 있는가 */ column: number; - /** exclude 키워드가 포함된 statement의 위치 */ + /** Position of the statement containing the exclude keyword + * exclude 키워드가 포함된 statement의 위치 */ start: number; }; diff --git a/src/comments/interfaces/IInlineGenerationStyleInfo.ts b/src/comments/interfaces/IInlineGenerationStyleInfo.ts index 50bacba..e8db904 100644 --- a/src/comments/interfaces/IInlineGenerationStyleInfo.ts +++ b/src/comments/interfaces/IInlineGenerationStyleInfo.ts @@ -1,21 +1,27 @@ import type { CE_GENERATION_STYLE } from '#/configs/const-enum/CE_GENERATION_STYLE'; export interface IInlineGenerationStyleInfo { - /** 주석 내용 */ + /** Comment content + * 주석 내용 */ commentCode: string; - /** 소스 파일 경로 */ + /** Source file path + * 소스 파일 경로 */ filePath: string; - /** export 생성 스타일 */ + /** Export generation style + * export 생성 스타일 */ style: CE_GENERATION_STYLE; pos: { - /** inline exclude 키워드가 몇 번째 line에 있는가 */ + /** Which line number the inline exclude keyword is on + * inline exclude 키워드가 몇 번째 line에 있는가 */ line: number; - /** inline exclude 키워드가 몇 번째 col에 있는가 */ + /** Which column number the inline exclude keyword is on + * inline exclude 키워드가 몇 번째 col에 있는가 */ column: number; - /** exclude 키워드가 포함된 statement의 위치 */ + /** Position of the statement containing the exclude keyword + * exclude 키워드가 포함된 statement의 위치 */ start: number; }; diff --git a/src/comments/interfaces/IStatementComments.ts b/src/comments/interfaces/IStatementComments.ts index 41b93e1..cd69937 100644 --- a/src/comments/interfaces/IStatementComments.ts +++ b/src/comments/interfaces/IStatementComments.ts @@ -6,11 +6,14 @@ export interface IStatementComments { // position: location of the statement commented on pos: { - /** inline exclude 키워드가 몇 번째 line에 있는가 */ + /** Which line number the inline exclude keyword is on + * inline exclude 키워드가 몇 번째 line에 있는가 */ line: number; - /** inline exclude 키워드가 몇 번째 col에 있는가 */ + /** Which column number the inline exclude keyword is on + * inline exclude 키워드가 몇 번째 col에 있는가 */ column: number; - /** exclude 키워드가 포함된 statement의 위치 */ + /** Position of the statement containing the exclude keyword + * exclude 키워드가 포함된 statement의 위치 */ start: number; }; diff --git a/src/compilers/getExportStatement.ts b/src/compilers/getExportStatement.ts index d48a50b..c39a1f0 100644 --- a/src/compilers/getExportStatement.ts +++ b/src/compilers/getExportStatement.ts @@ -21,6 +21,7 @@ export async function getExportStatement( replaceSepToPosix(correctedFilePath.replace(dirPath, '')), path.posix.sep, ); + // One of rootDir, output, or project must be selected and used // rootDir 또는 output, project 셋 중에 하나를 선택해서 써야 한다 const relativePath = posixRelative(await getDirname(option.project), dirPath); // sourceFile.getExportDeclarations().at(0)?.getNamedExports @@ -48,6 +49,9 @@ export async function getExportStatement( const [exportedDeclaration] = exportedDeclarations; const kind = getExportedKind(exportedDeclaration); + // Not sure why this pattern is used for module declarations + // There should be no pattern found when doing something like declare module "react" {}... + // This is because of example08... // 모듈일 때 왜 패턴을 하는지 잘 모르겠다 // declare module "react" {} 같은 것을 할 때 패턴이 발견될 리가 없는데... // example08번 때문이네... diff --git a/src/compilers/interfaces/IExportStatement.ts b/src/compilers/interfaces/IExportStatement.ts index 9b2d910..ad17eba 100644 --- a/src/compilers/interfaces/IExportStatement.ts +++ b/src/compilers/interfaces/IExportStatement.ts @@ -37,10 +37,12 @@ export interface IExportStatement { * export 할 때 사용된 이름 */ identifier: { - /** export 할 때 사용된 이름, default는 default가 입력된다 */ + /** Name used when exporting; "default" is used for default exports + * export 할 때 사용된 이름, default는 default가 입력된다 */ name: string; - /** export를 alias할 때 사용할 이름, default에서 사용되며 파일이름이 사용된다 */ + /** Name used when aliasing the export; used for default exports where the filename is used + * export를 alias할 때 사용할 이름, default에서 사용되며 파일이름이 사용된다 */ alias: string; }; diff --git a/src/configs/getExtendOptions.ts b/src/configs/getExtendOptions.ts index 37e3ab0..56c772d 100644 --- a/src/configs/getExtendOptions.ts +++ b/src/configs/getExtendOptions.ts @@ -11,6 +11,8 @@ export async function getExtendOptions(project: string): Promise const tsconfig = getTypeScriptConfig(projectPath); const resolvedProjectDirPath = replaceSepToPosix(await getDirname(projectPath)); + // Based on various tests, a project with no include specified also includes all ts files in directories + // above the tsconfig.json file. It is necessary to filter out files that are above the tsconfig.json file. // 여러가지 테스트를 해본 결과, include에 아무것도 지정하지 않은 프로젝트는 tsconfig.json 파일보다 // 더 상위 디렉터리에 있는 ts 파일도 모두 포함한다. 이런 식으로 필터를 걸어서, tsconfig.json 파일보다 // 상위에 있는 것을 제외하는 작업이 필요하다 diff --git a/src/configs/transforms/createBuildOptions.ts b/src/configs/transforms/createBuildOptions.ts index 1ab1686..52fd159 100644 --- a/src/configs/transforms/createBuildOptions.ts +++ b/src/configs/transforms/createBuildOptions.ts @@ -44,8 +44,8 @@ export async function createBuildOptions( ProgressBar.it.stream = argv.progressStream; Reasoner.it.stream = argv.reasonerStream; - // config 파일을 읽은 다음, options 필드가 존재하는 경우 argv.include, argv.exclude는 무시된다 // After reading the config file, argv.include, argv.exclude are excluded if the options field is present + // config 파일을 읽은 다음, options 필드가 존재하는 경우 argv.include, argv.exclude는 무시된다 if (argv.options != null) { options.options = argv.options; diff --git a/src/modules/commands/initializing.ts b/src/modules/commands/initializing.ts index bcb665f..b4d1e55 100644 --- a/src/modules/commands/initializing.ts +++ b/src/modules/commands/initializing.ts @@ -97,6 +97,7 @@ export async function initializing(option: TCommandInitOptions) { const parsedRenderedOptions = parse(renderedOptions); if (answer.configPosition === 'tsconfig.json') { + // Since the tsconfig.json file is important, let's ask if they want to create a backup // tsconfig.json 파일은 중요하니까, 백업 만들 생각이 있냐고 물어보자 await Promise.all( answer.tsconfig.map(async (tsconfigFilePath) => { diff --git a/src/modules/commands/moduling.ts b/src/modules/commands/moduling.ts index 54bf634..1215594 100644 --- a/src/modules/commands/moduling.ts +++ b/src/modules/commands/moduling.ts @@ -141,6 +141,7 @@ export async function moduling(_buildOptions: TCommandBuildOptions, moduleOption await indexWrites(indexFiles, moduleOption, extendOptions); + // After writing the index file, it needs to be registered in the project // index 파일을 쓰고 나면 이걸 project에 등록해줘야 한다 ProjectContainer.addSourceFilesAtPaths(moduleOption.project, Array.from(outputMap.keys())); diff --git a/src/modules/file/getCreateModeFileTree.ts b/src/modules/file/getCreateModeFileTree.ts index 056ecb2..20d0e02 100644 --- a/src/modules/file/getCreateModeFileTree.ts +++ b/src/modules/file/getCreateModeFileTree.ts @@ -11,6 +11,7 @@ export async function getCreateModeFileTree(startFrom: string) { const fileOrDirs = await fs.promises.readdir(resolved, { withFileTypes: true, recursive: true }); const dirs = fileOrDirs.filter((fileOrDir) => fileOrDir.isDirectory()); + // Create the root node // root 노드를 만든다 const root: IModuleRoot = { kind: 'root', @@ -44,9 +45,11 @@ export async function getCreateModeFileTree(startFrom: string) { const parent = map.get(child.parent); if (parent != null) { + // When the node has a parent that is not the root // root 아닌 부모 노드를 가지고 있는 경우 parent.children.push(child); } else if (parent == null && child.parent === root.path) { + // When the node is a direct child of the root node // root 노드 자식인 경우 root.children.push(child); } diff --git a/src/modules/path/getCorrectCasedPath.ts b/src/modules/path/getCorrectCasedPath.ts index c8a7cfe..21fedf0 100644 --- a/src/modules/path/getCorrectCasedPath.ts +++ b/src/modules/path/getCorrectCasedPath.ts @@ -14,6 +14,9 @@ import path from 'node:path'; * @returns The file path with correct casing matching the actual filesystem */ export async function getCorrectCasedPath(inputPath: string): Promise { + // In the type14 scenario, sourceFile.getFilePath incorrectly returns SomeApiService.ts or SomeAPIService.ts. + // This is a problem that occurs on case-insensitive OSes like Windows and macOS. + // A way to correctly fix this needs to be investigated. // type14 상황에서 sourceFile.getFilePath 함수가 SomeApiService.ts 또는 SomeAPIService.ts를 // 잘못 반환하는 이슈가 있다. window, macOS와 같이 case-sensitive가 아닌 OS에서 발생하는 문제이다. // 이 부분을 올바르게 수정할 수 있는 방법을 연구해야 한다. diff --git a/src/templates/interfaces/IIndexRenderData.ts b/src/templates/interfaces/IIndexRenderData.ts index e8d5d2e..f62cfa3 100644 --- a/src/templates/interfaces/IIndexRenderData.ts +++ b/src/templates/interfaces/IIndexRenderData.ts @@ -2,14 +2,17 @@ import type { IExportStatement } from '#/compilers/interfaces/IExportStatement'; export interface IIndexRenderData { options: { - /** 따옴표 종류 */ + /** Type of quote character + * 따옴표 종류 */ quote: string; - /** 세미콜론 추가 여부 */ + /** Whether to add semicolon + * 세미콜론 추가 여부 */ useSemicolon: boolean; }; - /** 파일 경로 */ + /** File path + * 파일 경로 */ filePath: string; statement: { @@ -19,25 +22,32 @@ export interface IIndexRenderData { * */ importPath: string; - /** 파일 확장자 */ + /** File extension + * 파일 확장자 */ extname: { - /** 원본 확장자 */ + /** Original extension + * 원본 확장자 */ origin: string; - /** 렌더링용 확장자 */ + /** Extension for rendering + * 렌더링용 확장자 */ render: string; }; - /** default export를 가지고 있는가 여부 */ + /** Whether the file has a default export + * default export를 가지고 있는가 여부 */ isHasDefault: boolean; - /** named export에서 일부 export statement가 exclude에 포함되어 있는가 */ + /** Whether some export statements in named exports are included in exclude + * named export에서 일부 export statement가 exclude에 포함되어 있는가 */ isHasPartialExclude: boolean; - /** default export statemt 정보 */ + /** Default export statement information + * default export statemt 정보 */ default?: IExportStatement; - /** named export statements 정보 */ + /** Named export statements information + * named export statements 정보 */ named: IExportStatement[]; }; } diff --git a/src/templates/modules/getAutoRenderCase.ts b/src/templates/modules/getAutoRenderCase.ts index 2e24189..267ae04 100644 --- a/src/templates/modules/getAutoRenderCase.ts +++ b/src/templates/modules/getAutoRenderCase.ts @@ -81,6 +81,8 @@ export function getAutoRenderCase(renderData: IIndexRenderData): { renderData.statement.named.length <= 0 && renderData.statement.isHasPartialExclude ) { + // A warning is needed when this pattern is used; in rollup-plugin-dts, this causes an error + // because the default export is emitted twice. // 이 방식으로 되어 있을 때는 경고가 필요하다, rollup-plugin-dts에서는 이 방식인 경우, // default export를 2번 내보내서 오류가 발생한다. return { @@ -101,6 +103,8 @@ export function getAutoRenderCase(renderData: IIndexRenderData): { renderData.statement.named.length > 0 && renderData.statement.isHasPartialExclude ) { + // A warning is needed when this pattern is used; in rollup-plugin-dts, this causes an error + // because the default export is emitted twice. // 이 방식으로 되어 있을 때는 경고가 필요하다, rollup-plugin-dts에서는 이 방식인 경우, // default export를 2번 내보내서 오류가 발생한다. return { From c42481e0ce334dcaaf5c48679666215d9908fe3a Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sat, 2 May 2026 00:08:17 +0900 Subject: [PATCH 12/15] refactor(naming): replace single-letter lambda params with descriptive names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bundling.ts: p → rawFilePath, sf → sourceFile - creating.ts: p → rawFilePath, sf → sourceFile - buildCorrectCasePathMap.ts: f → file - Fix typo in PROJECT.md coding guideline example (rawFilePaths → rawFilePath) Co-Authored-By: Claude Sonnet 4.6 --- .claude/PROJECT.md | 2 +- src/modules/commands/bundling.ts | 8 ++++---- src/modules/commands/creating.ts | 8 ++++---- src/modules/path/buildCorrectCasePathMap.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.claude/PROJECT.md b/.claude/PROJECT.md index 7a32827..2a8b401 100644 --- a/.claude/PROJECT.md +++ b/.claude/PROJECT.md @@ -137,7 +137,7 @@ Test projects for various scenarios. Contains examples with different structures - Don't use one alphabet variable name - Not use: `const filePaths = rawFilePaths.map((p) => caseMap.get(p) ?? p);` - use singular name - - `const filePaths = rawFilePaths.map((rawFilePath) => caseMap.get(rawFilePaths) ?? rawFilePaths);` + - `const filePaths = rawFilePaths.map((rawFilePath) => caseMap.get(rawFilePath) ?? rawFilePath);` ### Commit Log diff --git a/src/modules/commands/bundling.ts b/src/modules/commands/bundling.ts index a7af285..bc10784 100644 --- a/src/modules/commands/bundling.ts +++ b/src/modules/commands/bundling.ts @@ -67,16 +67,16 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: .getSourceFiles() .map((sourceFile) => sourceFile.getFilePath().toString()); const caseMap = await buildCorrectCasePathMap(rawFilePaths); - const filePaths = rawFilePaths.map((p) => caseMap.get(p) ?? p); + const filePaths = rawFilePaths.map((rawFilePath) => caseMap.get(rawFilePath) ?? rawFilePath); // Build correctedPath → SourceFile map so lookups remain correct after case // correction. ts-morph registers files by their original (possibly wrong-cased) // path, so project.getSourceFile(correctedPath) can return null on // case-sensitive systems or strict ts-morph builds. const sourceFileMap = new Map( - project.getSourceFiles().map((sf) => { - const original = sf.getFilePath().toString(); - return [caseMap.get(original) ?? original, sf]; + project.getSourceFiles().map((sourceFile) => { + const original = sourceFile.getFilePath().toString(); + return [caseMap.get(original) ?? original, sourceFile]; }), ); diff --git a/src/modules/commands/creating.ts b/src/modules/commands/creating.ts index a3274ad..07c8c27 100644 --- a/src/modules/commands/creating.ts +++ b/src/modules/commands/creating.ts @@ -59,16 +59,16 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption .getSourceFiles() .map((sourceFile) => sourceFile.getFilePath().toString()); const caseMap = await buildCorrectCasePathMap(rawFilePaths); - const filePaths = rawFilePaths.map((p) => caseMap.get(p) ?? p); + const filePaths = rawFilePaths.map((rawFilePath) => caseMap.get(rawFilePath) ?? rawFilePath); // Build correctedPath → SourceFile map so lookups remain correct after case // correction. ts-morph registers files by their original (possibly wrong-cased) // path, so project.getSourceFile(correctedPath) can return null on // case-sensitive systems or strict ts-morph builds. const sourceFileMap = new Map( - project.getSourceFiles().map((sf) => { - const original = sf.getFilePath().toString(); - return [caseMap.get(original) ?? original, sf]; + project.getSourceFiles().map((sourceFile) => { + const original = sourceFile.getFilePath().toString(); + return [caseMap.get(original) ?? original, sourceFile]; }), ); diff --git a/src/modules/path/buildCorrectCasePathMap.ts b/src/modules/path/buildCorrectCasePathMap.ts index 3fc741c..952ec06 100644 --- a/src/modules/path/buildCorrectCasePathMap.ts +++ b/src/modules/path/buildCorrectCasePathMap.ts @@ -104,7 +104,7 @@ export async function buildCorrectCasePathMap(inputPaths: string[]): Promise 0) { const lines = conflicts.map( ({ dir: conflictDir, files }) => - ` ${chalk.cyan(conflictDir)}: ${files.map((f) => chalk.yellow(f)).join(', ')}`, + ` ${chalk.cyan(conflictDir)}: ${files.map((file) => chalk.yellow(file)).join(', ')}`, ); const platformLabel = platform === 'darwin' ? 'macOS' : 'Windows'; From b9da832f772a2f300d3422a7cc7ff442e1edf34a Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sat, 2 May 2026 00:10:15 +0900 Subject: [PATCH 13/15] chore(version): bump version to 2.8.0 Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7852196..f090372 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctix", - "version": "2.7.6-beta1", + "version": "2.8.0", "description": "Automatic create index.ts file", "scripts": { "clean": "rimraf ./dist", From 4fa2b8c60e5effef5ab31acfe51281bc8eaf5760 Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sat, 2 May 2026 00:28:05 +0900 Subject: [PATCH 14/15] fix(typescript): resolve TypeScript 5.9 compatibility issues - Restrict peerDependencies typescript to >=5 <6 to prevent TypeScript 6.x from being installed (>=5 was resolving to 6.x which deprecated baseUrl and moduleResolution:node10 as hard errors) - Fix Buffer type incompatibility in indexWrites.ts introduced in TypeScript 5.9: readFile now returns Buffer which is no longer assignable to writeFile's parameter type; resolve by passing 'utf-8' encoding to return string instead Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- pnpm-lock.yaml | 170 +++++++++++++++--------------- src/modules/writes/indexWrites.ts | 2 +- 3 files changed, 87 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index f090372..0f28bc7 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "peerDependencies": { "prettier": ">=3", "prettier-plugin-organize-imports": ">=3", - "typescript": ">=5" + "typescript": ">=5 <6" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99d3aab..387b5e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: 5.0.1 version: 5.0.1 typescript: - specifier: '>=5' - version: 5.5.2 + specifier: '>=5 <6' + version: 5.9.3 yaml: specifier: 2.8.1 version: 2.8.1 @@ -107,7 +107,7 @@ importers: devDependencies: '@commitlint/cli': specifier: 20.1.0 - version: 20.1.0(@types/node@20.9.0)(typescript@5.5.2) + version: 20.1.0(@types/node@20.9.0)(typescript@5.9.3) '@commitlint/config-conventional': specifier: 20.0.0 version: 20.0.0 @@ -146,10 +146,10 @@ importers: version: 17.0.32 '@typescript-eslint/eslint-plugin': specifier: ^7.6.0 - version: 7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2) + version: 7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint@8.57.0)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^7.6.0 - version: 7.6.0(eslint@8.57.0)(typescript@5.5.2) + version: 7.6.0(eslint@8.57.0)(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^3.1.1 version: 3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.9.0)) @@ -167,16 +167,16 @@ importers: version: 8.57.0 eslint-config-airbnb-typescript: specifier: ^18.0.0 - version: 18.0.0(@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2))(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + version: 18.0.0(@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint@8.57.0)(typescript@5.9.3))(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) eslint-import-resolver-typescript: specifier: ^3.6.1 - version: 3.6.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + version: 3.6.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + version: 2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsdoc: specifier: ^48.2.3 version: 48.2.3(eslint@8.57.0) @@ -206,7 +206,7 @@ importers: version: 16.3.0 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.0.0(prettier@3.2.5)(typescript@5.5.2) + version: 4.0.0(prettier@3.2.5)(typescript@5.9.3) read-pkg: specifier: ^5.2.0 version: 5.2.0 @@ -218,10 +218,10 @@ importers: version: 4.14.2 rollup-plugin-dts: specifier: ^6.1.0 - version: 6.1.0(rollup@4.14.2)(typescript@5.5.2) + version: 6.1.0(rollup@4.14.2)(typescript@5.9.3) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.9.0)(typescript@5.5.2) + version: 10.9.2(@types/node@20.9.0)(typescript@5.9.3) tsc-alias: specifier: ^1.8.8 version: 1.8.8 @@ -230,13 +230,13 @@ importers: version: 4.2.0 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.5.2)(vite@5.1.4(@types/node@20.9.0)) + version: 5.1.4(typescript@5.9.3)(vite@5.1.4(@types/node@20.9.0)) vitest: specifier: ^3.1.1 version: 3.1.1(@types/debug@4.1.12)(@types/node@20.9.0) vue: specifier: ^3.4.21 - version: 3.4.21(typescript@5.5.2) + version: 3.4.21(typescript@5.9.3) packages: @@ -3154,8 +3154,8 @@ packages: resolution: {integrity: sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==} engines: {node: '>= 14'} - typescript@5.5.2: - resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -3398,11 +3398,11 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@commitlint/cli@20.1.0(@types/node@20.9.0)(typescript@5.5.2)': + '@commitlint/cli@20.1.0(@types/node@20.9.0)(typescript@5.9.3)': dependencies: '@commitlint/format': 20.0.0 '@commitlint/lint': 20.0.0 - '@commitlint/load': 20.1.0(@types/node@20.9.0)(typescript@5.5.2) + '@commitlint/load': 20.1.0(@types/node@20.9.0)(typescript@5.9.3) '@commitlint/read': 20.0.0 '@commitlint/types': 20.0.0 tinyexec: 1.0.1 @@ -3449,15 +3449,15 @@ snapshots: '@commitlint/rules': 20.0.0 '@commitlint/types': 20.0.0 - '@commitlint/load@20.1.0(@types/node@20.9.0)(typescript@5.5.2)': + '@commitlint/load@20.1.0(@types/node@20.9.0)(typescript@5.9.3)': dependencies: '@commitlint/config-validator': 20.0.0 '@commitlint/execute-rule': 20.0.0 '@commitlint/resolve-extends': 20.1.0 '@commitlint/types': 20.0.0 chalk: 5.3.0 - cosmiconfig: 9.0.0(typescript@5.5.2) - cosmiconfig-typescript-loader: 6.1.0(@types/node@20.9.0)(cosmiconfig@9.0.0(typescript@5.5.2))(typescript@5.5.2) + cosmiconfig: 9.0.0(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@20.9.0)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -3903,13 +3903,13 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2)': + '@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint@8.57.0)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.9.3) '@typescript-eslint/scope-manager': 7.6.0 - '@typescript-eslint/type-utils': 7.6.0(eslint@8.57.0)(typescript@5.5.2) - '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/type-utils': 7.6.0(eslint@8.57.0)(typescript@5.9.3) + '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 7.6.0 debug: 4.3.4 eslint: 8.57.0 @@ -3917,35 +3917,35 @@ snapshots: ignore: 5.3.1 natural-compare: 1.4.0 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.5.2) + ts-api-utils: 1.3.0(typescript@5.9.3) optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.11.0(eslint@8.57.0)(typescript@5.5.2)': + '@typescript-eslint/parser@6.11.0(eslint@8.57.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 6.11.0 '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.2) + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 eslint: 8.57.0 optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2)': + '@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 7.6.0 '@typescript-eslint/types': 7.6.0 - '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.5.2) + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 7.6.0 debug: 4.3.4 eslint: 8.57.0 optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3959,15 +3959,15 @@ snapshots: '@typescript-eslint/types': 7.6.0 '@typescript-eslint/visitor-keys': 7.6.0 - '@typescript-eslint/type-utils@7.6.0(eslint@8.57.0)(typescript@5.5.2)': + '@typescript-eslint/type-utils@7.6.0(eslint@8.57.0)(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.5.2) - '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.9.3) + '@typescript-eslint/utils': 7.6.0(eslint@8.57.0)(typescript@5.9.3) debug: 4.3.4 eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.5.2) + ts-api-utils: 1.3.0(typescript@5.9.3) optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3975,7 +3975,7 @@ snapshots: '@typescript-eslint/types@7.6.0': {} - '@typescript-eslint/typescript-estree@6.11.0(typescript@5.5.2)': + '@typescript-eslint/typescript-estree@6.11.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 6.11.0 '@typescript-eslint/visitor-keys': 6.11.0 @@ -3983,13 +3983,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.0 - ts-api-utils: 1.0.3(typescript@5.5.2) + ts-api-utils: 1.0.3(typescript@5.9.3) optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@7.6.0(typescript@5.5.2)': + '@typescript-eslint/typescript-estree@7.6.0(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 7.6.0 '@typescript-eslint/visitor-keys': 7.6.0 @@ -3998,20 +3998,20 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.4 semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.5.2) + ts-api-utils: 1.3.0(typescript@5.9.3) optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.6.0(eslint@8.57.0)(typescript@5.5.2)': + '@typescript-eslint/utils@7.6.0(eslint@8.57.0)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 7.6.0 '@typescript-eslint/types': 7.6.0 - '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.5.2) + '@typescript-eslint/typescript-estree': 7.6.0(typescript@5.9.3) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -4133,11 +4133,11 @@ snapshots: '@vue/shared': 3.4.21 csstype: 3.1.3 - '@vue/server-renderer@3.4.21(vue@3.4.21(typescript@5.5.2))': + '@vue/server-renderer@3.4.21(vue@3.4.21(typescript@5.9.3))': dependencies: '@vue/compiler-ssr': 3.4.21 '@vue/shared': 3.4.21 - vue: 3.4.21(typescript@5.5.2) + vue: 3.4.21(typescript@5.9.3) '@vue/shared@3.4.21': {} @@ -4478,21 +4478,21 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.1.0(@types/node@20.9.0)(cosmiconfig@9.0.0(typescript@5.5.2))(typescript@5.5.2): + cosmiconfig-typescript-loader@6.1.0(@types/node@20.9.0)(cosmiconfig@9.0.0(typescript@5.9.3))(typescript@5.9.3): dependencies: '@types/node': 20.9.0 - cosmiconfig: 9.0.0(typescript@5.5.2) + cosmiconfig: 9.0.0(typescript@5.9.3) jiti: 2.6.1 - typescript: 5.5.2 + typescript: 5.9.3 - cosmiconfig@9.0.0(typescript@5.5.2): + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 create-require@1.1.1: {} @@ -4754,15 +4754,15 @@ snapshots: dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) object.assign: 4.1.4 object.entries: 1.1.7 semver: 6.3.1 - eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2))(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint@8.57.0)(typescript@5.9.3))(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: - '@typescript-eslint/eslint-plugin': 7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint@8.57.0)(typescript@5.5.2) - '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/eslint-plugin': 7.6.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint@8.57.0)(typescript@5.9.3) + '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.9.3) eslint: 8.57.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: @@ -4780,13 +4780,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 is-core-module: 2.13.1 @@ -4797,18 +4797,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.9.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -4818,7 +4818,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.6.0(eslint@8.57.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -4829,7 +4829,7 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/parser': 7.6.0(eslint@8.57.0)(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5823,7 +5823,7 @@ snapshots: prettier-eslint@16.3.0: dependencies: - '@typescript-eslint/parser': 6.11.0(eslint@8.57.0)(typescript@5.5.2) + '@typescript-eslint/parser': 6.11.0(eslint@8.57.0)(typescript@5.9.3) common-tags: 1.8.2 dlv: 1.1.3 eslint: 8.57.0 @@ -5833,7 +5833,7 @@ snapshots: prettier: 3.2.5 pretty-format: 29.7.0 require-relative: 0.8.7 - typescript: 5.5.2 + typescript: 5.9.3 vue-eslint-parser: 9.3.2(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -5842,10 +5842,10 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.0.0(prettier@3.2.5)(typescript@5.5.2): + prettier-plugin-organize-imports@4.0.0(prettier@3.2.5)(typescript@5.9.3): dependencies: prettier: 3.2.5 - typescript: 5.5.2 + typescript: 5.9.3 prettier@3.2.5: {} @@ -5936,11 +5936,11 @@ snapshots: dependencies: glob: 10.3.12 - rollup-plugin-dts@6.1.0(rollup@4.14.2)(typescript@5.5.2): + rollup-plugin-dts@6.1.0(rollup@4.14.2)(typescript@5.9.3): dependencies: magic-string: 0.30.5 rollup: 4.14.2 - typescript: 5.5.2 + typescript: 5.9.3 optionalDependencies: '@babel/code-frame': 7.22.13 @@ -6246,20 +6246,20 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - ts-api-utils@1.0.3(typescript@5.5.2): + ts-api-utils@1.0.3(typescript@5.9.3): dependencies: - typescript: 5.5.2 + typescript: 5.9.3 - ts-api-utils@1.3.0(typescript@5.5.2): + ts-api-utils@1.3.0(typescript@5.9.3): dependencies: - typescript: 5.5.2 + typescript: 5.9.3 ts-morph@27.0.0: dependencies: '@ts-morph/common': 0.28.0 code-block-writer: 13.0.3 - ts-node@10.9.2(@types/node@20.9.0)(typescript@5.5.2): + ts-node@10.9.2(@types/node@20.9.0)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -6273,7 +6273,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.2 + typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -6288,9 +6288,9 @@ snapshots: normalize-path: 3.0.0 plimit-lit: 1.6.1 - tsconfck@3.0.3(typescript@5.5.2): + tsconfck@3.0.3(typescript@5.9.3): optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 tsconfig-paths@3.15.0: dependencies: @@ -6352,7 +6352,7 @@ snapshots: typed-function@4.1.1: {} - typescript@5.5.2: {} + typescript@5.9.3: {} unbox-primitive@1.0.2: dependencies: @@ -6403,11 +6403,11 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.1.4(typescript@5.5.2)(vite@5.1.4(@types/node@20.9.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@5.1.4(@types/node@20.9.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 - tsconfck: 3.0.3(typescript@5.5.2) + tsconfck: 3.0.3(typescript@5.9.3) optionalDependencies: vite: 5.1.4(@types/node@20.9.0) transitivePeerDependencies: @@ -6471,15 +6471,15 @@ snapshots: transitivePeerDependencies: - supports-color - vue@3.4.21(typescript@5.5.2): + vue@3.4.21(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.4.21 '@vue/compiler-sfc': 3.4.21 '@vue/runtime-dom': 3.4.21 - '@vue/server-renderer': 3.4.21(vue@3.4.21(typescript@5.5.2)) + '@vue/server-renderer': 3.4.21(vue@3.4.21(typescript@5.9.3)) '@vue/shared': 3.4.21 optionalDependencies: - typescript: 5.5.2 + typescript: 5.9.3 wcwidth@1.0.1: dependencies: diff --git a/src/modules/writes/indexWrites.ts b/src/modules/writes/indexWrites.ts index 215436b..26f090f 100644 --- a/src/modules/writes/indexWrites.ts +++ b/src/modules/writes/indexWrites.ts @@ -23,7 +23,7 @@ export async function indexWrites( if (option.backup) { if (await exists(file.path)) { - await writeFile(`${file.path}.bak`, await readFile(file.path)); + await writeFile(`${file.path}.bak`, await readFile(file.path, 'utf-8')); } await writeFile(file.path, `${prettified.contents.trim()}${extendOptions.eol}`); From 8600ffbe06ede3ccabcbc6b0acf009109b9906ce Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Sat, 2 May 2026 00:51:30 +0900 Subject: [PATCH 15/15] test(coverage): improve test coverage from 97.29% to 100% - Add Debugger.ts tests: table(), logList(), close(), logFile setter, enable getter, bootstrap idempotency, and stream write path - Add getCwd.ts tests: USE_INIT_CWD/INIT_CWD env var branches - Add IncludeContainer.ts test: empty map (map.size <= 0) branch - Add buildCorrectCasePathMap.ts test: win32 platform label branch - Mark genuinely unreachable defensive branches with v8 ignore comments Co-Authored-By: Claude Sonnet 4.6 --- src/cli/ux/Debugger.ts | 3 + src/cli/ux/__tests__/debugger.test.ts | 156 ++++++++++++++++++ .../build.correct.case.path.map.test.ts | 18 ++ src/modules/path/__tests__/get.cwd.test.ts | 35 ++++ src/modules/path/buildCorrectCasePathMap.ts | 2 + .../scope/__tests__/include.container.test.ts | 10 ++ 6 files changed, 224 insertions(+) create mode 100644 src/cli/ux/__tests__/debugger.test.ts create mode 100644 src/modules/path/__tests__/get.cwd.test.ts diff --git a/src/cli/ux/Debugger.ts b/src/cli/ux/Debugger.ts index 5212e8e..e3f468b 100644 --- a/src/cli/ux/Debugger.ts +++ b/src/cli/ux/Debugger.ts @@ -55,9 +55,12 @@ export class Debugger { } #openStream() { + // Both callers guard logFile != null before calling; this is a defensive check + /* v8 ignore start */ if (this.#logFile == null) { return; } + /* v8 ignore stop */ const logDir = path.dirname(this.#logFile); diff --git a/src/cli/ux/__tests__/debugger.test.ts b/src/cli/ux/__tests__/debugger.test.ts new file mode 100644 index 0000000..899ded0 --- /dev/null +++ b/src/cli/ux/__tests__/debugger.test.ts @@ -0,0 +1,156 @@ +import { Debugger } from '#/cli/ux/Debugger'; +import fs from 'node:fs'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +describe('Debugger', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('table - does nothing when disabled', () => { + const debuggerInstance = new Debugger(false, undefined); + const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + debuggerInstance.table('label', [['key', 'value']]); + + expect(writeSpy).not.toHaveBeenCalled(); + }); + + it('table - logs each entry when enabled', () => { + const debuggerInstance = new Debugger(true, undefined); + const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + debuggerInstance.table('label', [ + ['key1', 'value1'], + ['key2', 42], + ]); + + expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('label')); + expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('key1 => value1')); + expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('key2 => 42')); + }); + + it('logList - does nothing when disabled', () => { + const debuggerInstance = new Debugger(false, undefined); + const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + debuggerInstance.logList('label', ['item1']); + + expect(writeSpy).not.toHaveBeenCalled(); + }); + + it('logList - logs each item when enabled', () => { + const debuggerInstance = new Debugger(true, undefined); + const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + debuggerInstance.logList('label', ['item1', 'item2']); + + expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('label (2)')); + expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('- item1')); + expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('- item2')); + }); + + it('logList - logs (empty) when items array is empty', () => { + const debuggerInstance = new Debugger(true, undefined); + const writeSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + debuggerInstance.logList('label', []); + + expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('(empty)')); + }); + + it('log - writes to stream when stream is open', () => { + const mockStream = { end: vi.fn(), write: vi.fn() }; + vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream); + vi.spyOn(fs, 'existsSync').mockReturnValue(true); + vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + const debuggerInstance = new Debugger(false, '/tmp/ctix-test.log'); + debuggerInstance.enable = true; + debuggerInstance.log('hello'); + + expect(mockStream.write).toHaveBeenCalledWith(expect.stringContaining('hello')); + }); + + it('openStream - creates log directory when it does not exist', () => { + const mockStream = { end: vi.fn(), write: vi.fn() }; + vi.spyOn(fs, 'existsSync').mockReturnValue(false); + const mkdirSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined); + vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream); + + const debuggerInstance = new Debugger(false, '/tmp/nonexistent/ctix-test.log'); + debuggerInstance.enable = true; + + expect(mkdirSpy).toHaveBeenCalledWith('/tmp/nonexistent', { recursive: true }); + }); + + it('isBootstrap - returns true after module load auto-bootstrap', () => { + expect(Debugger.isBootstrap).toBe(true); + }); + + it('bootstrap - is idempotent and does not reset the singleton', () => { + const before = Debugger.it; + Debugger.bootstrap(); + expect(Debugger.it).toBe(before); + }); + + it('enable getter - returns current enable state', () => { + const debuggerInstance = new Debugger(false, undefined); + expect(debuggerInstance.enable).toBe(false); + + debuggerInstance.enable = true; + expect(debuggerInstance.enable).toBe(true); + }); + + it('logFile setter - opens stream when enable is true and logFile is set', () => { + const mockStream = { end: vi.fn(), write: vi.fn() }; + vi.spyOn(fs, 'existsSync').mockReturnValue(true); + const createWriteStreamSpy = vi + .spyOn(fs, 'createWriteStream') + .mockReturnValue(mockStream as unknown as fs.WriteStream); + + const debuggerInstance = new Debugger(true, undefined); + debuggerInstance.logFile = '/tmp/ctix-test.log'; + + expect(createWriteStreamSpy).toHaveBeenCalled(); + }); + + it('logFile setter - does not open stream when enable is false', () => { + const createWriteStreamSpy = vi.spyOn(fs, 'createWriteStream'); + + const debuggerInstance = new Debugger(false, undefined); + debuggerInstance.logFile = '/tmp/ctix-test.log'; + + expect(createWriteStreamSpy).not.toHaveBeenCalled(); + }); + + it('close - does nothing when stream is not open', () => { + const debuggerInstance = new Debugger(false, undefined); + expect(() => debuggerInstance.close()).not.toThrow(); + }); + + it('close - ends and clears the write stream', () => { + const mockStream = { end: vi.fn(), write: vi.fn() }; + vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream); + vi.spyOn(fs, 'existsSync').mockReturnValue(true); + + const debuggerInstance = new Debugger(false, '/tmp/ctix-test.log'); + debuggerInstance.enable = true; + debuggerInstance.close(); + + expect(mockStream.end).toHaveBeenCalledOnce(); + }); + + it('close - second call does nothing after stream is cleared', () => { + const mockStream = { end: vi.fn(), write: vi.fn() }; + vi.spyOn(fs, 'createWriteStream').mockReturnValue(mockStream as unknown as fs.WriteStream); + vi.spyOn(fs, 'existsSync').mockReturnValue(true); + + const debuggerInstance = new Debugger(false, '/tmp/ctix-test.log'); + debuggerInstance.enable = true; + debuggerInstance.close(); + debuggerInstance.close(); + + expect(mockStream.end).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/modules/path/__tests__/build.correct.case.path.map.test.ts b/src/modules/path/__tests__/build.correct.case.path.map.test.ts index b61ee1e..378e056 100644 --- a/src/modules/path/__tests__/build.correct.case.path.map.test.ts +++ b/src/modules/path/__tests__/build.correct.case.path.map.test.ts @@ -111,6 +111,24 @@ describe('buildCorrectCasePathMap', () => { exitSpy.mockRestore(); }); + it('should use "Windows" label when platform is win32 and case conflicts exist', async () => { + const platformSpy = vi.spyOn(os, 'platform').mockReturnValue('win32'); + const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + mockReaddir.mockResolvedValueOnce(['Aa.ts', 'aA.ts'] as any); + + const inputs = ['/project/src/Aa.ts', '/project/src/aA.ts']; + await buildCorrectCasePathMap(inputs); + + expect(exitSpy).toHaveBeenCalledWith(1); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Windows')); + + platformSpy.mockRestore(); + exitSpy.mockRestore(); + consoleSpy.mockRestore(); + }); + it('should not exit on case-sensitive platforms even with same-lower-case filenames', async () => { const platformSpy = vi.spyOn(os, 'platform').mockReturnValue('linux'); const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); diff --git a/src/modules/path/__tests__/get.cwd.test.ts b/src/modules/path/__tests__/get.cwd.test.ts new file mode 100644 index 0000000..d8171d6 --- /dev/null +++ b/src/modules/path/__tests__/get.cwd.test.ts @@ -0,0 +1,35 @@ +import { getCwd } from '#/modules/path/getCwd'; +import { afterEach, describe, expect, it } from 'vitest'; + +describe('getCwd', () => { + afterEach(() => { + delete process.env.USE_INIT_CWD; + delete process.env.INIT_CWD; + }); + + it('returns process.cwd() when USE_INIT_CWD is not set', () => { + delete process.env.USE_INIT_CWD; + expect(getCwd()).toBe(process.cwd()); + }); + + it('returns INIT_CWD when USE_INIT_CWD is "true" and INIT_CWD is set', () => { + process.env.USE_INIT_CWD = 'true'; + process.env.INIT_CWD = '/some/init/cwd'; + + expect(getCwd()).toBe('/some/init/cwd'); + }); + + it('returns process.cwd() when USE_INIT_CWD is "true" but INIT_CWD is not set', () => { + process.env.USE_INIT_CWD = 'true'; + delete process.env.INIT_CWD; + + expect(getCwd()).toBe(process.cwd()); + }); + + it('returns process.cwd() when USE_INIT_CWD is not "true"', () => { + process.env.USE_INIT_CWD = 'false'; + process.env.INIT_CWD = '/some/init/cwd'; + + expect(getCwd()).toBe(process.cwd()); + }); +}); diff --git a/src/modules/path/buildCorrectCasePathMap.ts b/src/modules/path/buildCorrectCasePathMap.ts index 952ec06..a91a648 100644 --- a/src/modules/path/buildCorrectCasePathMap.ts +++ b/src/modules/path/buildCorrectCasePathMap.ts @@ -56,6 +56,8 @@ export async function buildCorrectCasePathMap(inputPaths: string[]): Promise { expect(container.isInclude(windowsRelativePath)).toBe(true); }); + it('isInclude - returns false when map is empty (no files matched)', () => { + const container = new IncludeContainer({ + config: { include: ['__nonexistent_dir_ctix_test__/**/*.ts'] }, + cwd: process.cwd(), + }); + + expect(container.map.size).toBe(0); + expect(container.isInclude('src/modules/scope/IncludeContainer.ts')).toBe(false); + }); + it('files - string path', () => { const expactation = getGlobFiles( new Glob('examples/type03/**/*.ts', {