diff --git a/packages/js/src/plugins/typescript/plugin.spec.ts b/packages/js/src/plugins/typescript/plugin.spec.ts index 2e1b0d1ef5..3a49e7d38d 100644 --- a/packages/js/src/plugins/typescript/plugin.spec.ts +++ b/packages/js/src/plugins/typescript/plugin.spec.ts @@ -795,6 +795,307 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => { `); }); + it('should normalize and add directories in `include` with the ts extensions', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.json': JSON.stringify({ + include: ['src'], + // set this to keep outputs smaller + compilerOptions: { outDir: 'dist' }, + }), + 'libs/my-lib/package.json': `{}`, + }); + + expect(await invokeCreateNodesOnMatchingFiles(context, {})) + .toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "typecheck": { + "cache": true, + "command": "tsc --build --emitDeclarationOnly", + "dependsOn": [ + "^typecheck", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Runs type-checking for the project.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + + it('should normalize and add directories in `include` with the ts and js extensions when `allowJs` is true', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.json': JSON.stringify({ + include: ['src'], + // set this to keep outputs smaller + compilerOptions: { outDir: 'dist', allowJs: true }, + }), + 'libs/my-lib/package.json': `{}`, + }); + + expect(await invokeCreateNodesOnMatchingFiles(context, {})) + .toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "typecheck": { + "cache": true, + "command": "tsc --build --emitDeclarationOnly", + "dependsOn": [ + "^typecheck", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.js", + "{projectRoot}/src/**/*.jsx", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.cjs", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "{projectRoot}/src/**/*.mjs", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Runs type-checking for the project.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + + it('should normalize and add directories in `include` with the ts and json extensions when `resolveJsonModule` is true', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.json': JSON.stringify({ + include: ['src'], + // set this to keep outputs smaller + compilerOptions: { + outDir: 'dist', + resolveJsonModule: true, + }, + }), + 'libs/my-lib/package.json': `{}`, + }); + + expect(await invokeCreateNodesOnMatchingFiles(context, {})) + .toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "typecheck": { + "cache": true, + "command": "tsc --build --emitDeclarationOnly", + "dependsOn": [ + "^typecheck", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "{projectRoot}/src/**/*.json", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Runs type-checking for the project.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + + it('should normalize and add directories in `include` with the ts, js and json extensions when `allowJs` and `resolveJsonModule` are true', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.json': JSON.stringify({ + include: ['src'], + // set this to keep outputs smaller + compilerOptions: { + outDir: 'dist', + allowJs: true, + resolveJsonModule: true, + }, + }), + 'libs/my-lib/package.json': `{}`, + }); + + expect(await invokeCreateNodesOnMatchingFiles(context, {})) + .toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "typecheck": { + "cache": true, + "command": "tsc --build --emitDeclarationOnly", + "dependsOn": [ + "^typecheck", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.js", + "{projectRoot}/src/**/*.jsx", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.cjs", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "{projectRoot}/src/**/*.mjs", + "{projectRoot}/src/**/*.json", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Runs type-checking for the project.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + it('should add extended config files', async () => { await applyFilesToTempFsAndContext(tempFs, context, { 'tsconfig.base.json': JSON.stringify({ @@ -1107,6 +1408,103 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => { `); }); + it('should normalize and add directories in `include` from internal project references', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.json': JSON.stringify({ + files: [], + include: [], + references: [ + { path: './tsconfig.lib.json' }, + { path: './tsconfig.spec.json' }, + ], + // set this to keep outputs smaller + compilerOptions: { outDir: 'dist' }, + }), + 'libs/my-lib/tsconfig.lib.json': JSON.stringify({ + include: ['src/**/*.ts'], + exclude: ['tests'], // should be ignored because another referenced internal project includes this same pattern + // set this to keep outputs smaller + compilerOptions: { outDir: 'dist' }, + }), + 'libs/my-lib/tsconfig.spec.json': JSON.stringify({ + include: ['tests'], // directory pattern that should be normalized + references: [{ path: './tsconfig.lib.json' }], + compilerOptions: { + outDir: 'dist', // set this to keep outputs smaller + allowJs: true, // should result in including js extensions in the normalized include paths + }, + }), + 'libs/my-lib/package.json': `{}`, + }); + + expect(await invokeCreateNodesOnMatchingFiles(context, {})) + .toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "typecheck": { + "cache": true, + "command": "tsc --build --emitDeclarationOnly", + "dependsOn": [ + "^typecheck", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.json", + "{projectRoot}/tsconfig.lib.json", + "{projectRoot}/tsconfig.spec.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/tests/**/*.ts", + "{projectRoot}/tests/**/*.tsx", + "{projectRoot}/tests/**/*.d.ts", + "{projectRoot}/tests/**/*.js", + "{projectRoot}/tests/**/*.jsx", + "{projectRoot}/tests/**/*.cts", + "{projectRoot}/tests/**/*.d.cts", + "{projectRoot}/tests/**/*.cjs", + "{projectRoot}/tests/**/*.mts", + "{projectRoot}/tests/**/*.d.mts", + "{projectRoot}/tests/**/*.mjs", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Runs type-checking for the project.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + it('should only add exclude paths that are not part of other tsconfig files include paths', async () => { // exact match await applyFilesToTempFsAndContext(tempFs, context, { @@ -2845,6 +3243,320 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => { `); }); + it('should normalize and add directories in `include` with the ts extensions', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.lib.json': JSON.stringify({ + compilerOptions: { outDir: 'dist' }, + include: ['src'], + }), + 'libs/my-lib/tsconfig.json': `{}`, + 'libs/my-lib/package.json': `{"main": "dist/index.js"}`, + }); + + expect( + await invokeCreateNodesOnMatchingFiles(context, { + typecheck: false, + build: true, + }) + ).toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "build": { + "cache": true, + "command": "tsc --build tsconfig.lib.json", + "dependsOn": [ + "^build", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.lib.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Builds the project with \`tsc\`.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + + it('should normalize and add directories in `include` with the ts and js extensions when `allowJs` is true', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.lib.json': JSON.stringify({ + compilerOptions: { outDir: 'dist', allowJs: true }, + include: ['src'], + }), + 'libs/my-lib/tsconfig.json': `{}`, + 'libs/my-lib/package.json': `{"main": "dist/index.js"}`, + }); + + expect( + await invokeCreateNodesOnMatchingFiles(context, { + typecheck: false, + build: true, + }) + ).toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "build": { + "cache": true, + "command": "tsc --build tsconfig.lib.json", + "dependsOn": [ + "^build", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.lib.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.js", + "{projectRoot}/src/**/*.jsx", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.cjs", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "{projectRoot}/src/**/*.mjs", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Builds the project with \`tsc\`.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + + it('should normalize and add directories in `include` with the ts and json extensions when `resolveJsonModule` is true', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.lib.json': JSON.stringify({ + compilerOptions: { outDir: 'dist', resolveJsonModule: true }, + include: ['src'], + }), + 'libs/my-lib/tsconfig.json': `{}`, + 'libs/my-lib/package.json': `{"main": "dist/index.js"}`, + }); + + expect( + await invokeCreateNodesOnMatchingFiles(context, { + typecheck: false, + build: true, + }) + ).toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "build": { + "cache": true, + "command": "tsc --build tsconfig.lib.json", + "dependsOn": [ + "^build", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.lib.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "{projectRoot}/src/**/*.json", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Builds the project with \`tsc\`.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + + it('should normalize and add directories in `include` with the ts, js and json extensions when `allowJs` and `resolveJsonModule` are true', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.lib.json': JSON.stringify({ + compilerOptions: { + outDir: 'dist', + allowJs: true, + resolveJsonModule: true, + }, + include: ['src'], + }), + 'libs/my-lib/tsconfig.json': `{}`, + 'libs/my-lib/package.json': `{"main": "dist/index.js"}`, + }); + + expect( + await invokeCreateNodesOnMatchingFiles(context, { + typecheck: false, + build: true, + }) + ).toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "build": { + "cache": true, + "command": "tsc --build tsconfig.lib.json", + "dependsOn": [ + "^build", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.lib.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.tsx", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.js", + "{projectRoot}/src/**/*.jsx", + "{projectRoot}/src/**/*.cts", + "{projectRoot}/src/**/*.d.cts", + "{projectRoot}/src/**/*.cjs", + "{projectRoot}/src/**/*.mts", + "{projectRoot}/src/**/*.d.mts", + "{projectRoot}/src/**/*.mjs", + "{projectRoot}/src/**/*.json", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Builds the project with \`tsc\`.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + it('should be able to extended config files', async () => { await applyFilesToTempFsAndContext(tempFs, context, { 'tsconfig.base.json': JSON.stringify({ @@ -3100,6 +3812,94 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => { `); }); + it('should normalize and add directories in `include` from internal project references', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.json': '{}', + 'libs/my-lib/tsconfig.lib.json': JSON.stringify({ + compilerOptions: { outDir: 'dist' }, + include: ['src/**/*.ts'], + exclude: ['src/**/foo.ts'], // should be ignored because a referenced internal project includes this same pattern + references: [{ path: './tsconfig.other.json' }], + }), + 'libs/my-lib/tsconfig.other.json': JSON.stringify({ + include: ['other', 'src/**/foo.ts'], + compilerOptions: { + outDir: 'dist', // set this to keep outputs smaller + resolveJsonModule: true, // should result in including json extensions in the normalized include paths + }, + }), + 'libs/my-lib/package.json': `{"main": "dist/index.js"}`, // Should be defined so that the project is considered buildable + }); + + expect( + await invokeCreateNodesOnMatchingFiles(context, { + typecheck: false, + build: true, + }) + ).toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "build": { + "cache": true, + "command": "tsc --build tsconfig.lib.json", + "dependsOn": [ + "^build", + ], + "inputs": [ + "{projectRoot}/package.json", + "{projectRoot}/tsconfig.lib.json", + "{projectRoot}/tsconfig.other.json", + "{projectRoot}/src/**/*.ts", + "{projectRoot}/other/**/*.ts", + "{projectRoot}/other/**/*.tsx", + "{projectRoot}/other/**/*.d.ts", + "{projectRoot}/other/**/*.cts", + "{projectRoot}/other/**/*.d.cts", + "{projectRoot}/other/**/*.mts", + "{projectRoot}/other/**/*.d.mts", + "{projectRoot}/other/**/*.json", + "{projectRoot}/src/**/foo.ts", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Builds the project with \`tsc\`.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/dist", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + it('should only add exclude paths that are not part of other tsconfig files include paths', async () => { // exact match await applyFilesToTempFsAndContext(tempFs, context, { diff --git a/packages/js/src/plugins/typescript/plugin.ts b/packages/js/src/plugins/typescript/plugin.ts index 1aa26de95a..c54d921a9f 100644 --- a/packages/js/src/plugins/typescript/plugin.ts +++ b/packages/js/src/plugins/typescript/plugin.ts @@ -1,5 +1,4 @@ import { - CreateNodesContextV2, createNodesFromFiles, detectPackageManager, getPackageManagerCommand, @@ -11,18 +10,17 @@ import { type CreateDependencies, type CreateNodes, type CreateNodesContext, + type CreateNodesContextV2, type CreateNodesV2, type NxJsonConfiguration, type ProjectConfiguration, type TargetConfiguration, } from '@nx/devkit'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; -import picomatch = require('picomatch'); import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { basename, dirname, - extname, join, normalize, relative, @@ -31,10 +29,11 @@ import { } from 'node:path'; import * as posix from 'node:path/posix'; import { hashArray, hashFile, hashObject } from 'nx/src/hasher/file-hasher'; +import picomatch = require('picomatch'); // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { getLockFileName } from 'nx/src/plugins/js/lock-file/lock-file'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; -import type { ParsedCommandLine, System } from 'typescript'; +import type { Extension, ParsedCommandLine, System } from 'typescript'; import { addBuildAndWatchDepsTargets, isValidPackageJsonBuildConfig, @@ -83,6 +82,7 @@ type TsconfigCacheData = { extendedFilesHash: string; }; +let ts: typeof import('typescript'); const pmc = getPackageManagerCommand(); let tsConfigCache: Record; @@ -320,6 +320,9 @@ async function getConfigFileHash( lockFileHash, optionsHash, ...(packageJson ? [hashObject(packageJson)] : []), + // change this to bust the cache when making changes that would yield + // different results for the same hash + hashObject({ bust: 1 }), ]); } @@ -534,12 +537,65 @@ function getInputs( ...Object.entries(internalProjectReferences), ]; const absoluteProjectRoot = join(workspaceRoot, projectRoot); + + if (!ts) { + ts = require('typescript'); + } + // https://github.com/microsoft/TypeScript/blob/19b777260b26aac5707b1efd34202054164d4a9d/src/compiler/utilities.ts#L9869 + const supportedTSExtensions: readonly Extension[] = [ + ts.Extension.Ts, + ts.Extension.Tsx, + ts.Extension.Dts, + ts.Extension.Cts, + ts.Extension.Dcts, + ts.Extension.Mts, + ts.Extension.Dmts, + ]; + // https://github.com/microsoft/TypeScript/blob/19b777260b26aac5707b1efd34202054164d4a9d/src/compiler/utilities.ts#L9878 + const allSupportedExtensions: readonly Extension[] = [ + ts.Extension.Ts, + ts.Extension.Tsx, + ts.Extension.Dts, + ts.Extension.Js, + ts.Extension.Jsx, + ts.Extension.Cts, + ts.Extension.Dcts, + ts.Extension.Cjs, + ts.Extension.Mts, + ts.Extension.Dmts, + ts.Extension.Mjs, + ]; + + const normalizeInput = ( + input: string, + config: ParsedTsconfigData + ): string[] => { + const extensions = config.options.allowJs + ? [...allSupportedExtensions] + : [...supportedTSExtensions]; + if (config.options.resolveJsonModule) { + extensions.push(ts.Extension.Json); + } + + const segments = input.split('/'); + // An "includes" path "foo" is implicitly a glob "foo/**/*" if its last + // segment has no extension, and does not contain any glob characters + // itself. + // https://github.com/microsoft/TypeScript/blob/19b777260b26aac5707b1efd34202054164d4a9d/src/compiler/utilities.ts#L9577-L9585 + if (!/[.*?]/.test(segments.at(-1))) { + return extensions.map((ext) => `${segments.join('/')}/**/*${ext}`); + } + + return [input]; + }; + projectTsConfigFiles.forEach(([configPath, config]) => { configFiles.add(configPath); const offset = relative(absoluteProjectRoot, dirname(configPath)); - (config.raw?.include ?? []).forEach((p: string) => - includePaths.add(join(offset, p)) - ); + (config.raw?.include ?? []).forEach((p: string) => { + const normalized = normalizeInput(join(offset, p), config); + normalized.forEach((input) => includePaths.add(input)); + }); if (config.raw?.exclude) { /** @@ -1071,7 +1127,6 @@ function getExtendedFilesHash( return hashes.join('|'); } -let ts: typeof import('typescript'); function readTsConfig( tsConfigPath: string, workspaceRoot: string