* [eslint] add rule to prevent export* in plugin index files * deduplicate export names for types/instances with the same name * attempt to auto-fix duplicate exports too * capture exported enums too * enforce no_export_all for core too * disable rule by default, allow opting-in for help fixing * update tests * reduce yarn.lock duplication * add rule but no fixes * disable all existing violations * update api docs with new line numbers * revert unnecessary changes to yarn.lock which only had drawbacks * remove unnecessary eslint-disable * rework codegen to split type exports and use babel to generate valid code * check for "export types" deeply * improve test by using fixtures * add comments to some helper functions * disable fix for namespace exports including types * label all eslint-disable comments with related team-specific issue * ensure that child exports of `export type` are always tracked as types Co-authored-by: spalger <spalger@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
206 lines
5.6 KiB
JavaScript
206 lines
5.6 KiB
JavaScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
|
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
|
* Side Public License, v 1.
|
|
*/
|
|
|
|
const Fs = require('fs');
|
|
const Path = require('path');
|
|
const ts = require('typescript');
|
|
const { REPO_ROOT } = require('@kbn/dev-utils');
|
|
const { ExportSet } = require('./export_set');
|
|
|
|
/** @typedef {import("@typescript-eslint/types").TSESTree.ExportAllDeclaration} ExportAllDeclaration */
|
|
/** @typedef {import("estree").Node} Node */
|
|
/** @typedef {(path: string) => ts.SourceFile} Parser */
|
|
/** @typedef {ts.Identifier|ts.BindingName} ExportNameNode */
|
|
|
|
const RESOLUTION_EXTENSIONS = ['.js', '.json', '.ts', '.tsx', '.d.ts'];
|
|
|
|
/** @param {ts.Statement} node */
|
|
const hasExportMod = (node) => node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
|
|
/** @param {string} path */
|
|
const safeStat = (path) => {
|
|
try {
|
|
return Fs.statSync(path);
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
return undefined;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} dir
|
|
* @param {string} specifier
|
|
* @returns {string|undefined}
|
|
*/
|
|
const normalizeRelativeSpecifier = (dir, specifier) => {
|
|
if (specifier.startsWith('src/') || specifier.startsWith('x-pack/')) {
|
|
return Path.resolve(REPO_ROOT, specifier);
|
|
}
|
|
if (specifier.startsWith('.')) {
|
|
return Path.resolve(dir, specifier);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} basePath
|
|
* @returns {string | undefined}
|
|
*/
|
|
const checkExtensions = (basePath) => {
|
|
for (const ext of RESOLUTION_EXTENSIONS) {
|
|
const withExt = `${basePath}${ext}`;
|
|
const stats = safeStat(withExt);
|
|
if (stats?.isFile()) {
|
|
return withExt;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {string} dir
|
|
* @param {string} specifier
|
|
* @returns {string|undefined}
|
|
*/
|
|
const getImportPath = (dir, specifier) => {
|
|
const base = normalizeRelativeSpecifier(dir, specifier);
|
|
if (!specifier) {
|
|
return undefined;
|
|
}
|
|
|
|
const noExt = safeStat(base);
|
|
if (noExt && noExt.isFile()) {
|
|
return base;
|
|
}
|
|
|
|
if (noExt && noExt.isDirectory()) {
|
|
return checkExtensions(Path.resolve(base, 'index'));
|
|
}
|
|
|
|
if (Path.extname(base) !== '') {
|
|
return;
|
|
}
|
|
|
|
return checkExtensions(base);
|
|
};
|
|
|
|
/**
|
|
* Recursively traverse from a file path to collect all the exported values/types
|
|
* from the file. Returns an ExportSet which groups the exports by type, either
|
|
* "value" or "type" exports.
|
|
*
|
|
* @param {Parser} parser
|
|
* @param {string} from
|
|
* @param {ts.ExportDeclaration} exportFrom
|
|
* @param {ExportSet | undefined} exportSet only passed when called recursively
|
|
* @param {boolean | undefined} assumeAllTypes only passed when called recursively
|
|
* @returns {ExportSet | undefined}
|
|
*/
|
|
const getExportNamesDeep = (
|
|
parser,
|
|
from,
|
|
exportFrom,
|
|
exportSet = new ExportSet(),
|
|
assumeAllTypes = false
|
|
) => {
|
|
const specifier = ts.isStringLiteral(exportFrom.moduleSpecifier)
|
|
? exportFrom.moduleSpecifier.text
|
|
: undefined;
|
|
|
|
if (!specifier) {
|
|
return undefined;
|
|
}
|
|
|
|
const importPath = getImportPath(Path.dirname(from), specifier);
|
|
if (!importPath) {
|
|
return undefined;
|
|
}
|
|
|
|
const sourceFile = parser(importPath);
|
|
|
|
for (const statement of sourceFile.statements) {
|
|
// export function xyz() ...
|
|
if (ts.isFunctionDeclaration(statement) && statement.name && hasExportMod(statement)) {
|
|
exportSet.add(assumeAllTypes ? 'type' : 'value', statement.name.getText());
|
|
continue;
|
|
}
|
|
|
|
// export const/let foo = ...
|
|
if (ts.isVariableStatement(statement) && hasExportMod(statement)) {
|
|
for (const dec of statement.declarationList.declarations) {
|
|
exportSet.add(assumeAllTypes ? 'type' : 'value', dec.name.getText());
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// export class xyc
|
|
if (ts.isClassDeclaration(statement) && statement.name && hasExportMod(statement)) {
|
|
exportSet.add(assumeAllTypes ? 'type' : 'value', statement.name.getText());
|
|
continue;
|
|
}
|
|
|
|
// export interface Foo {...}
|
|
if (ts.isInterfaceDeclaration(statement) && hasExportMod(statement)) {
|
|
exportSet.add('type', statement.name.getText());
|
|
continue;
|
|
}
|
|
|
|
// export type Foo = ...
|
|
if (ts.isTypeAliasDeclaration(statement) && hasExportMod(statement)) {
|
|
exportSet.add('type', statement.name.getText());
|
|
continue;
|
|
}
|
|
|
|
// export enum ...
|
|
if (ts.isEnumDeclaration(statement) && hasExportMod(statement)) {
|
|
exportSet.add(assumeAllTypes ? 'type' : 'value', statement.name.getText());
|
|
continue;
|
|
}
|
|
|
|
if (ts.isExportDeclaration(statement)) {
|
|
const clause = statement.exportClause;
|
|
const types = assumeAllTypes || statement.isTypeOnly;
|
|
|
|
// export * from '../foo';
|
|
if (!clause) {
|
|
const childTypes = getExportNamesDeep(
|
|
parser,
|
|
sourceFile.fileName,
|
|
statement,
|
|
exportSet,
|
|
types
|
|
);
|
|
|
|
if (!childTypes) {
|
|
// abort if we can't get all the exported names
|
|
return undefined;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// export * as foo from './foo'
|
|
if (ts.isNamespaceExport(clause)) {
|
|
exportSet.add(types ? 'type' : 'value', clause.name.getText());
|
|
continue;
|
|
}
|
|
|
|
// export { foo }
|
|
// export { foo as x } from 'other'
|
|
// export { default as foo } from 'other'
|
|
for (const e of clause.elements) {
|
|
exportSet.add(types ? 'type' : 'value', e.name.getText());
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return exportSet;
|
|
};
|
|
|
|
module.exports = { getExportNamesDeep };
|