mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {RuleFailure, WalkContext} from 'tslint/lib';
|
|
import {AbstractRule} from 'tslint/lib/rules';
|
|
import ts from 'typescript';
|
|
|
|
const noNamedExportsError =
|
|
'Named import is not allowed. The module does not expose named exports when ' +
|
|
'imported in an ES module. Use a default import instead.';
|
|
|
|
const noDefaultExportError =
|
|
'Default import is not allowed. The module does not expose a default export at ' +
|
|
'runtime. Use a named import instead.';
|
|
|
|
interface RuleOptions {
|
|
/**
|
|
* List of modules without any named exports that NodeJS can statically detect when the
|
|
* CommonJS module is imported from ESM. Node only exposes named exports which are
|
|
* statically discoverable: https://nodejs.org/api/esm.html#esm_import_statements.
|
|
*/
|
|
noNamedExports?: string[];
|
|
/**
|
|
* List of modules which appear to have named exports in the typings but do
|
|
* not have any at runtime due to NodeJS not being able to discover these
|
|
* through static analysis: https://nodejs.org/api/esm.html#esm_import_statements.
|
|
* */
|
|
noDefaultExport?: string[];
|
|
/**
|
|
* List of modules which are always incompatible. The rule allows for a custom
|
|
* message to be provided when it discovers an import to such a module.
|
|
*/
|
|
incompatibleModules?: Record<string, string>;
|
|
}
|
|
|
|
/**
|
|
* Rule that blocks named imports from being used for certain configured module
|
|
* specifiers. This is helpful for enforcing an ESM-compatible interop with CommonJS
|
|
* modules which do not expose named bindings at runtime.
|
|
*
|
|
* For example, consider the `typescript` module. It does not statically expose named
|
|
* exports even though the type definition suggests it. An import like the following
|
|
* will break at runtime when the `typescript` CommonJS module is imported inside an ESM.
|
|
*
|
|
* ```
|
|
* import * as ts from 'typescript';
|
|
* console.log(ts.SyntaxKind.CallExpression); // `SyntaxKind is undefined`.
|
|
* ```
|
|
*
|
|
* More details here: https://nodejs.org/api/esm.html#esm_import_statements.
|
|
*/
|
|
export class Rule extends AbstractRule {
|
|
override apply(sourceFile: ts.SourceFile): RuleFailure[] {
|
|
const options = this.getOptions().ruleArguments[0];
|
|
return this.applyWithFunction(sourceFile, (ctx) => visitNode(sourceFile, ctx, options));
|
|
}
|
|
}
|
|
|
|
function visitNode(node: ts.Node, ctx: WalkContext, options: RuleOptions) {
|
|
if (options.incompatibleModules && ts.isImportDeclaration(node)) {
|
|
const specifier = node.moduleSpecifier as ts.StringLiteral;
|
|
const failureMsg = options.incompatibleModules[specifier.text];
|
|
|
|
if (failureMsg !== undefined) {
|
|
ctx.addFailureAtNode(node, failureMsg);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (options.noNamedExports && isNamedImportToDisallowedModule(node, options.noNamedExports)) {
|
|
ctx.addFailureAtNode(node, noNamedExportsError);
|
|
}
|
|
|
|
if (options.noDefaultExport && isDefaultImportToDisallowedModule(node, options.noDefaultExport)) {
|
|
ctx.addFailureAtNode(node, noDefaultExportError);
|
|
}
|
|
|
|
ts.forEachChild(node, (n) => visitNode(n, ctx, options));
|
|
}
|
|
|
|
function isNamedImportToDisallowedModule(node: ts.Node, disallowed: string[]): boolean {
|
|
if (!ts.isImportDeclaration(node) || node.importClause === undefined) {
|
|
return false;
|
|
}
|
|
const specifier = node.moduleSpecifier as ts.StringLiteral;
|
|
return !!node.importClause.namedBindings && disallowed.includes(specifier.text);
|
|
}
|
|
|
|
function isDefaultImportToDisallowedModule(node: ts.Node, disallowed: string[]) {
|
|
if (!ts.isImportDeclaration(node) || node.importClause === undefined) {
|
|
return false;
|
|
}
|
|
const specifier = node.moduleSpecifier as ts.StringLiteral;
|
|
|
|
return node.importClause.name !== undefined && disallowed.includes(specifier.text);
|
|
}
|