import * as path from 'path'; import * as ts from 'typescript'; import * as Lint from 'tslint'; import * as minimatch from 'minimatch'; /** * Rule that enforces that the specified external packages have been included in our Rollup config. * Usage: [true, './path/to/rollup/config.json'] */ export class Rule extends Lint.Rules.AbstractRule { apply(sourceFile: ts.SourceFile) { return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); } } class Walker extends Lint.RuleWalker { /** Path to the rollup globals configuration file. */ private _configPath: string; /** Rollup globals configuration object. */ private _config: {[globalName: string]: string}; /** Whether the walker should check the current source file. */ private _enabled: boolean; constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { super(sourceFile, options); if (!options.ruleArguments.length) { throw Error('missing-rollup-globals: The Rollup config path has to be specified.'); } const [configPath, ...fileGlobs] = options.ruleArguments; // Relative path for the current TypeScript source file. const relativeFilePath = path.relative(process.cwd(), sourceFile.fileName); this._configPath = path.resolve(process.cwd(), configPath); this._config = require(this._configPath).rollupGlobals; this._enabled = fileGlobs.some(p => minimatch(relativeFilePath, p)); } visitImportDeclaration(node: ts.ImportDeclaration) { // Parse out the module name. The first and last characters are the quote marks. const module = node.moduleSpecifier.getText().slice(1, -1); const isExternal = !module.startsWith('.') && !module.startsWith('/'); // Check whether the module is external and whether it's in our config. if (this._enabled && isExternal && !this._config[module]) { this.addFailureAtNode(node, `Module "${module}" is missing from file ${this._configPath}.`); } super.visitImportDeclaration(node); } }