diff --git a/.claude/PROJECT.md b/.claude/PROJECT.md index b5c35f1..2a8b401 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(rawFilePath) ?? rawFilePath);` + ### Commit Log - Use Conventional Commit format diff --git a/package.json b/package.json index 7852196..0f28bc7 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", @@ -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/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 { - 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/cli/ux/Debugger.ts b/src/cli/ux/Debugger.ts new file mode 100644 index 0000000..e3f468b --- /dev/null +++ b/src/cli/ux/Debugger.ts @@ -0,0 +1,123 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export class Debugger { + static #it: Debugger; + + static #isBootstrap: boolean = false; + + static get it() { + return Debugger.#it; + } + + static get isBootstrap() { + return Debugger.#isBootstrap; + } + + static bootstrap() { + if (Debugger.#isBootstrap) { + return; + } + + Debugger.#it = new Debugger(false, undefined); + Debugger.#isBootstrap = true; + } + + #enable: boolean; + + #logFile: string | undefined; + + #stream: fs.WriteStream | undefined; + + constructor(enable: boolean, logFile: string | undefined) { + this.#enable = enable; + this.#logFile = logFile; + } + + get enable() { + return this.#enable; + } + + set enable(value: boolean) { + this.#enable = value; + + if (this.#logFile != null && value) { + this.#openStream(); + } + } + + set logFile(filePath: string | undefined) { + this.#logFile = filePath; + + if (filePath != null && this.#enable) { + this.#openStream(); + } + } + + #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); + + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + this.#stream = fs.createWriteStream(this.#logFile, { flags: 'a' }); + } + + log(message: string) { + if (!this.#enable) { + return; + } + + const line = `[DEBUG] ${message}`; + process.stderr.write(`${line}\n`); + + if (this.#stream != null) { + this.#stream.write(`${line}\n`); + } + } + + 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; + } + + this.log(`${label}:`); + + for (const [key, value] of entries) { + this.log(` ${key} => ${String(value)}`); + } + } + + close() { + if (this.#stream != null) { + this.#stream.end(); + this.#stream = undefined; + } + } +} + +Debugger.bootstrap(); 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/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/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/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/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/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/configs/transforms/createBuildOptions.ts b/src/configs/transforms/createBuildOptions.ts index d6b1fe4..52fd159 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'; @@ -31,6 +32,7 @@ export async function createBuildOptions( spinnerStream: argv.spinnerStream, progressStream: argv.progressStream, reasonerStream: argv.reasonerStream, + verbose: argv.verbose ?? false, options: [], }; @@ -42,15 +44,15 @@ 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; 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( @@ -84,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( @@ -117,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( @@ -153,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 = @@ -180,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, @@ -193,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, @@ -209,8 +216,8 @@ export async function createBuildOptions( } options.options = [ - transformBundleMode(argv, { - ...argv, + transformBundleMode(resolvedArgv, { + ...resolvedArgv, mode: CE_CTIX_BUILD_MODE.BUNDLE_MODE, output, include, 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/commands/bundling.ts b/src/modules/commands/bundling.ts index 3aeb621..bc10784 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'; @@ -17,8 +18,9 @@ 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'; import { ExcludeContainer } from '#/modules/scope/ExcludeContainer'; import { IncludeContainer } from '#/modules/scope/IncludeContainer'; @@ -57,23 +59,44 @@ 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)); - const filePaths = await Promise.all( - project - .getSourceFiles() - .map((sourceFile) => getCorrectCasedPath(sourceFile.getFilePath().toString())), + // 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 rawFilePaths = project + .getSourceFiles() + .map((sourceFile) => sourceFile.getFilePath().toString()); + const caseMap = await buildCorrectCasePathMap(rawFilePaths); + 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((sourceFile) => { + const original = sourceFile.getFilePath().toString(); + return [caseMap.get(original) ?? original, sourceFile]; + }), ); + 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, }); + // 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 된 파일을 별도로 전달한다. 이렇게 하는 이유는, 이 파일은 왜 포함되지 @@ -81,7 +104,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, @@ -89,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; } @@ -103,9 +130,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(); @@ -124,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 d3ad3de..07c8c27 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'; @@ -22,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'; @@ -54,22 +55,40 @@ 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((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((sourceFile) => { + const original = sourceFile.getFilePath().toString(); + return [caseMap.get(original) ?? original, sourceFile]; + }), ); + 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, }); + // 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, @@ -86,16 +105,22 @@ 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, 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(); @@ -113,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/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 b34e517..1215594 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'; @@ -11,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'; @@ -34,7 +36,14 @@ 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}`); + Debugger.it.logList('[module] tsconfig fileNames', extendOptions.tsconfig.fileNames); const include = new IncludeContainer({ config: { include: getTsIncludeFiles({ config: moduleOption, extend: extendOptions }) }, @@ -53,13 +62,22 @@ 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, }); - 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(); @@ -123,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/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/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/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/__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..378e056 --- /dev/null +++ b/src/modules/path/__tests__/build.correct.case.path.map.test.ts @@ -0,0 +1,146 @@ +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 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); + + 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/__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 new file mode 100644 index 0000000..a91a648 --- /dev/null +++ b/src/modules/path/buildCorrectCasePathMap.ts @@ -0,0 +1,129 @@ +/* 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) { + // dirEntries is built from dirToInputs.keys(), so get() is always defined here + /* v8 ignore next */ + 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((file) => chalk.yellow(file)).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; +} diff --git a/src/modules/path/getCorrectCasedPath.ts b/src/modules/path/getCorrectCasedPath.ts index c48403a..21fedf0 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'; @@ -13,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에서 발생하는 문제이다. // 이 부분을 올바르게 수정할 수 있는 방법을 연구해야 한다. @@ -65,9 +69,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/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(); } diff --git a/src/modules/scope/ExcludeContainer.ts b/src/modules/scope/ExcludeContainer.ts index ff15bab..06b293a 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[]; @@ -40,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[]> { @@ -52,16 +66,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..7798ba4 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,13 +6,25 @@ 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[]; #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, @@ -21,6 +34,23 @@ 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}"`); + 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 — defaulting to **/*.ts, **/*.tsx within cwd', + ); + } + + Debugger.it.logList( + 'IncludeContainer: resolved files in map', + files.map(([key]) => key), + ); } get globs(): Readonly[]> { @@ -33,15 +63,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..64d3488 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({ @@ -16,14 +25,23 @@ describe('IncludeContainer', () => { expect(container.map).toBeDefined(); }); - it('isInclude - no glob files', () => { + 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'); - expect(r01).toBeFalsy(); + // 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(); }); it('isInclude', () => { @@ -75,6 +93,68 @@ 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('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', { 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}`); 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 {