diff --git a/Cargo.lock b/Cargo.lock index a76ee7bd..e546d850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" @@ -378,6 +384,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "fast-glob" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d26eec0ae9682c457cb0f85de67ad417b716ae852736a5d94c2ad6e92a997c9" +dependencies = [ + "arrayvec", +] + [[package]] name = "filetime" version = "0.2.26" @@ -826,6 +841,7 @@ dependencies = [ "dirs", "document-features", "fancy-regex", + "fast-glob", "indexmap", "json-strip-comments", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 17824597..496aea7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ name = "resolver" [dependencies] cfg-if = "1" +fast-glob = "1.0.0" indexmap = { version = "2", features = ["serde"] } json-strip-comments = "3" once_cell = "1" # Use `std::sync::OnceLock::get_or_try_init` when it is stable. diff --git a/fixtures/tsconfig/cases/absolute_patterns/excluded/file.ts b/fixtures/tsconfig/cases/absolute_patterns/excluded/file.ts new file mode 100644 index 00000000..b713dbbe --- /dev/null +++ b/fixtures/tsconfig/cases/absolute_patterns/excluded/file.ts @@ -0,0 +1 @@ +export const excluded = "excluded"; diff --git a/fixtures/tsconfig/cases/absolute_patterns/src/index.ts b/fixtures/tsconfig/cases/absolute_patterns/src/index.ts new file mode 100644 index 00000000..25e8c809 --- /dev/null +++ b/fixtures/tsconfig/cases/absolute_patterns/src/index.ts @@ -0,0 +1 @@ +export const index = "index"; diff --git a/fixtures/tsconfig/cases/absolute_patterns/tsconfig.json b/fixtures/tsconfig/cases/absolute_patterns/tsconfig.json new file mode 100644 index 00000000..cbadbc3e --- /dev/null +++ b/fixtures/tsconfig/cases/absolute_patterns/tsconfig.json @@ -0,0 +1,4 @@ +{ + "include": ["src/**/*.ts"], + "exclude": ["excluded"] +} diff --git a/fixtures/tsconfig/cases/case_sensitivity/src/index.ts b/fixtures/tsconfig/cases/case_sensitivity/src/index.ts new file mode 100644 index 00000000..c5ac122d --- /dev/null +++ b/fixtures/tsconfig/cases/case_sensitivity/src/index.ts @@ -0,0 +1 @@ +export const upper = "upper"; diff --git a/fixtures/tsconfig/cases/case_sensitivity/tsconfig.json b/fixtures/tsconfig/cases/case_sensitivity/tsconfig.json new file mode 100644 index 00000000..5d9f97af --- /dev/null +++ b/fixtures/tsconfig/cases/case_sensitivity/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["src/**/*.ts"] +} diff --git a/fixtures/tsconfig/cases/character_set_patterns/App.ts b/fixtures/tsconfig/cases/character_set_patterns/App.ts new file mode 100644 index 00000000..fc3cc42b --- /dev/null +++ b/fixtures/tsconfig/cases/character_set_patterns/App.ts @@ -0,0 +1 @@ +export const app2 = "app2"; diff --git a/fixtures/tsconfig/cases/character_set_patterns/Button.ts b/fixtures/tsconfig/cases/character_set_patterns/Button.ts new file mode 100644 index 00000000..b278dd42 --- /dev/null +++ b/fixtures/tsconfig/cases/character_set_patterns/Button.ts @@ -0,0 +1 @@ +export const button2 = "button2"; diff --git a/fixtures/tsconfig/cases/character_set_patterns/tsconfig.json b/fixtures/tsconfig/cases/character_set_patterns/tsconfig.json new file mode 100644 index 00000000..c8214950 --- /dev/null +++ b/fixtures/tsconfig/cases/character_set_patterns/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["*.ts"] +} diff --git a/fixtures/tsconfig/cases/complex_patterns/src/deep/nested/test/component.spec.ts b/fixtures/tsconfig/cases/complex_patterns/src/deep/nested/test/component.spec.ts new file mode 100644 index 00000000..9aeb9fd1 --- /dev/null +++ b/fixtures/tsconfig/cases/complex_patterns/src/deep/nested/test/component.spec.ts @@ -0,0 +1 @@ +export const component = "component"; diff --git a/fixtures/tsconfig/cases/complex_patterns/src/index.ts b/fixtures/tsconfig/cases/complex_patterns/src/index.ts new file mode 100644 index 00000000..25e8c809 --- /dev/null +++ b/fixtures/tsconfig/cases/complex_patterns/src/index.ts @@ -0,0 +1 @@ +export const index = "index"; diff --git a/fixtures/tsconfig/cases/complex_patterns/src/test/unit.spec.ts b/fixtures/tsconfig/cases/complex_patterns/src/test/unit.spec.ts new file mode 100644 index 00000000..be6ff32e --- /dev/null +++ b/fixtures/tsconfig/cases/complex_patterns/src/test/unit.spec.ts @@ -0,0 +1 @@ +export const spec = "spec"; diff --git a/fixtures/tsconfig/cases/complex_patterns/src/test/unit.test.ts b/fixtures/tsconfig/cases/complex_patterns/src/test/unit.test.ts new file mode 100644 index 00000000..99be6fe5 --- /dev/null +++ b/fixtures/tsconfig/cases/complex_patterns/src/test/unit.test.ts @@ -0,0 +1 @@ +export const test = "test"; diff --git a/fixtures/tsconfig/cases/complex_patterns/src/utils/helper.ts b/fixtures/tsconfig/cases/complex_patterns/src/utils/helper.ts new file mode 100644 index 00000000..3a5ec535 --- /dev/null +++ b/fixtures/tsconfig/cases/complex_patterns/src/utils/helper.ts @@ -0,0 +1 @@ +export const utils = "utils"; diff --git a/fixtures/tsconfig/cases/complex_patterns/src/utils/test/helper.spec.ts b/fixtures/tsconfig/cases/complex_patterns/src/utils/test/helper.spec.ts new file mode 100644 index 00000000..6119c1e3 --- /dev/null +++ b/fixtures/tsconfig/cases/complex_patterns/src/utils/test/helper.spec.ts @@ -0,0 +1 @@ +export const helper = "helper"; diff --git a/fixtures/tsconfig/cases/complex_patterns/tsconfig.json b/fixtures/tsconfig/cases/complex_patterns/tsconfig.json new file mode 100644 index 00000000..1960cdfb --- /dev/null +++ b/fixtures/tsconfig/cases/complex_patterns/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["src/**/test/**/*.spec.ts"] +} diff --git a/fixtures/tsconfig/cases/configdir_syntax/dist/output.js b/fixtures/tsconfig/cases/configdir_syntax/dist/output.js new file mode 100644 index 00000000..9602fdec --- /dev/null +++ b/fixtures/tsconfig/cases/configdir_syntax/dist/output.js @@ -0,0 +1 @@ +// Built file diff --git a/fixtures/tsconfig/cases/configdir_syntax/index.ts b/fixtures/tsconfig/cases/configdir_syntax/index.ts new file mode 100644 index 00000000..f7ef6a6c --- /dev/null +++ b/fixtures/tsconfig/cases/configdir_syntax/index.ts @@ -0,0 +1 @@ +export const index = 'index'; diff --git a/fixtures/tsconfig/cases/configdir_syntax/log.ts b/fixtures/tsconfig/cases/configdir_syntax/log.ts new file mode 100644 index 00000000..d77f3724 --- /dev/null +++ b/fixtures/tsconfig/cases/configdir_syntax/log.ts @@ -0,0 +1 @@ +export const log = console.log.bind(console); diff --git a/fixtures/tsconfig/cases/configdir_syntax/src/index.ts b/fixtures/tsconfig/cases/configdir_syntax/src/index.ts new file mode 100644 index 00000000..335f9615 --- /dev/null +++ b/fixtures/tsconfig/cases/configdir_syntax/src/index.ts @@ -0,0 +1 @@ +export const src = 'src'; diff --git a/fixtures/tsconfig/cases/configdir_syntax/tsconfig.json b/fixtures/tsconfig/cases/configdir_syntax/tsconfig.json new file mode 100644 index 00000000..d484647f --- /dev/null +++ b/fixtures/tsconfig/cases/configdir_syntax/tsconfig.json @@ -0,0 +1,9 @@ +{ + "include": ["${configDir}/*.ts"], + "exclude": ["dist"], + "compilerOptions": { + "paths": { + "~/*": ["${configDir}/*"] + } + } +} diff --git a/fixtures/tsconfig/cases/empty_files_no_include/index.ts b/fixtures/tsconfig/cases/empty_files_no_include/index.ts new file mode 100644 index 00000000..f7ef6a6c --- /dev/null +++ b/fixtures/tsconfig/cases/empty_files_no_include/index.ts @@ -0,0 +1 @@ +export const index = 'index'; diff --git a/fixtures/tsconfig/cases/empty_files_no_include/tsconfig.json b/fixtures/tsconfig/cases/empty_files_no_include/tsconfig.json new file mode 100644 index 00000000..d946318d --- /dev/null +++ b/fixtures/tsconfig/cases/empty_files_no_include/tsconfig.json @@ -0,0 +1,3 @@ +{ + "files": [] +} diff --git a/fixtures/tsconfig/cases/empty_include/index.ts b/fixtures/tsconfig/cases/empty_include/index.ts new file mode 100644 index 00000000..f7ef6a6c --- /dev/null +++ b/fixtures/tsconfig/cases/empty_include/index.ts @@ -0,0 +1 @@ +export const index = 'index'; diff --git a/fixtures/tsconfig/cases/empty_include/tsconfig.json b/fixtures/tsconfig/cases/empty_include/tsconfig.json new file mode 100644 index 00000000..8de5ab30 --- /dev/null +++ b/fixtures/tsconfig/cases/empty_include/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": [] +} diff --git a/fixtures/tsconfig/cases/exclude_basic/node_modules/foo.ts b/fixtures/tsconfig/cases/exclude_basic/node_modules/foo.ts new file mode 100644 index 00000000..3329a7d9 --- /dev/null +++ b/fixtures/tsconfig/cases/exclude_basic/node_modules/foo.ts @@ -0,0 +1 @@ +export const foo = 'foo'; diff --git a/fixtures/tsconfig/cases/exclude_basic/src/helper.ts b/fixtures/tsconfig/cases/exclude_basic/src/helper.ts new file mode 100644 index 00000000..276ec414 --- /dev/null +++ b/fixtures/tsconfig/cases/exclude_basic/src/helper.ts @@ -0,0 +1 @@ +export const helper = 'helper'; diff --git a/fixtures/tsconfig/cases/exclude_basic/src/index.test.ts b/fixtures/tsconfig/cases/exclude_basic/src/index.test.ts new file mode 100644 index 00000000..59ad34fe --- /dev/null +++ b/fixtures/tsconfig/cases/exclude_basic/src/index.test.ts @@ -0,0 +1 @@ +export const test = 'test'; diff --git a/fixtures/tsconfig/cases/exclude_basic/src/index.ts b/fixtures/tsconfig/cases/exclude_basic/src/index.ts new file mode 100644 index 00000000..3fecd0a3 --- /dev/null +++ b/fixtures/tsconfig/cases/exclude_basic/src/index.ts @@ -0,0 +1,3 @@ +import { helper } from '@/helper'; + +export { helper }; diff --git a/fixtures/tsconfig/cases/exclude_basic/tsconfig.json b/fixtures/tsconfig/cases/exclude_basic/tsconfig.json new file mode 100644 index 00000000..324f0756 --- /dev/null +++ b/fixtures/tsconfig/cases/exclude_basic/tsconfig.json @@ -0,0 +1,10 @@ +{ + "include": ["**/*.ts"], + "exclude": ["**/*.test.ts"], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/fixtures/tsconfig/cases/extends_paths/my-app/index.ts b/fixtures/tsconfig/cases/extends_paths/my-app/index.ts new file mode 100644 index 00000000..25e8c809 --- /dev/null +++ b/fixtures/tsconfig/cases/extends_paths/my-app/index.ts @@ -0,0 +1 @@ +export const index = "index"; diff --git a/fixtures/tsconfig/cases/extends_paths/my-app/message.ts b/fixtures/tsconfig/cases/extends_paths/my-app/message.ts new file mode 100644 index 00000000..2d85992d --- /dev/null +++ b/fixtures/tsconfig/cases/extends_paths/my-app/message.ts @@ -0,0 +1 @@ +export const message = "message"; diff --git a/fixtures/tsconfig/cases/extends_paths/my-app/tsconfig.json b/fixtures/tsconfig/cases/extends_paths/my-app/tsconfig.json new file mode 100644 index 00000000..7f41c254 --- /dev/null +++ b/fixtures/tsconfig/cases/extends_paths/my-app/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["./"] +} diff --git a/fixtures/tsconfig/cases/extends_paths/tsconfig.json b/fixtures/tsconfig/cases/extends_paths/tsconfig.json new file mode 100644 index 00000000..b2acc37a --- /dev/null +++ b/fixtures/tsconfig/cases/extends_paths/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./my-app/*"] + } + }, + "include": [], + "references": [{ "path": "./my-app/" }] +} diff --git a/fixtures/tsconfig/cases/files_priority/other.ts b/fixtures/tsconfig/cases/files_priority/other.ts new file mode 100644 index 00000000..36aef5ae --- /dev/null +++ b/fixtures/tsconfig/cases/files_priority/other.ts @@ -0,0 +1 @@ +export const other = 'other'; diff --git a/fixtures/tsconfig/cases/files_priority/test.ts b/fixtures/tsconfig/cases/files_priority/test.ts new file mode 100644 index 00000000..59ad34fe --- /dev/null +++ b/fixtures/tsconfig/cases/files_priority/test.ts @@ -0,0 +1 @@ +export const test = 'test'; diff --git a/fixtures/tsconfig/cases/files_priority/tsconfig.json b/fixtures/tsconfig/cases/files_priority/tsconfig.json new file mode 100644 index 00000000..1323713f --- /dev/null +++ b/fixtures/tsconfig/cases/files_priority/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": ["test.ts"], + "exclude": ["test.ts"] +} diff --git a/fixtures/tsconfig/cases/globstar_patterns/index.js b/fixtures/tsconfig/cases/globstar_patterns/index.js new file mode 100644 index 00000000..0af6ac66 --- /dev/null +++ b/fixtures/tsconfig/cases/globstar_patterns/index.js @@ -0,0 +1 @@ +export const js = 'js'; diff --git a/fixtures/tsconfig/cases/globstar_patterns/index.ts b/fixtures/tsconfig/cases/globstar_patterns/index.ts new file mode 100644 index 00000000..f7ef6a6c --- /dev/null +++ b/fixtures/tsconfig/cases/globstar_patterns/index.ts @@ -0,0 +1 @@ +export const index = 'index'; diff --git a/fixtures/tsconfig/cases/globstar_patterns/src/index.ts b/fixtures/tsconfig/cases/globstar_patterns/src/index.ts new file mode 100644 index 00000000..335f9615 --- /dev/null +++ b/fixtures/tsconfig/cases/globstar_patterns/src/index.ts @@ -0,0 +1 @@ +export const src = 'src'; diff --git a/fixtures/tsconfig/cases/globstar_patterns/src/utils/deep/nested/file.ts b/fixtures/tsconfig/cases/globstar_patterns/src/utils/deep/nested/file.ts new file mode 100644 index 00000000..a0a2710b --- /dev/null +++ b/fixtures/tsconfig/cases/globstar_patterns/src/utils/deep/nested/file.ts @@ -0,0 +1 @@ +export const file = 'file'; diff --git a/fixtures/tsconfig/cases/globstar_patterns/src/utils/helper.ts b/fixtures/tsconfig/cases/globstar_patterns/src/utils/helper.ts new file mode 100644 index 00000000..276ec414 --- /dev/null +++ b/fixtures/tsconfig/cases/globstar_patterns/src/utils/helper.ts @@ -0,0 +1 @@ +export const helper = 'helper'; diff --git a/fixtures/tsconfig/cases/globstar_patterns/tsconfig.json b/fixtures/tsconfig/cases/globstar_patterns/tsconfig.json new file mode 100644 index 00000000..ff3a2302 --- /dev/null +++ b/fixtures/tsconfig/cases/globstar_patterns/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["**/*.ts"] +} diff --git a/fixtures/tsconfig/cases/include_basic/dist/output.js b/fixtures/tsconfig/cases/include_basic/dist/output.js new file mode 100644 index 00000000..9602fdec --- /dev/null +++ b/fixtures/tsconfig/cases/include_basic/dist/output.js @@ -0,0 +1 @@ +// Built file diff --git a/fixtures/tsconfig/cases/include_basic/src/index.ts b/fixtures/tsconfig/cases/include_basic/src/index.ts new file mode 100644 index 00000000..88ab6922 --- /dev/null +++ b/fixtures/tsconfig/cases/include_basic/src/index.ts @@ -0,0 +1,3 @@ +import { helper } from '@/utils/helper'; + +export { helper }; diff --git a/fixtures/tsconfig/cases/include_basic/src/utils/helper.ts b/fixtures/tsconfig/cases/include_basic/src/utils/helper.ts new file mode 100644 index 00000000..276ec414 --- /dev/null +++ b/fixtures/tsconfig/cases/include_basic/src/utils/helper.ts @@ -0,0 +1 @@ +export const helper = 'helper'; diff --git a/fixtures/tsconfig/cases/include_basic/test.ts b/fixtures/tsconfig/cases/include_basic/test.ts new file mode 100644 index 00000000..59ad34fe --- /dev/null +++ b/fixtures/tsconfig/cases/include_basic/test.ts @@ -0,0 +1 @@ +export const test = 'test'; diff --git a/fixtures/tsconfig/cases/include_basic/tsconfig.json b/fixtures/tsconfig/cases/include_basic/tsconfig.json new file mode 100644 index 00000000..0dbc9a7f --- /dev/null +++ b/fixtures/tsconfig/cases/include_basic/tsconfig.json @@ -0,0 +1,9 @@ +{ + "include": ["src/**/*.ts"], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/fixtures/tsconfig/cases/include_exclude_extends/base-tsconfig.json b/fixtures/tsconfig/cases/include_exclude_extends/base-tsconfig.json new file mode 100644 index 00000000..490f3027 --- /dev/null +++ b/fixtures/tsconfig/cases/include_exclude_extends/base-tsconfig.json @@ -0,0 +1,4 @@ +{ + "include": ["**/*.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/fixtures/tsconfig/cases/include_exclude_extends/lib/utils.ts b/fixtures/tsconfig/cases/include_exclude_extends/lib/utils.ts new file mode 100644 index 00000000..5ebf4105 --- /dev/null +++ b/fixtures/tsconfig/cases/include_exclude_extends/lib/utils.ts @@ -0,0 +1 @@ +export const utils = 'utils'; diff --git a/fixtures/tsconfig/cases/include_exclude_extends/src/index.test.ts b/fixtures/tsconfig/cases/include_exclude_extends/src/index.test.ts new file mode 100644 index 00000000..59ad34fe --- /dev/null +++ b/fixtures/tsconfig/cases/include_exclude_extends/src/index.test.ts @@ -0,0 +1 @@ +export const test = 'test'; diff --git a/fixtures/tsconfig/cases/include_exclude_extends/src/index.ts b/fixtures/tsconfig/cases/include_exclude_extends/src/index.ts new file mode 100644 index 00000000..f7ef6a6c --- /dev/null +++ b/fixtures/tsconfig/cases/include_exclude_extends/src/index.ts @@ -0,0 +1 @@ +export const index = 'index'; diff --git a/fixtures/tsconfig/cases/include_exclude_extends/tsconfig.json b/fixtures/tsconfig/cases/include_exclude_extends/tsconfig.json new file mode 100644 index 00000000..66028f7f --- /dev/null +++ b/fixtures/tsconfig/cases/include_exclude_extends/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./base-tsconfig.json", + "include": ["src/**/*.ts"] +} diff --git a/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/dist/index.js b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/dist/index.js new file mode 100644 index 00000000..2ecd1290 --- /dev/null +++ b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/dist/index.js @@ -0,0 +1 @@ +// dist diff --git a/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/src/index.ts b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/src/index.ts new file mode 100644 index 00000000..0b1e65b0 --- /dev/null +++ b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/src/index.ts @@ -0,0 +1 @@ +export const pkgA = "pkgA"; diff --git a/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/test.ts b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/test.ts new file mode 100644 index 00000000..99be6fe5 --- /dev/null +++ b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-a/test.ts @@ -0,0 +1 @@ +export const test = "test"; diff --git a/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-b/src/utils.ts b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-b/src/utils.ts new file mode 100644 index 00000000..918bfbda --- /dev/null +++ b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-b/src/utils.ts @@ -0,0 +1 @@ +export const pkgB = "pkgB"; diff --git a/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-c/src/deep/nested/file.ts b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-c/src/deep/nested/file.ts new file mode 100644 index 00000000..561f30df --- /dev/null +++ b/fixtures/tsconfig/cases/monorepo_patterns/packages/pkg-c/src/deep/nested/file.ts @@ -0,0 +1 @@ +export const pkgC = "pkgC"; diff --git a/fixtures/tsconfig/cases/monorepo_patterns/shared/utils.ts b/fixtures/tsconfig/cases/monorepo_patterns/shared/utils.ts new file mode 100644 index 00000000..265b90b3 --- /dev/null +++ b/fixtures/tsconfig/cases/monorepo_patterns/shared/utils.ts @@ -0,0 +1 @@ +export const shared = "shared"; diff --git a/fixtures/tsconfig/cases/monorepo_patterns/tsconfig.json b/fixtures/tsconfig/cases/monorepo_patterns/tsconfig.json new file mode 100644 index 00000000..07f0de90 --- /dev/null +++ b/fixtures/tsconfig/cases/monorepo_patterns/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["packages/*/src/**/*"] +} diff --git a/fixtures/tsconfig/cases/outdir_exclude/dist/index.d.ts b/fixtures/tsconfig/cases/outdir_exclude/dist/index.d.ts new file mode 100644 index 00000000..c213b620 --- /dev/null +++ b/fixtures/tsconfig/cases/outdir_exclude/dist/index.d.ts @@ -0,0 +1 @@ +// Declaration diff --git a/fixtures/tsconfig/cases/outdir_exclude/dist/index.js b/fixtures/tsconfig/cases/outdir_exclude/dist/index.js new file mode 100644 index 00000000..02660bc1 --- /dev/null +++ b/fixtures/tsconfig/cases/outdir_exclude/dist/index.js @@ -0,0 +1 @@ +// Built diff --git a/fixtures/tsconfig/cases/outdir_exclude/src/index.ts b/fixtures/tsconfig/cases/outdir_exclude/src/index.ts new file mode 100644 index 00000000..25e8c809 --- /dev/null +++ b/fixtures/tsconfig/cases/outdir_exclude/src/index.ts @@ -0,0 +1 @@ +export const index = "index"; diff --git a/fixtures/tsconfig/cases/outdir_exclude/tsconfig.json b/fixtures/tsconfig/cases/outdir_exclude/tsconfig.json new file mode 100644 index 00000000..f90163c8 --- /dev/null +++ b/fixtures/tsconfig/cases/outdir_exclude/tsconfig.json @@ -0,0 +1,6 @@ +{ + "include": ["**/*.ts", "**/*.js", "**/*.d.ts"], + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/fixtures/tsconfig/cases/paths_outside_root/my-app/index.ts b/fixtures/tsconfig/cases/paths_outside_root/my-app/index.ts new file mode 100644 index 00000000..25e8c809 --- /dev/null +++ b/fixtures/tsconfig/cases/paths_outside_root/my-app/index.ts @@ -0,0 +1 @@ +export const index = "index"; diff --git a/fixtures/tsconfig/cases/paths_outside_root/my-app/tsconfig.json b/fixtures/tsconfig/cases/paths_outside_root/my-app/tsconfig.json new file mode 100644 index 00000000..f501eed2 --- /dev/null +++ b/fixtures/tsconfig/cases/paths_outside_root/my-app/tsconfig.json @@ -0,0 +1,8 @@ +{ + "exclude": ["dist"], + "compilerOptions": { + "paths": { + "~utils/*": ["../my-utils/*"] + } + } +} diff --git a/fixtures/tsconfig/cases/paths_outside_root/my-utils/log.ts b/fixtures/tsconfig/cases/paths_outside_root/my-utils/log.ts new file mode 100644 index 00000000..d77f3724 --- /dev/null +++ b/fixtures/tsconfig/cases/paths_outside_root/my-utils/log.ts @@ -0,0 +1 @@ +export const log = console.log.bind(console); diff --git a/fixtures/tsconfig/cases/wildcard_patterns/index.ts b/fixtures/tsconfig/cases/wildcard_patterns/index.ts new file mode 100644 index 00000000..f7ed9094 --- /dev/null +++ b/fixtures/tsconfig/cases/wildcard_patterns/index.ts @@ -0,0 +1 @@ +export const root = "root"; diff --git a/fixtures/tsconfig/cases/wildcard_patterns/src/helper.ts b/fixtures/tsconfig/cases/wildcard_patterns/src/helper.ts new file mode 100644 index 00000000..6119c1e3 --- /dev/null +++ b/fixtures/tsconfig/cases/wildcard_patterns/src/helper.ts @@ -0,0 +1 @@ +export const helper = "helper"; diff --git a/fixtures/tsconfig/cases/wildcard_patterns/src/index.ts b/fixtures/tsconfig/cases/wildcard_patterns/src/index.ts new file mode 100644 index 00000000..25e8c809 --- /dev/null +++ b/fixtures/tsconfig/cases/wildcard_patterns/src/index.ts @@ -0,0 +1 @@ +export const index = "index"; diff --git a/fixtures/tsconfig/cases/wildcard_patterns/src/utils/helper.ts b/fixtures/tsconfig/cases/wildcard_patterns/src/utils/helper.ts new file mode 100644 index 00000000..64239695 --- /dev/null +++ b/fixtures/tsconfig/cases/wildcard_patterns/src/utils/helper.ts @@ -0,0 +1 @@ +export const nested = "nested"; diff --git a/fixtures/tsconfig/cases/wildcard_patterns/tsconfig.json b/fixtures/tsconfig/cases/wildcard_patterns/tsconfig.json new file mode 100644 index 00000000..be202e17 --- /dev/null +++ b/fixtures/tsconfig/cases/wildcard_patterns/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["src/*.ts"] +} diff --git a/fixtures/tsconfig/cases/with_baseurl/dist/output.js b/fixtures/tsconfig/cases/with_baseurl/dist/output.js new file mode 100644 index 00000000..9602fdec --- /dev/null +++ b/fixtures/tsconfig/cases/with_baseurl/dist/output.js @@ -0,0 +1 @@ +// Built file diff --git a/fixtures/tsconfig/cases/with_baseurl/index.ts b/fixtures/tsconfig/cases/with_baseurl/index.ts new file mode 100644 index 00000000..43200da0 --- /dev/null +++ b/fixtures/tsconfig/cases/with_baseurl/index.ts @@ -0,0 +1,4 @@ +import { log } from '~/log'; +import { log as log2 } from 'log'; + +export { log, log2 }; diff --git a/fixtures/tsconfig/cases/with_baseurl/log.ts b/fixtures/tsconfig/cases/with_baseurl/log.ts new file mode 100644 index 00000000..d77f3724 --- /dev/null +++ b/fixtures/tsconfig/cases/with_baseurl/log.ts @@ -0,0 +1 @@ +export const log = console.log.bind(console); diff --git a/fixtures/tsconfig/cases/with_baseurl/tsconfig.json b/fixtures/tsconfig/cases/with_baseurl/tsconfig.json new file mode 100644 index 00000000..e9e91f99 --- /dev/null +++ b/fixtures/tsconfig/cases/with_baseurl/tsconfig.json @@ -0,0 +1,9 @@ +{ + "exclude": ["dist"], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/*": ["./*"] + } + } +} diff --git a/fixtures/tsconfig/cases/without_baseurl/bower_components/lib.ts b/fixtures/tsconfig/cases/without_baseurl/bower_components/lib.ts new file mode 100644 index 00000000..da7d8c96 --- /dev/null +++ b/fixtures/tsconfig/cases/without_baseurl/bower_components/lib.ts @@ -0,0 +1 @@ +export const lib = 'lib'; diff --git a/fixtures/tsconfig/cases/without_baseurl/dist/output.js b/fixtures/tsconfig/cases/without_baseurl/dist/output.js new file mode 100644 index 00000000..9602fdec --- /dev/null +++ b/fixtures/tsconfig/cases/without_baseurl/dist/output.js @@ -0,0 +1 @@ +// Built file diff --git a/fixtures/tsconfig/cases/without_baseurl/index.ts b/fixtures/tsconfig/cases/without_baseurl/index.ts new file mode 100644 index 00000000..e5bfcda7 --- /dev/null +++ b/fixtures/tsconfig/cases/without_baseurl/index.ts @@ -0,0 +1,3 @@ +import { log } from './log'; + +export { log }; diff --git a/fixtures/tsconfig/cases/without_baseurl/jspm_packages/mod.ts b/fixtures/tsconfig/cases/without_baseurl/jspm_packages/mod.ts new file mode 100644 index 00000000..7a595a68 --- /dev/null +++ b/fixtures/tsconfig/cases/without_baseurl/jspm_packages/mod.ts @@ -0,0 +1 @@ +export const mod = 'mod'; diff --git a/fixtures/tsconfig/cases/without_baseurl/log.ts b/fixtures/tsconfig/cases/without_baseurl/log.ts new file mode 100644 index 00000000..d77f3724 --- /dev/null +++ b/fixtures/tsconfig/cases/without_baseurl/log.ts @@ -0,0 +1 @@ +export const log = console.log.bind(console); diff --git a/fixtures/tsconfig/cases/without_baseurl/node_modules/package/index.ts b/fixtures/tsconfig/cases/without_baseurl/node_modules/package/index.ts new file mode 100644 index 00000000..6c87163d --- /dev/null +++ b/fixtures/tsconfig/cases/without_baseurl/node_modules/package/index.ts @@ -0,0 +1 @@ +export const pkg = 'pkg'; diff --git a/fixtures/tsconfig/cases/without_baseurl/tsconfig.json b/fixtures/tsconfig/cases/without_baseurl/tsconfig.json new file mode 100644 index 00000000..0adc593a --- /dev/null +++ b/fixtures/tsconfig/cases/without_baseurl/tsconfig.json @@ -0,0 +1,8 @@ +{ + "exclude": ["dist"], + "compilerOptions": { + "paths": { + "~/*": ["./*"] + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 31224bb3..55259c3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1479,6 +1479,14 @@ impl ResolverGeneric { } }; + // Check if the importer file matches the tsconfig's include/exclude patterns + // Only check for actual files, not directories (directories are used when there's no specific importer) + // If the importer doesn't match, don't use this tsconfig's path mappings + let is_file = cached_path.meta(&self.cache.fs).is_some_and(|m| m.is_file); + if is_file && !tsconfig.matches_file(cached_path.path()) { + return Ok(None); + } + let paths = tsconfig.resolve(cached_path.path(), specifier); for path in paths { let resolved_path = self.cache.value(&path); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 68054156..7d93c154 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -26,6 +26,8 @@ mod simple; mod symlink; mod tsconfig_discovery; mod tsconfig_extends; +mod tsconfig_file_matcher; +mod tsconfig_include_exclude; mod tsconfig_paths; mod tsconfig_project_references; #[cfg(target_os = "windows")] diff --git a/src/tests/tsconfig_file_matcher.rs b/src/tests/tsconfig_file_matcher.rs new file mode 100644 index 00000000..9d2d5406 --- /dev/null +++ b/src/tests/tsconfig_file_matcher.rs @@ -0,0 +1,249 @@ +//! Unit tests for TsconfigFileMatcher pattern matching +//! +//! Tests various glob patterns, include/exclude logic, and edge cases +//! using fixtures from tsconfig/cases/ + +use crate::tsconfig::TsconfigFileMatcher; +use std::path::PathBuf; + +/// Helper to create a TsconfigFileMatcher from a fixture directory +fn create_matcher_from_fixture(fixture_name: &str) -> (TsconfigFileMatcher, PathBuf) { + let fixture_dir = super::fixture_root().join("tsconfig/cases").join(fixture_name); + let tsconfig_path = fixture_dir.join("tsconfig.json"); + + // Read and parse tsconfig.json + let tsconfig_str = std::fs::read_to_string(&tsconfig_path) + .unwrap_or_else(|_| panic!("Failed to read tsconfig.json from {fixture_name}")); + let json: serde_json::Value = serde_json::from_str(&tsconfig_str) + .unwrap_or_else(|_| panic!("Failed to parse tsconfig.json from {fixture_name}")); + + // Helper to substitute ${configDir} template variable + #[allow(clippy::option_if_let_else)] // map_or causes borrow checker issues + let substitute_template = |s: String| -> String { + match s.strip_prefix("${configDir}") { + Some(stripped) => fixture_dir.to_str().unwrap().to_string() + stripped, + None => s, + } + }; + + // Extract fields + let files = json.get("files").and_then(|v| { + v.as_array().map(|arr| { + arr.iter() + .filter_map(|s| s.as_str().map(String::from)) + .map(substitute_template) + .collect() + }) + }); + + let include = json.get("include").and_then(|v| { + v.as_array().map(|arr| { + arr.iter() + .filter_map(|s| s.as_str().map(String::from)) + .map(substitute_template) + .collect() + }) + }); + + let exclude = json.get("exclude").and_then(|v| { + v.as_array().map(|arr| { + arr.iter() + .filter_map(|s| s.as_str().map(String::from)) + .map(substitute_template) + .collect() + }) + }); + + let out_dir = json + .get("compilerOptions") + .and_then(|opts| opts.get("outDir")) + .and_then(|v| v.as_str()) + .map(std::path::Path::new); + + // Create matcher + let matcher = TsconfigFileMatcher::new(files, include, exclude, out_dir, fixture_dir.clone()); + + (matcher, fixture_dir) +} + +#[test] +fn test_globstar_patterns() { + let (matcher, fixture_dir) = create_matcher_from_fixture("globstar_patterns"); + + // Test cases: (path, should_match, description) + let test_cases = [ + ("index.ts", true, "globstar matches root level"), + ("src/index.ts", true, "globstar matches one level deep"), + ("src/utils/helper.ts", true, "globstar matches two levels deep"), + ("src/utils/deep/nested/file.ts", true, "globstar matches deeply nested"), + ("index.js", false, "globstar doesn't match .js files"), + ("src/index.js", false, "globstar doesn't match .js files in subdirs"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_wildcard_patterns() { + let (matcher, fixture_dir) = create_matcher_from_fixture("wildcard_patterns"); + + let test_cases = [ + ("src/index.ts", true, "wildcard matches files in src/"), + ("src/helper.ts", true, "wildcard matches files in src/"), + ("src/utils/helper.ts", false, "wildcard doesn't match subdirectories"), + ("index.ts", false, "wildcard doesn't match parent directory"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_simple_wildcard_patterns() { + let (matcher, fixture_dir) = create_matcher_from_fixture("character_set_patterns"); + + let test_cases = [ + ("App.ts", true, "wildcard matches .ts files"), + ("Button.ts", true, "wildcard matches .ts files"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_complex_patterns() { + let (matcher, fixture_dir) = create_matcher_from_fixture("complex_patterns"); + + let test_cases = [ + ("src/test/unit.spec.ts", true, "complex pattern matches src/test/*.spec.ts"), + ("src/utils/test/helper.spec.ts", true, "complex pattern matches src/**/test/*.spec.ts"), + ("src/deep/nested/test/component.spec.ts", true, "complex pattern matches deeply nested"), + ("src/index.ts", false, "file not in test directory"), + ("src/utils/helper.ts", false, "file not in test directory"), + ("src/test/unit.test.ts", false, "wrong extension (.test.ts not .spec.ts)"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_monorepo_patterns() { + let (matcher, fixture_dir) = create_matcher_from_fixture("monorepo_patterns"); + + let test_cases = [ + ("packages/pkg-a/src/index.ts", true, "monorepo pattern matches pkg-a"), + ("packages/pkg-b/src/utils.ts", true, "monorepo pattern matches pkg-b"), + ("packages/pkg-c/src/deep/nested/file.ts", true, "monorepo pattern matches nested"), + ("packages/pkg-a/dist/index.js", false, "dist not in src directory"), + ("shared/utils.ts", false, "shared not in packages/*/src"), + ("packages/pkg-a/test.ts", false, "test.ts not in src directory"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_files_priority() { + let (matcher, fixture_dir) = create_matcher_from_fixture("files_priority"); + + let test_cases = [ + ("test.ts", true, "files field overrides exclude"), + ("other.ts", false, "file not in files array"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_outdir_exclude() { + let (matcher, fixture_dir) = create_matcher_from_fixture("outdir_exclude"); + + let test_cases = [ + ("src/index.ts", true, "source files included"), + ("dist/index.js", false, "outDir automatically excluded"), + ("dist/index.d.ts", false, "outDir automatically excluded"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_absolute_patterns() { + let (matcher, fixture_dir) = create_matcher_from_fixture("absolute_patterns"); + + let test_cases = [ + ("src/index.ts", true, "absolute pattern matches"), + ("excluded/file.ts", false, "excluded directory"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_configdir_syntax() { + let (matcher, fixture_dir) = create_matcher_from_fixture("configdir_syntax"); + + let test_cases = [ + ("index.ts", true, "${configDir} matches root level .ts files"), + ("log.ts", true, "${configDir} matches root level .ts files"), + ("dist/output.js", false, "dist excluded"), + ("src/index.ts", false, "${configDir}/*.ts doesn't match subdirectories"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} + +#[test] +fn test_without_baseurl() { + let (matcher, fixture_dir) = create_matcher_from_fixture("without_baseurl"); + + let test_cases = [ + ("index.ts", true, "regular files included"), + ("log.ts", true, "regular files included"), + ("node_modules/package/index.ts", false, "node_modules excluded by default"), + ("bower_components/lib.ts", false, "bower_components excluded by default"), + ("jspm_packages/mod.ts", false, "jspm_packages excluded by default"), + ("dist/output.js", false, "custom exclude pattern works"), + ]; + + for (file_path, should_match, comment) in test_cases { + let full_path = fixture_dir.join(file_path); + let result = matcher.matches(&full_path); + assert_eq!(result, should_match, "{comment}: path={file_path}"); + } +} diff --git a/src/tests/tsconfig_include_exclude.rs b/src/tests/tsconfig_include_exclude.rs new file mode 100644 index 00000000..49a03626 --- /dev/null +++ b/src/tests/tsconfig_include_exclude.rs @@ -0,0 +1,236 @@ +//! Tests for tsconfig `include`, `exclude`, and `files` fields +//! +//! Tests ported from vite-tsconfig-paths: +//! + +use crate::{ResolveOptions, Resolver, TsconfigDiscovery, TsconfigOptions, TsconfigReferences}; + +/// Test include/exclude/files patterns via actual path resolution +/// Tests that tsconfig path mappings are applied when importer is included, +/// and not applied when importer is excluded +#[test] +fn tsconfig_include_exclude_patterns() { + let f = super::fixture_root().join("tsconfig/cases"); + + // (fixture_dir, importer_file, specifier, should_resolve, description) + #[rustfmt::skip] + let test_cases = [ + // Include basic - Pattern: src/**/*.ts + // Files in src/ can use path mappings, files outside cannot + ("include_basic", "src/index.ts", "@/utils/helper", true, "file in src/ can use path mapping"), + ("include_basic", "test.ts", "@/utils/helper", false, "file outside include pattern cannot use path mapping"), + ("include_basic", "dist/output.js", "@/utils/helper", false, "file in dist/ cannot use path mapping"), + + // Exclude basic - Include: **/*.ts, Exclude: **/*.test.ts + // Test files are excluded from using path mappings + ("exclude_basic", "src/index.ts", "@/helper", true, "non-test file can use path mapping"), + ("exclude_basic", "src/index.test.ts", "@/helper", false, "test file excluded from using path mapping"), + ("exclude_basic", "node_modules/foo.ts", "@/helper", false, "node_modules excluded by default"), + + // Default include (no include specified, defaults to **/*) - Exclude: [dist] + // All files except dist/ can use path mappings + ("with_baseurl", "index.ts", "~/log", true, "file in root can use path mapping"), + ("with_baseurl", "index.ts", "log", true, "file in root can use baseUrl"), + ("with_baseurl", "dist/output.js", "~/log", false, "file in excluded dist cannot use path mapping"), + ]; + + for (fixture, importer, specifier, should_resolve, comment) in test_cases { + let fixture_dir = f.join(fixture); + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: fixture_dir.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + extensions: vec![".ts".into(), ".js".into()], + ..ResolveOptions::default() + }); + + let importer_path = fixture_dir.join(importer); + let result = resolver.resolve(&importer_path, specifier); + + if should_resolve { + assert!( + result.is_ok(), + "{comment}: fixture={fixture} importer={importer} specifier={specifier} - expected success but got: {:?}", + result.err() + ); + } else { + assert!( + result.is_err(), + "{comment}: fixture={fixture} importer={importer} specifier={specifier} - expected failure but got: {:?}", + result.ok() + ); + } + } +} + +/// Test empty files array with no include +/// When files is explicitly empty and include is missing/empty, no files should match +#[test] +fn test_empty_files_no_include() { + let f = super::fixture_root().join("tsconfig/cases/empty_files_no_include"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + let tsconfig = resolver.resolve_tsconfig(&f).unwrap(); + + // With empty files and no include, no files should match + assert!(!tsconfig.matches_file(&f.join("index.ts"))); + assert!(!tsconfig.matches_file(&f.join("src/index.ts"))); +} + +/// Test empty include array +/// When include is explicitly set to empty array, no files should be included +#[test] +fn test_empty_include_array() { + let f = super::fixture_root().join("tsconfig/cases/empty_include"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + let tsconfig = resolver.resolve_tsconfig(&f).unwrap(); + + // With empty include, no files should match (unless in files array) + assert!(!tsconfig.matches_file(&f.join("index.ts"))); + assert!(!tsconfig.matches_file(&f.join("src/index.ts"))); +} + +/// Test extends inheritance behavior for include/exclude +/// Verifies that child config's include/exclude patterns override parent's +#[test] +fn test_extends_include_exclude_inheritance() { + let f = super::fixture_root().join("tsconfig/cases/include_exclude_extends"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + let tsconfig = resolver.resolve_tsconfig(&f).unwrap(); + + // Child config's include overrides parent's include + assert!(tsconfig.matches_file(&f.join("src/index.ts"))); + + // Files outside child's include pattern should not be included + // (even if they would match parent's include) + assert!(!tsconfig.matches_file(&f.join("lib/utils.ts"))); + + // Test whether exclude from parent applies to child's include + assert!(!tsconfig.matches_file(&f.join("src/index.test.ts"))); +} + +/// Test project references with include/exclude +/// References allow composing multiple tsconfigs, each with their own include/exclude +#[test] +fn test_project_references_with_include_exclude() { + let f = super::fixture_root().join("tsconfig/cases/extends_paths"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + // Root tsconfig has empty include: [] + let root_tsconfig = resolver.resolve_tsconfig(&f).unwrap(); + + // Root has empty include, so no files at root should match + assert!(!root_tsconfig.matches_file(&f.join("index.ts"))); + + // Referenced project should have its own include + let my_app_dir = f.join("my-app"); + let my_app_tsconfig = resolver.resolve_tsconfig(&my_app_dir).unwrap(); + + // my-app has include: ["./"], so files in my-app should match + assert!(my_app_tsconfig.matches_file(&my_app_dir.join("index.ts"))); + assert!(my_app_tsconfig.matches_file(&my_app_dir.join("message.ts"))); +} + +/// Test paths outside project root +/// Paths can reference files outside the tsconfig directory +#[test] +fn test_paths_outside_root() { + let f = super::fixture_root().join("tsconfig/cases/paths_outside_root"); + let my_app_dir = f.join("my-app"); + let my_utils_dir = f.join("my-utils"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: my_app_dir.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + let tsconfig = resolver.resolve_tsconfig(&my_app_dir).unwrap(); + + // Files in my-app should be included (default include) + assert!(tsconfig.matches_file(&my_app_dir.join("index.ts"))); + + // Files outside the tsconfig directory + assert!(!tsconfig.matches_file(&my_utils_dir.join("log.ts"))); +} + +/// Test case sensitivity on Unix systems +/// On Unix, file paths are case-sensitive +#[test] +#[cfg(not(target_os = "windows"))] +fn test_case_sensitivity_unix() { + let f = super::fixture_root().join("tsconfig/cases/case_sensitivity"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + let tsconfig = resolver.resolve_tsconfig(&f).unwrap(); + + // Pattern: src/**/*.ts + assert!(tsconfig.matches_file(&f.join("src/index.ts"))); + + // On Unix, Src != src (case-sensitive) + assert!(!tsconfig.matches_file(&f.join("Src/index.ts"))); +} + +/// Test case insensitivity on Windows +/// On Windows, file paths are case-insensitive +#[test] +#[cfg(target_os = "windows")] +fn test_case_insensitivity_windows() { + let f = super::fixture_root().join("tsconfig/cases/case_sensitivity"); + + let resolver = Resolver::new(ResolveOptions { + tsconfig: Some(TsconfigDiscovery::Manual(TsconfigOptions { + config_file: f.join("tsconfig.json"), + references: TsconfigReferences::Auto, + })), + ..ResolveOptions::default() + }); + + let tsconfig = resolver.resolve_tsconfig(&f).unwrap(); + + // Pattern: src/**/*.ts + assert!(tsconfig.matches_file(&f.join("src/index.ts"))); + + // On Windows, Src == src (case-insensitive) + assert!(tsconfig.matches_file(&f.join("Src/index.ts"))); +} diff --git a/src/tsconfig/file_matcher.rs b/src/tsconfig/file_matcher.rs new file mode 100644 index 00000000..ea07ea00 --- /dev/null +++ b/src/tsconfig/file_matcher.rs @@ -0,0 +1,300 @@ +//! File matching logic for tsconfig include/exclude/files patterns. +//! +//! Based on vite-tsconfig-paths implementation: +//! + +use std::path::{Path, PathBuf}; + +/// Matches files against tsconfig include/exclude/files patterns. +/// +/// Implements the matching logic from vite-tsconfig-paths which uses globrex +/// for pattern compilation. This implementation uses fast-glob instead. +/// +/// ## Matching Rules +/// +/// 1. **Files priority**: If a file is in the `files` array, it's included regardless of exclude patterns +/// 2. **Include matching**: File must match at least one include pattern +/// 3. **Exclude filtering**: File must NOT match any exclude pattern +/// +/// ## Default Values +/// +/// - Include: `["**/*"]` if not specified +/// - Exclude: `["node_modules", "bower_components", "jspm_packages"]` + outDir if specified +#[derive(Debug)] +pub struct TsconfigFileMatcher { + /// Explicit files (highest priority, overrides exclude) + files: Option>, + + /// Include patterns (defaults to `["**/*"]`) + include_patterns: Vec, + + /// Exclude patterns (defaults to node_modules, bower_components, jspm_packages) + exclude_patterns: Vec, + + /// Directory containing tsconfig.json + tsconfig_dir: PathBuf, +} + +impl TsconfigFileMatcher { + /// Create a matcher that matches nothing (for empty files + no include case). + /// + /// Per vite-tsconfig-paths: when `files` is explicitly empty AND `include` is + /// missing or empty, the tsconfig should not match any files. + #[must_use] + pub fn empty() -> Self { + Self { + files: None, + include_patterns: Vec::new(), + exclude_patterns: Vec::new(), + tsconfig_dir: PathBuf::new(), + } + } + + /// Create matcher from tsconfig fields. + /// + /// # Arguments + /// + /// * `files` - Explicit files array from tsconfig + /// * `include` - Include patterns from tsconfig + /// * `exclude` - Exclude patterns from tsconfig + /// * `out_dir` - CompilerOptions.outDir (automatically added to exclude) + /// * `tsconfig_dir` - Directory containing tsconfig.json + #[must_use] + pub fn new( + files: Option>, + include: Option>, + exclude: Option>, + out_dir: Option<&Path>, + tsconfig_dir: PathBuf, + ) -> Self { + // Default include: **/* (only if include not specified AND files not specified) + // If files is specified without include, don't default include + let include_patterns = include.unwrap_or_else(|| { + if files.is_some() { + Vec::new() // No default include when files is specified + } else { + vec!["**/*".to_string()] + } + }); + + // Start with default excludes + let mut exclude_patterns = vec![ + "node_modules".to_string(), + "bower_components".to_string(), + "jspm_packages".to_string(), + ]; + + // Merge user-specified excludes with defaults + if let Some(user_excludes) = exclude { + exclude_patterns.extend(user_excludes); + } + + // Add outDir to exclude if specified + if let Some(out_dir) = out_dir { + if let Some(out_dir_str) = out_dir.to_str() { + exclude_patterns.push(out_dir_str.to_string()); + } + } + + Self { + files: files.map(|f| Self::normalize_patterns(f, &tsconfig_dir)), + include_patterns: Self::normalize_patterns(include_patterns, &tsconfig_dir), + exclude_patterns: Self::normalize_patterns(exclude_patterns, &tsconfig_dir), + tsconfig_dir, + } + } + + /// Normalize patterns per vite-tsconfig-paths logic. + /// + /// Rules: + /// 1. Convert absolute paths to relative from tsconfig_dir + /// 2. Ensure patterns start with ./ + /// 3. Expand non-glob patterns: "foo" → `["./foo/**"]` + /// 4. File-like patterns: "foo.ts" → `["./foo.ts", "./foo.ts/**"]` + fn normalize_patterns(patterns: Vec, tsconfig_dir: &Path) -> Vec { + patterns + .into_iter() + .flat_map(|#[cfg_attr(not(target_os = "windows"), allow(unused_mut))] mut pattern| { + // On Windows, convert to lowercase for case-insensitive matching + #[cfg(target_os = "windows")] + { + pattern = pattern.to_lowercase(); + } + // Convert absolute to relative + #[allow(clippy::option_if_let_else)] // map_or would cause borrow checker issues + let pattern = if Path::new(&pattern).is_absolute() { + match Path::new(&pattern).strip_prefix(tsconfig_dir) { + Ok(rel) => rel.to_string_lossy().to_string(), + Err(_) => pattern, + } + } else { + pattern + }; + + // Ensure starts with ./ + let pattern = if pattern.starts_with("./") || pattern.starts_with("../") { + pattern + } else { + format!("./{pattern}") + }; + + // Handle non-glob patterns + let ends_with_glob = pattern + .split('/') + .next_back() + .is_some_and(|part| part.contains('*') || part.contains('?')); + + if ends_with_glob { + // Pattern already has wildcards, use as-is + vec![pattern] + } else { + // Non-glob pattern: expand to match directory + // Strip trailing slash before adding /** + let pattern_base = pattern.trim_end_matches('/'); + let mut patterns = Vec::new(); + + // If looks like a file (has extension after last slash), also match exact + if pattern + .rsplit('/') + .next() + .is_some_and(|part| part.contains('.') && part != "." && part != "..") + { + patterns.push(pattern.clone()); + } + + patterns.push(format!("{pattern_base}/**")); + patterns + } + }) + .collect() + } + + /// Test if a file matches this tsconfig's patterns. + /// + /// # Returns + /// + /// `true` if the file matches, `false` otherwise. + /// + /// # Algorithm + /// + /// 1. Normalize the file path (relative to tsconfig_dir with ./ prefix) + /// 2. Check files array first (highest priority, overrides exclude) + /// 3. Check if path matches any include pattern + /// 4. Check if path matches any exclude pattern + #[must_use] + pub fn matches(&self, file_path: &Path) -> bool { + // Normalize path for matching + #[allow(clippy::manual_let_else)] // Match is clearer here + #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] + let mut normalized = match self.normalize_path(file_path) { + Some(p) => p, + None => return false, // Path can't be normalized + }; + + // On Windows, convert to lowercase for case-insensitive matching + #[cfg(target_os = "windows")] + { + normalized = normalized.to_lowercase(); + } + + // 1. Check files array first (absolute priority) + if let Some(files) = &self.files { + for file in files { + if normalized == *file { + return true; // Files overrides exclude + } + } + // If files specified but no match, continue to include/exclude + // (unless include is empty) + if self.include_patterns.is_empty() { + return false; + } + } else if self.include_patterns.is_empty() { + // No files array and empty patterns (match nothing case) + return false; + } + + // 2. Test against include patterns + let mut included = false; + for pattern in &self.include_patterns { + if fast_glob::glob_match(pattern, &normalized) { + included = true; + break; + } + } + + if !included { + return false; + } + + // 3. Test against exclude patterns + for pattern in &self.exclude_patterns { + if fast_glob::glob_match(pattern, &normalized) { + return false; + } + } + + true + } + + /// Normalize file path for matching. + /// + /// Rules (from vite-tsconfig-paths): + /// 1. Remove query parameters (e.g., `?inline`) + /// 2. Convert to absolute if relative + /// 3. Make relative to tsconfig_dir + /// 4. Use forward slashes for cross-platform consistency + /// 5. Prepend ./ if needed + /// + /// # Returns + /// + /// `None` if path is outside tsconfig_dir or can't be normalized. + fn normalize_path(&self, file_path: &Path) -> Option { + // Remove query parameters + let path_str = file_path.to_str()?; + let path_str = path_str.split('?').next()?; + let file_path = Path::new(path_str); + + // Make absolute if relative + let absolute = if file_path.is_absolute() { + file_path.to_path_buf() + } else { + std::env::current_dir().ok()?.join(file_path) + }; + + // Make relative to tsconfig directory + let relative = absolute.strip_prefix(&self.tsconfig_dir).ok()?; + + // Convert to string with forward slashes + let mut normalized = relative.to_str()?.replace('\\', "/"); + + // Ensure starts with ./ + if !normalized.starts_with("./") && !normalized.starts_with("../") { + normalized = format!("./{normalized}"); + } + + Some(normalized) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_matcher() { + let matcher = TsconfigFileMatcher::empty(); + let path = PathBuf::from("index.ts"); + assert!(!matcher.matches(&path)); + } + + #[test] + fn test_normalize_patterns() { + let tsconfig_dir = PathBuf::from("/project"); + let patterns = vec!["src/**/*.ts".to_string(), "lib".to_string(), "file.ts".to_string()]; + + let normalized = TsconfigFileMatcher::normalize_patterns(patterns, &tsconfig_dir); + + assert_eq!(normalized, vec!["./src/**/*.ts", "./lib/**", "./file.ts", "./file.ts/**",]); + } +} diff --git a/src/tsconfig.rs b/src/tsconfig/mod.rs similarity index 88% rename from src/tsconfig.rs rename to src/tsconfig/mod.rs index 9fd2ab0e..bda508bd 100644 --- a/src/tsconfig.rs +++ b/src/tsconfig/mod.rs @@ -1,3 +1,7 @@ +mod file_matcher; + +pub use file_matcher::TsconfigFileMatcher; + use std::{ fmt::Debug, hash::BuildHasherDefault, @@ -45,6 +49,10 @@ pub struct TsConfig { /// Bubbled up project references with a reference to their tsconfig. #[serde(default)] pub references: Vec, + + /// File matcher for include/exclude/files (built during build()) + #[serde(skip)] + file_matcher: Option, } impl TsConfig { @@ -131,6 +139,33 @@ impl TsConfig { self.references.iter_mut() } + /// Tests whether a file matches this tsconfig's include/exclude/files patterns. + /// + /// Returns `false` if: + /// - The file is outside the tsconfig directory + /// - The file doesn't match any include pattern + /// - The file matches an exclude pattern + /// - The tsconfig has empty files and no include + /// + /// # Examples + /// + /// ```no_run + /// use std::path::Path; + /// use oxc_resolver::{Resolver, ResolveOptions, TsconfigDiscovery, TsconfigOptions, TsconfigReferences}; + /// + /// let resolver = Resolver::default(); + /// let tsconfig_dir = Path::new("/project"); + /// let tsconfig = resolver.resolve_tsconfig(tsconfig_dir).unwrap(); + /// + /// // Check if file is included in tsconfig + /// assert!(tsconfig.matches_file(&tsconfig_dir.join("src/index.ts"))); + /// assert!(!tsconfig.matches_file(&tsconfig_dir.join("node_modules/foo.ts"))); + /// ``` + #[must_use] + pub fn matches_file(&self, path: &Path) -> bool { + self.file_matcher.as_ref().is_some_and(|matcher| matcher.matches(path)) + } + /// Returns the base path from which to resolve aliases. /// /// The base path can be configured by the user as part of the @@ -297,13 +332,36 @@ impl TsConfig { /// * `baseUrl` to absolute path #[must_use] pub(crate) fn build(mut self) -> Self { + let config_dir = self.directory().to_path_buf(); + + // SPECIAL CASE: empty files + no include = skip file matching + let is_empty_case = self.files.as_ref().is_some_and(std::vec::Vec::is_empty) + && self.include.as_ref().is_some_and(std::vec::Vec::is_empty); + + if is_empty_case { + self.file_matcher = Some(TsconfigFileMatcher::empty()); + // Only the root tsconfig requires paths resolution. + if !self.root() { + return self; + } + } + // Only the root tsconfig requires paths resolution. if !self.root() { + // But still build file matcher for non-root tsconfigs (if not already set) + if !is_empty_case { + let out_dir = self.compiler_options.out_dir.as_deref(); + self.file_matcher = Some(TsconfigFileMatcher::new( + self.files.clone(), + self.include.clone(), + self.exclude.clone(), + out_dir, + config_dir, + )); + } return self; } - let config_dir = self.directory().to_path_buf(); - if let Some(base_url) = self.compiler_options().base_url() { // Substitute template variable in `tsconfig.compilerOptions.baseUrl`. let base_url = base_url.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE).map_or_else( @@ -332,6 +390,33 @@ impl TsConfig { } } + // Substitute template variable in include patterns + if let Some(include) = self.include.as_mut() { + for pattern in include { + Self::substitute_template_variable(&config_dir, pattern); + } + } + + // Substitute template variable in exclude patterns + if let Some(exclude) = self.exclude.as_mut() { + for pattern in exclude { + Self::substitute_template_variable(&config_dir, pattern); + } + } + + // Build file matcher for root tsconfig (after template substitution) + // Only skip if explicitly empty case + if !is_empty_case { + let out_dir = self.compiler_options.out_dir.as_deref(); + self.file_matcher = Some(TsconfigFileMatcher::new( + self.files.clone(), + self.include.clone(), + self.exclude.clone(), + out_dir, + config_dir, + )); + } + self } @@ -484,6 +569,9 @@ pub struct CompilerOptions { /// pub allow_js: Option, + + /// + pub out_dir: Option, } impl CompilerOptions {