UNPKG

typescript-eslint-parser-for-extra-files

Version:

An experimental ESLint custom parser for Vue, Svelte, and Astro for use with TypeScript. It provides type information in combination with each framework's ESLint custom parser.

384 lines (371 loc) 15.8 kB
import { createRequire } from "module"; import path from "path"; import * as ts$1 from "typescript"; import ts from "typescript"; import * as tsEslintParser from "@typescript-eslint/parser"; import fs from "fs"; import { globSync, isDynamicPattern } from "tinyglobby"; //#region rolldown:runtime var __require = /* @__PURE__ */ createRequire(import.meta.url); //#endregion //#region src/transform/vue.ts function transformForVue(code, context) { if (context.current) return code; const compiler = __require("vue/compiler-sfc"); const result = compiler.parse(code); const compiled = compiler.compileScript(result.descriptor, { id: "id", reactivityTransform: true }); return compiled.content; } //#endregion //#region src/transform/svelte.ts function transformForSvelte(code, context) { if (context.current) return code; const svelte2tsx = __require("svelte2tsx"); const result = svelte2tsx.svelte2tsx(code, { filename: context.filePath }); return `/// <reference types="svelte2tsx/svelte-shims-v4" /> /// <reference types="svelte2tsx/svelte-shims" /> ${result.code}`; } //#endregion //#region src/transform/astro.ts function transformForAstro(code, context) { if (context.current) return code; const compiler = __require("astrojs-compiler-sync"); const result = compiler.convertToTSX(code, { sourcefile: context.filePath }); return result.code; } //#endregion //#region src/transform/index.ts function transformExtraFile(code, context) { const ext = path.extname(context.filePath); const transform = ext === ".vue" ? transformForVue : ext === ".svelte" ? transformForSvelte : ext === ".astro" ? transformForAstro : () => code; try { return transform(code, context); } catch {} return code; } //#endregion //#region src/ts.ts var TSServiceManager = class { constructor() { this.tsServices = new Map(); } getProgram(code, options) { const tsconfigPath = options.project; const extraFileExtensions = [...new Set(options.extraFileExtensions)]; let serviceList = this.tsServices.get(tsconfigPath); if (!serviceList) { serviceList = []; this.tsServices.set(tsconfigPath, serviceList); } let service = serviceList.find((service$1) => extraFileExtensions.every((ext) => service$1.extraFileExtensions.includes(ext))); if (!service) { service = new TSService(tsconfigPath, extraFileExtensions); serviceList.unshift(service); } return service.getProgram(code, options.filePath); } }; var TSService = class { constructor(tsconfigPath, extraFileExtensions) { this.patchedHostSet = new WeakSet(); this.currTarget = { code: "", filePath: "", dirMap: new Map() }; this.fileWatchCallbacks = new Map(); this.tsconfigPath = tsconfigPath; this.extraFileExtensions = extraFileExtensions; this.watch = this.createWatch(tsconfigPath, extraFileExtensions); } getProgram(code, filePath) { const normalized = normalizeFileName(filePath); const lastTarget = this.currTarget; const dirMap = new Map(); let childPath = normalized; for (const dirName of iterateDirs(normalized)) { dirMap.set(dirName, { path: childPath, name: path.basename(childPath) }); childPath = dirName; } this.currTarget = { code, filePath: normalized, dirMap }; for (const { filePath: targetPath } of [this.currTarget, lastTarget]) { var _this$fileWatchCallba2; if (!targetPath) continue; if (!ts.sys.fileExists(targetPath)) { var _this$fileWatchCallba; (_this$fileWatchCallba = this.fileWatchCallbacks.get(normalizeFileName(this.tsconfigPath))) === null || _this$fileWatchCallba === void 0 || _this$fileWatchCallba.update(); } (_this$fileWatchCallba2 = this.fileWatchCallbacks.get(normalizeFileName(targetPath))) === null || _this$fileWatchCallba2 === void 0 || _this$fileWatchCallba2.update(); } const program = this.watch.getProgram().getProgram(); program.getTypeChecker(); return program; } createWatch(tsconfigPath, extraFileExtensions) { const createAbstractBuilder = (...args) => { const [rootNames, options, argHost, oldProgram, configFileParsingDiagnostics, projectReferences] = args; const host = argHost; if (!this.patchedHostSet.has(host)) { this.patchedHostSet.add(host); const getTargetSourceFile = (fileName, languageVersionOrOptions) => { if (this.currTarget.filePath === normalizeFileName(fileName) && isExtra(fileName, extraFileExtensions)) return this.currTarget.sourceFile ??= ts.createSourceFile(this.currTarget.filePath, this.currTarget.code, languageVersionOrOptions, true, ts.ScriptKind.TSX); return null; }; const original$1 = { getSourceFile: host.getSourceFile, getSourceFileByPath: host.getSourceFileByPath }; host.getSourceFile = (fileName, languageVersionOrOptions, ...args$1) => { const originalSourceFile = original$1.getSourceFile.call(host, fileName, languageVersionOrOptions, ...args$1); return getTargetSourceFile(fileName, languageVersionOrOptions) ?? originalSourceFile; }; host.getSourceFileByPath = (fileName, path$1, languageVersionOrOptions, ...args$1) => { const originalSourceFile = original$1.getSourceFileByPath.call(host, fileName, path$1, languageVersionOrOptions, ...args$1); return getTargetSourceFile(fileName, languageVersionOrOptions) ?? originalSourceFile; }; } return ts.createAbstractBuilder(rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences); }; const watchCompilerHost = ts.createWatchCompilerHost(tsconfigPath, { noEmit: true, jsx: ts.JsxEmit.Preserve, allowNonTsExtensions: true }, ts.sys, createAbstractBuilder, (diagnostic) => { throw new Error(formatDiagnostics([diagnostic])); }, () => {}, void 0, extraFileExtensions.map((extension) => ({ extension, isMixedContent: true, scriptKind: ts.ScriptKind.Deferred }))); const original = { readFile: watchCompilerHost.readFile, fileExists: watchCompilerHost.fileExists, readDirectory: watchCompilerHost.readDirectory, directoryExists: watchCompilerHost.directoryExists, getDirectories: watchCompilerHost.getDirectories }; watchCompilerHost.getDirectories = (dirName, ...args) => { var _this$currTarget$dirM; const result = distinctArray(...original.getDirectories.call(watchCompilerHost, dirName, ...args), (_this$currTarget$dirM = this.currTarget.dirMap.get(normalizeFileName(dirName))) === null || _this$currTarget$dirM === void 0 ? void 0 : _this$currTarget$dirM.name); return result; }; watchCompilerHost.directoryExists = (dirName, ...args) => { return original.directoryExists.call(watchCompilerHost, dirName, ...args) || this.currTarget.dirMap.has(normalizeFileName(dirName)); }; watchCompilerHost.readDirectory = (dirName, ...args) => { let results = original.readDirectory.call(watchCompilerHost, dirName, ...args); const file = this.currTarget.dirMap.get(normalizeFileName(dirName)); if (file) if (file.path === this.currTarget.filePath) results.push(file.path); else results = results.filter((f) => file.path !== f && file.name !== f); return distinctArray(...results); }; watchCompilerHost.readFile = (fileName, ...args) => { const realFileName = getRealFileNameIfExist(fileName); if (realFileName == null) return void 0; if (this.currTarget.filePath === realFileName) return transformExtraFile(this.currTarget.code, { filePath: realFileName, current: true }); const code = original.readFile.call(watchCompilerHost, realFileName, ...args); if (!code) return code; return transformExtraFile(code, { filePath: realFileName, current: false }); }; watchCompilerHost.fileExists = (fileName) => { return getRealFileNameIfExist(fileName) != null; }; const getRealFileNameIfExist = (fileName) => { const normalizedFileName = normalizeFileName(fileName); if (this.currTarget.dirMap.has(normalizedFileName)) return null; if (this.currTarget.filePath === normalizedFileName) return normalizedFileName; const exists = original.fileExists.call(watchCompilerHost, normalizedFileName); if (exists) return normalizedFileName; if (isVirtualTSX(normalizedFileName, extraFileExtensions)) { const real = normalizedFileName.slice(0, -4); for (const dts of toExtraDtsFileNames(real, extraFileExtensions)) if (original.fileExists.call(watchCompilerHost, dts)) return null; if (original.fileExists.call(watchCompilerHost, real)) return real; } return null; }; watchCompilerHost.watchFile = (fileName, callback) => { const normalized = normalizeFileName(fileName); this.fileWatchCallbacks.set(normalized, { update: () => callback(fileName, ts.FileWatcherEventKind.Changed) }); return { close: () => { this.fileWatchCallbacks.delete(normalized); } }; }; watchCompilerHost.watchDirectory = () => { return { close: () => {} }; }; /** * It heavily references typescript-eslint. * @see https://github.com/typescript-eslint/typescript-eslint/blob/84e316be33dac5302bd0367c4d1960bef40c484d/packages/typescript-estree/src/create-program/createWatchProgram.ts#L297-L309 */ watchCompilerHost.afterProgramCreate = (program) => { const originalDiagnostics = program.getConfigFileParsingDiagnostics(); const configFileDiagnostics = originalDiagnostics.filter((diag) => diag.category === ts.DiagnosticCategory.Error && diag.code !== 18003); if (configFileDiagnostics.length > 0) throw new Error(formatDiagnostics(configFileDiagnostics)); }; const watch = ts.createWatchProgram(watchCompilerHost); return watch; } }; /** If the given filename has extra extensions, returns the d.ts filename. */ function toExtraDtsFileNames(fileName, extraFileExtensions) { const ext = getExtIfExtra(fileName, extraFileExtensions); if (ext != null) return [`${fileName}.d.ts`, `${fileName.slice(0, -ext.length)}.d${ext}.ts`]; return []; } /** Checks the given filename has extra extension or not. */ function isExtra(fileName, extraFileExtensions) { return getExtIfExtra(fileName, extraFileExtensions) != null; } /** Gets the file extension if the given file is an extra extension file. */ function getExtIfExtra(fileName, extraFileExtensions) { for (const extraFileExtension of extraFileExtensions) if (fileName.endsWith(extraFileExtension)) return extraFileExtension; return null; } /** Checks the given filename is virtual file tsx or not. */ function isVirtualTSX(fileName, extraFileExtensions) { for (const extraFileExtension of extraFileExtensions) if (fileName.endsWith(`${extraFileExtension}.tsx`)) return true; return false; } function formatDiagnostics(diagnostics) { return ts.formatDiagnostics(diagnostics, { getCanonicalFileName: (f) => f, getCurrentDirectory: () => process.cwd(), getNewLine: () => "\n" }); } function normalizeFileName(fileName) { let normalized = path.normalize(fileName); if (normalized.endsWith(path.sep)) normalized = normalized.slice(0, -1); if (ts.sys.useCaseSensitiveFileNames) return toAbsolutePath(normalized, null); return toAbsolutePath(normalized.toLowerCase(), null); } function toAbsolutePath(filePath, baseDir) { return path.isAbsolute(filePath) ? filePath : path.join(baseDir || process.cwd(), filePath); } function* iterateDirs(filePath) { let target = filePath; let parent; while ((parent = path.dirname(target)) !== target) { yield parent; target = parent; } } function distinctArray(...list) { return [...new Set(ts.sys.useCaseSensitiveFileNames ? list : list.map((s) => s === null || s === void 0 ? void 0 : s.toLowerCase()))].filter((s) => s != null); } //#endregion //#region src/utils/get-project-config-files.ts function getProjectConfigFiles(options) { if (options.project !== true) return Array.isArray(options.project) ? options.project : [options.project]; let directory = path.dirname(options.filePath); const checkedDirectories = [directory]; do { const tsconfigPath = path.join(directory, "tsconfig.json"); if (fs.existsSync(tsconfigPath)) return [tsconfigPath]; directory = path.dirname(directory); checkedDirectories.push(directory); } 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 '${options.tsconfigRootDir}'.`); } //#endregion //#region src/utils/resolve-project-list.ts /** * Normalizes, sanitizes, resolves and filters the provided project paths */ function resolveProjectList(options) { const sanitizedProjects = []; 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; }, []).map((folder) => folder.startsWith("!") ? folder : `!${folder}`); const nonGlobProjects = sanitizedProjects.filter((project) => !isDynamicPattern(project)); const globProjects = sanitizedProjects.filter((project) => isDynamicPattern(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); } const useCaseSensitiveFileNames = ts$1.sys !== void 0 ? ts$1.sys.useCaseSensitiveFileNames : true; const correctPathCasing = useCaseSensitiveFileNames ? (filePath) => filePath : (filePath) => filePath.toLowerCase(); function getCanonicalFileName(filePath) { let normalized = path.normalize(filePath); if (normalized.endsWith(path.sep)) normalized = normalized.slice(0, -1); return correctPathCasing(normalized); } function ensureAbsolutePath(p, tsconfigRootDir) { return path.isAbsolute(p) ? p : path.join(tsconfigRootDir || process.cwd(), p); } //#endregion //#region package.json var name$1 = "typescript-eslint-parser-for-extra-files"; var version = "0.9.0"; //#endregion //#region src/meta.ts const meta = { name: name$1, version }; var meta_default = meta; const name = name$1; //#endregion //#region src/index.ts const DEFAULT_EXTRA_FILE_EXTENSIONS = [ ".vue", ".svelte", ".astro" ]; const tsServiceManager = new TSServiceManager(); function parseForESLint(code, options = {}) { if (!options.project) return tsEslintParser.parseForESLint(code, options); const extraFileExtensions = options.extraFileExtensions || DEFAULT_EXTRA_FILE_EXTENSIONS; const programs = []; for (const option of iterateOptions(options)) programs.push(tsServiceManager.getProgram(code, option)); const filePath = options.filePath; const parserOptions = { ...options, filePath, programs, extraFileExtensions }; return tsEslintParser.parseForESLint(code, parserOptions); } function* iterateOptions(options) { if (!options) throw new Error("`parserOptions` is required."); if (!options.filePath) throw new Error("`filePath` is required."); if (!options.project) throw new Error("Specify `parserOptions.project`. Otherwise there is no point in using this parser."); 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, extraFileExtensions: options.extraFileExtensions || DEFAULT_EXTRA_FILE_EXTENSIONS }; } //#endregion export { meta_default as meta, name, parseForESLint };