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.
404 lines (391 loc) • 17.2 kB
JavaScript
;
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const path = __toESM(require("path"));
const typescript = __toESM(require("typescript"));
const __typescript_eslint_parser = __toESM(require("@typescript-eslint/parser"));
const fs = __toESM(require("fs"));
const tinyglobby = __toESM(require("tinyglobby"));
//#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.default.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.default.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 (!typescript.default.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 ??= typescript.default.createSourceFile(this.currTarget.filePath, this.currTarget.code, languageVersionOrOptions, true, typescript.default.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$5, languageVersionOrOptions, ...args$1) => {
const originalSourceFile = original$1.getSourceFileByPath.call(host, fileName, path$5, languageVersionOrOptions, ...args$1);
return getTargetSourceFile(fileName, languageVersionOrOptions) ?? originalSourceFile;
};
}
return typescript.default.createAbstractBuilder(rootNames, options, host, oldProgram, configFileParsingDiagnostics, projectReferences);
};
const watchCompilerHost = typescript.default.createWatchCompilerHost(tsconfigPath, {
noEmit: true,
jsx: typescript.default.JsxEmit.Preserve,
allowNonTsExtensions: true
}, typescript.default.sys, createAbstractBuilder, (diagnostic) => {
throw new Error(formatDiagnostics([diagnostic]));
}, () => {}, void 0, extraFileExtensions.map((extension) => ({
extension,
isMixedContent: true,
scriptKind: typescript.default.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, typescript.default.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 === typescript.default.DiagnosticCategory.Error && diag.code !== 18003);
if (configFileDiagnostics.length > 0) throw new Error(formatDiagnostics(configFileDiagnostics));
};
const watch = typescript.default.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 typescript.default.formatDiagnostics(diagnostics, {
getCanonicalFileName: (f) => f,
getCurrentDirectory: () => process.cwd(),
getNewLine: () => "\n"
});
}
function normalizeFileName(fileName) {
let normalized = path.default.normalize(fileName);
if (normalized.endsWith(path.default.sep)) normalized = normalized.slice(0, -1);
if (typescript.default.sys.useCaseSensitiveFileNames) return toAbsolutePath(normalized, null);
return toAbsolutePath(normalized.toLowerCase(), null);
}
function toAbsolutePath(filePath, baseDir) {
return path.default.isAbsolute(filePath) ? filePath : path.default.join(baseDir || process.cwd(), filePath);
}
function* iterateDirs(filePath) {
let target = filePath;
let parent;
while ((parent = path.default.dirname(target)) !== target) {
yield parent;
target = parent;
}
}
function distinctArray(...list) {
return [...new Set(typescript.default.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.default.dirname(options.filePath);
const checkedDirectories = [directory];
do {
const tsconfigPath = path.default.join(directory, "tsconfig.json");
if (fs.default.existsSync(tsconfigPath)) return [tsconfigPath];
directory = path.default.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) => !(0, tinyglobby.isDynamicPattern)(project));
const globProjects = sanitizedProjects.filter((project) => (0, tinyglobby.isDynamicPattern)(project));
const uniqueCanonicalProjectPaths = new Set(nonGlobProjects.concat(globProjects.length === 0 ? [] : (0, tinyglobby.globSync)([...globProjects, ...projectFolderIgnoreList], { cwd: options.tsconfigRootDir })).map((project) => getCanonicalFileName(ensureAbsolutePath(project, options.tsconfigRootDir))));
return Array.from(uniqueCanonicalProjectPaths);
}
const useCaseSensitiveFileNames = typescript.sys !== void 0 ? typescript.sys.useCaseSensitiveFileNames : true;
const correctPathCasing = useCaseSensitiveFileNames ? (filePath) => filePath : (filePath) => filePath.toLowerCase();
function getCanonicalFileName(filePath) {
let normalized = path.default.normalize(filePath);
if (normalized.endsWith(path.default.sep)) normalized = normalized.slice(0, -1);
return correctPathCasing(normalized);
}
function ensureAbsolutePath(p, tsconfigRootDir) {
return path.default.isAbsolute(p) ? p : path.default.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 __typescript_eslint_parser.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 __typescript_eslint_parser.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
exports.meta = meta_default
exports.name = name
exports.parseForESLint = parseForESLint