mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[eslint] add rule to prevent export* in plugin index files (#109357)
* [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>
This commit is contained in:
parent
4f0a63f575
commit
fecdba7eba
89 changed files with 792 additions and 4 deletions
|
@ -6,6 +6,7 @@ PKG_REQUIRE_NAME = "@kbn/eslint-plugin-eslint"
|
|||
SOURCE_FILES = glob(
|
||||
[
|
||||
"rules/**/*.js",
|
||||
"helpers/**/*.js",
|
||||
"index.js",
|
||||
"lib.js",
|
||||
],
|
||||
|
|
11
packages/kbn-eslint-plugin-eslint/__fixtures__/bar.ts
Normal file
11
packages/kbn-eslint-plugin-eslint/__fixtures__/bar.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
export class Bar {}
|
13
packages/kbn-eslint-plugin-eslint/__fixtures__/baz.ts
Normal file
13
packages/kbn-eslint-plugin-eslint/__fixtures__/baz.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
export const one = 1;
|
||||
export const two = 2;
|
||||
export const three = 3;
|
31
packages/kbn-eslint-plugin-eslint/__fixtures__/foo.ts
Normal file
31
packages/kbn-eslint-plugin-eslint/__fixtures__/foo.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
export type { Bar as ReexportedClass } from './bar';
|
||||
|
||||
export const someConst = 'bar';
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
export let someLet = 'bar';
|
||||
|
||||
export function someFunction() {}
|
||||
|
||||
export class SomeClass {}
|
||||
|
||||
export interface SomeInterface {
|
||||
prop: number;
|
||||
}
|
||||
|
||||
export enum SomeEnum {
|
||||
a = 'a',
|
||||
b = 'b',
|
||||
}
|
||||
|
||||
export type TypeAlias = string[];
|
11
packages/kbn-eslint-plugin-eslint/__fixtures__/top.ts
Normal file
11
packages/kbn-eslint-plugin-eslint/__fixtures__/top.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
||||
export * from './foo';
|
82
packages/kbn-eslint-plugin-eslint/helpers/codegen.js
Normal file
82
packages/kbn-eslint-plugin-eslint/helpers/codegen.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 t = require('@babel/types');
|
||||
const { default: generate } = require('@babel/generator');
|
||||
|
||||
/** @typedef {import('./export_set').ExportSet} ExportSet */
|
||||
|
||||
/**
|
||||
* Generate code for replacing a `export * from './path'`, ie.
|
||||
*
|
||||
* export type { foo } from './path'
|
||||
* export { bar } from './path'
|
||||
|
||||
* @param {ExportSet} exportSet
|
||||
* @param {string} source
|
||||
*/
|
||||
const getExportCode = (exportSet, source) => {
|
||||
const exportedTypes = exportSet.types.size
|
||||
? t.exportNamedDeclaration(
|
||||
undefined,
|
||||
Array.from(exportSet.types).map((n) => t.exportSpecifier(t.identifier(n), t.identifier(n))),
|
||||
t.stringLiteral(source)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (exportedTypes) {
|
||||
exportedTypes.exportKind = 'type';
|
||||
}
|
||||
|
||||
const exportedValues = exportSet.values.size
|
||||
? t.exportNamedDeclaration(
|
||||
undefined,
|
||||
Array.from(exportSet.values).map((n) =>
|
||||
t.exportSpecifier(t.identifier(n), t.identifier(n))
|
||||
),
|
||||
t.stringLiteral(source)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return generate(t.program([exportedTypes, exportedValues].filter(Boolean))).code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate code for replacing a `export * as name from './path'`, ie.
|
||||
*
|
||||
* import { foo, bar } from './path'
|
||||
* export const name = { foo, bar }
|
||||
*
|
||||
* @param {string} nsName
|
||||
* @param {string[]} exportNames
|
||||
* @param {string} source
|
||||
*/
|
||||
const getExportNamedNamespaceCode = (nsName, exportNames, source) => {
|
||||
return generate(
|
||||
t.program([
|
||||
t.importDeclaration(
|
||||
exportNames.map((n) => t.importSpecifier(t.identifier(n), t.identifier(n))),
|
||||
t.stringLiteral(source)
|
||||
),
|
||||
t.exportNamedDeclaration(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(nsName),
|
||||
t.objectExpression(
|
||||
exportNames.map((n) =>
|
||||
t.objectProperty(t.identifier(n), t.identifier(n), false, true)
|
||||
)
|
||||
)
|
||||
),
|
||||
])
|
||||
),
|
||||
])
|
||||
).code;
|
||||
};
|
||||
|
||||
module.exports = { getExportCode, getExportNamedNamespaceCode };
|
34
packages/kbn-eslint-plugin-eslint/helpers/export_set.js
Normal file
34
packages/kbn-eslint-plugin-eslint/helpers/export_set.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper class to collect exports of different types, either "value" exports or "type" exports
|
||||
*/
|
||||
class ExportSet {
|
||||
constructor() {
|
||||
/** @type {Set<string>} */
|
||||
this.values = new Set();
|
||||
|
||||
/** @type {Set<string>} */
|
||||
this.types = new Set();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.values.size + this.types.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'value'|'type'} type
|
||||
* @param {string} value
|
||||
*/
|
||||
add(type, value) {
|
||||
this[type + 's'].add(value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ExportSet };
|
206
packages/kbn-eslint-plugin-eslint/helpers/exports.js
Normal file
206
packages/kbn-eslint-plugin-eslint/helpers/exports.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* 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 };
|
|
@ -12,6 +12,7 @@ module.exports = {
|
|||
'disallow-license-headers': require('./rules/disallow_license_headers'),
|
||||
'no-restricted-paths': require('./rules/no_restricted_paths'),
|
||||
module_migration: require('./rules/module_migration'),
|
||||
no_export_all: require('./rules/no_export_all'),
|
||||
no_async_promise_body: require('./rules/no_async_promise_body'),
|
||||
},
|
||||
};
|
||||
|
|
84
packages/kbn-eslint-plugin-eslint/rules/no_export_all.js
Normal file
84
packages/kbn-eslint-plugin-eslint/rules/no_export_all.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 ts = require('typescript');
|
||||
const { getExportCode, getExportNamedNamespaceCode } = require('../helpers/codegen');
|
||||
const tsEstree = require('@typescript-eslint/typescript-estree');
|
||||
|
||||
const { getExportNamesDeep } = require('../helpers/exports');
|
||||
|
||||
/** @typedef {import("eslint").Rule.RuleModule} Rule */
|
||||
/** @typedef {import("@typescript-eslint/parser").ParserServices} ParserServices */
|
||||
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ExportAllDeclaration} EsTreeExportAllDeclaration */
|
||||
/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.StringLiteral} EsTreeStringLiteral */
|
||||
/** @typedef {import("typescript").ExportDeclaration} ExportDeclaration */
|
||||
/** @typedef {import("../helpers/exports").Parser} Parser */
|
||||
/** @typedef {import("eslint").Rule.RuleFixer} Fixer */
|
||||
|
||||
const ERROR_MSG =
|
||||
'`export *` is not allowed in the index files of plugins to prevent accidentally exporting too many APIs';
|
||||
|
||||
/** @type {Rule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
create: (context) => {
|
||||
return {
|
||||
ExportAllDeclaration(node) {
|
||||
const services = /** @type ParserServices */ (context.parserServices);
|
||||
const esNode = /** @type EsTreeExportAllDeclaration */ (node);
|
||||
const tsnode = /** @type ExportDeclaration */ (services.esTreeNodeToTSNodeMap.get(esNode));
|
||||
|
||||
/** @type Parser */
|
||||
const parser = (path) => {
|
||||
const code = Fs.readFileSync(path, 'utf-8');
|
||||
const result = tsEstree.parseAndGenerateServices(code, {
|
||||
...context.parserOptions,
|
||||
comment: false,
|
||||
filePath: path,
|
||||
});
|
||||
return result.services.program.getSourceFile(path);
|
||||
};
|
||||
|
||||
const exportSet = getExportNamesDeep(parser, context.getFilename(), tsnode);
|
||||
const isTypeExport = esNode.exportKind === 'type';
|
||||
const isNamespaceExportWithTypes =
|
||||
tsnode.exportClause &&
|
||||
ts.isNamespaceExport(tsnode.exportClause) &&
|
||||
(isTypeExport || exportSet.types.size);
|
||||
|
||||
/** @param {Fixer} fixer */
|
||||
const fix = (fixer) => {
|
||||
const source = /** @type EsTreeStringLiteral */ (esNode.source);
|
||||
|
||||
if (tsnode.exportClause && ts.isNamespaceExport(tsnode.exportClause)) {
|
||||
return fixer.replaceText(
|
||||
node,
|
||||
getExportNamedNamespaceCode(
|
||||
tsnode.exportClause.name.getText(),
|
||||
Array.from(exportSet.values),
|
||||
source.value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return fixer.replaceText(node, getExportCode(exportSet, source.value));
|
||||
};
|
||||
|
||||
context.report({
|
||||
message: ERROR_MSG,
|
||||
loc: node.loc,
|
||||
fix: exportSet?.size && !isNamespaceExportWithTypes ? fix : undefined,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 Path = require('path');
|
||||
|
||||
const { RuleTester } = require('eslint');
|
||||
const dedent = require('dedent');
|
||||
|
||||
const rule = require('./no_export_all');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2018,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('@kbn/eslint/no_export_all', rule, {
|
||||
valid: [
|
||||
{
|
||||
code: dedent`
|
||||
export { bar } from './foo';
|
||||
export { bar as box } from './foo';
|
||||
`,
|
||||
},
|
||||
],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
filename: Path.resolve(__dirname, '../__fixtures__/index.ts'),
|
||||
code: dedent`
|
||||
export * as baz from './baz';
|
||||
export * from './foo';
|
||||
`,
|
||||
|
||||
errors: [
|
||||
{
|
||||
line: 1,
|
||||
message:
|
||||
'`export *` is not allowed in the index files of plugins to prevent accidentally exporting too many APIs',
|
||||
},
|
||||
{
|
||||
line: 2,
|
||||
message:
|
||||
'`export *` is not allowed in the index files of plugins to prevent accidentally exporting too many APIs',
|
||||
},
|
||||
],
|
||||
|
||||
output: dedent`
|
||||
import { one, two, three } from "./baz";
|
||||
export const baz = {
|
||||
one,
|
||||
two,
|
||||
three
|
||||
};
|
||||
export type { ReexportedClass, SomeInterface, TypeAlias } from "./foo";
|
||||
export { someConst, someLet, someFunction, SomeClass, SomeEnum } from "./foo";
|
||||
`,
|
||||
},
|
||||
],
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue