diff --git a/package.json b/package.json index 3505978..f749650 100644 --- a/package.json +++ b/package.json @@ -68,12 +68,17 @@ "optional": true } }, + "dependencies": { + "globby": "^11.1.0", + "is-glob": "^4.0.3" + }, "devDependencies": { "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.24.2", "@ota-meshi/eslint-plugin": "^0.15.0", "@types/chai": "^4.3.0", "@types/eslint": "^8.0.0", + "@types/is-glob": "^4.0.4", "@types/mocha": "^10.0.0", "@types/node": "^20.0.0", "@types/semver": "^7.3.9", @@ -96,7 +101,6 @@ "eslint-plugin-svelte": "^2.11.0", "eslint-plugin-vue": "^9.6.0", "eslint-plugin-yml": "^1.2.0", - "glob": "^10.3.10", "mocha": "^10.0.0", "mocha-chai-jest-snapshot": "^1.1.3", "nyc": "^15.1.0", diff --git a/src/index.ts b/src/index.ts index 683396b..bdcb6e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import type { ProgramOptions } from "./ts"; import { TSServiceManager } from "./ts"; import * as tsEslintParser from "@typescript-eslint/parser"; import { getProjectConfigFiles } from "./utils/get-project-config-files"; +import { resolveProjectList } from "./utils/resolve-project-list"; export * as meta from "./meta"; export { name } from "./meta"; @@ -44,7 +45,20 @@ function* iterateOptions(options: ParserOptions): Iterable { "Specify `parserOptions.project`. Otherwise there is no point in using this parser.", ); } - for (const project of getProjectConfigFiles(options)) { + const tsconfigRootDir = + typeof options.tsconfigRootDir === "string" + ? options.tsconfigRootDir + : process.cwd(); + + for (const project of resolveProjectList({ + project: getProjectConfigFiles({ + project: options.project, + tsconfigRootDir, + filePath: options.filePath, + }), + projectFolderIgnoreList: options.projectFolderIgnoreList, + tsconfigRootDir, + })) { yield { project, filePath: options.filePath, diff --git a/src/utils/get-project-config-files.ts b/src/utils/get-project-config-files.ts index 28b08bc..e4e87d7 100644 --- a/src/utils/get-project-config-files.ts +++ b/src/utils/get-project-config-files.ts @@ -1,26 +1,17 @@ -import type { ParserOptions } from "@typescript-eslint/parser"; import fs from "fs"; -import { glob } from "glob"; import path from "path"; -function syncWithGlob(pattern: string, cwd: string): string[] { - return glob - .sync(pattern, { cwd }) - .map((filePath) => path.resolve(cwd, filePath)); -} - -export function getProjectConfigFiles(options: ParserOptions): string[] { - const tsconfigRootDir = - typeof options.tsconfigRootDir === "string" - ? options.tsconfigRootDir - : process.cwd(); - +export function getProjectConfigFiles( + options: Readonly<{ + project: string[] | string | true | null | undefined; + tsconfigRootDir: string; + filePath?: string; + }>, +): string[] { if (options.project !== true) { return Array.isArray(options.project) - ? options.project.flatMap((projectPattern: string) => - syncWithGlob(projectPattern, tsconfigRootDir), - ) - : syncWithGlob(options.project!, tsconfigRootDir); + ? options.project + : [options.project!]; } let directory = path.dirname(options.filePath!); @@ -34,9 +25,12 @@ export function getProjectConfigFiles(options: ParserOptions): string[] { directory = path.dirname(directory); checkedDirectories.push(directory); - } while (directory.length > 1 && directory.length >= tsconfigRootDir.length); + } while ( + directory.length > 1 && + directory.length >= options.tsconfigRootDir.length + ); throw new Error( - `project was set to \`true\` but couldn't find any tsconfig.json relative to '${options.filePath}' within '${tsconfigRootDir}'.`, + `project was set to \`true\` but couldn't find any tsconfig.json relative to '${options.filePath}' within '${options.tsconfigRootDir}'.`, ); } diff --git a/src/utils/resolve-project-list.ts b/src/utils/resolve-project-list.ts new file mode 100644 index 0000000..988a6f7 --- /dev/null +++ b/src/utils/resolve-project-list.ts @@ -0,0 +1,87 @@ +import { sync as globSync } from "globby"; +import isGlob from "is-glob"; +import path from "path"; +import * as ts from "typescript"; + +/** + * Normalizes, sanitizes, resolves and filters the provided project paths + */ +export function resolveProjectList( + options: Readonly<{ + project: string[] | null; + projectFolderIgnoreList: (RegExp | string)[] | undefined; + tsconfigRootDir: string; + }>, +): readonly string[] { + const sanitizedProjects: string[] = []; + + // Normalize and sanitize the project paths + if (options.project != null) { + for (const project of options.project) { + if (typeof project === "string") { + sanitizedProjects.push(project); + } + } + } + + if (sanitizedProjects.length === 0) { + return []; + } + + const projectFolderIgnoreList = ( + options.projectFolderIgnoreList ?? ["**/node_modules/**"] + ) + .reduce((acc, folder) => { + if (typeof folder === "string") { + acc.push(folder); + } + return acc; + }, []) + // prefix with a ! for not match glob + .map((folder) => (folder.startsWith("!") ? folder : `!${folder}`)); + + // Transform glob patterns into paths + const nonGlobProjects = sanitizedProjects.filter( + (project) => !isGlob(project), + ); + const globProjects = sanitizedProjects.filter((project) => isGlob(project)); + + const uniqueCanonicalProjectPaths = new Set( + nonGlobProjects + .concat( + globProjects.length === 0 + ? [] + : globSync([...globProjects, ...projectFolderIgnoreList], { + cwd: options.tsconfigRootDir, + }), + ) + .map((project) => + getCanonicalFileName( + ensureAbsolutePath(project, options.tsconfigRootDir), + ), + ), + ); + + return Array.from(uniqueCanonicalProjectPaths); +} + +// typescript doesn't provide a ts.sys implementation for browser environments +const useCaseSensitiveFileNames = + ts.sys !== undefined ? ts.sys.useCaseSensitiveFileNames : true; +const correctPathCasing = useCaseSensitiveFileNames + ? (filePath: string): string => filePath + : (filePath: string): string => filePath.toLowerCase(); + +function getCanonicalFileName(filePath: string): string { + let normalized = path.normalize(filePath); + if (normalized.endsWith(path.sep)) { + normalized = normalized.slice(0, -1); + } + return correctPathCasing(normalized); +} + +function ensureAbsolutePath(p: string, tsconfigRootDir: string): string { + return path.isAbsolute(p) + ? p + : path.join(tsconfigRootDir || process.cwd(), p); +}