diff --git a/tools/tslint/rollupConfigRule.ts b/tools/tslint/rollupConfigRule.ts new file mode 100644 index 0000000000..46d279b1bc --- /dev/null +++ b/tools/tslint/rollupConfigRule.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import {RuleFailure} from 'tslint/lib'; +import {AbstractRule} from 'tslint/lib/rules'; +import * as ts from 'typescript'; + + +function _isRollupPath(path: string) { + return /rollup\.config\.js$/.test(path); +} + +// Regexes to blacklist. +const sourceFilePathBlacklist = [ + /\.spec\.ts$/, + /_spec\.ts$/, + /_perf\.ts$/, + /_example\.ts$/, + /[/\\]test[/\\]/, + /[/\\]testing_internal\.ts$/, + /[/\\]integrationtest[/\\]/, + /[/\\]packages[/\\]bazel[/\\]/, + /[/\\]packages[/\\]benchpress[/\\]/, + /[/\\]packages[/\\]examples[/\\]/, + + // language-service bundles everything in its UMD, so we don't need a globals. There are + // exceptions but we simply ignore those files from this rule. + /[/\\]packages[/\\]language-service[/\\]/, + + // service-worker is a special package that has more than one rollup config. It confuses + // this lint rule and we simply ignore those files. + /[/\\]packages[/\\]service-worker[/\\]/, +]; + +// Import package name whitelist. These will be ignored. +const importsWhitelist = [ + '@angular/compiler-cli', // Not used in a browser. + '@angular/compiler-cli/src/language_services', // Deep import from language-service. + 'chokidar', // Not part of compiler-cli/browser, but still imported. + 'reflect-metadata', + 'tsickle', + 'url', // Part of node, no need to alias in rollup. + 'zone.js', +]; + +const packageScopedImportWhitelist: [RegExp, string[]][] = [ + [/service-worker[/\\]cli/, ['@angular/service-worker']], +]; + + +// Return true if the file should be linted. +function _pathShouldBeLinted(path: string) { + return /[/\\]packages[/\\]/.test(path) && sourceFilePathBlacklist.every(re => !re.test(path)); +} + + +interface RollupMatchInfo { + filePath: string; + globals: {[packageName: string]: string}; +} +const rollupConfigMap = new Map(); + + +export class Rule extends AbstractRule { + public apply(sourceFile: ts.SourceFile): RuleFailure[] { + const allImports = sourceFile.statements.filter( + x => x.kind === ts.SyntaxKind.ImportDeclaration); + + // Ignore specs, non-package files, and examples. + if (!_pathShouldBeLinted(sourceFile.fileName)) { + return []; + } + + // Find the rollup.config.js from this location, if it exists. + // If rollup cannot be found, this is an error. + let p = path.dirname(sourceFile.fileName); + let checkedPaths = []; + let match: RollupMatchInfo; + + while (p.startsWith(process.cwd())) { + if (rollupConfigMap.has(p)) { + // We already resolved for this directory, just return it. + match = rollupConfigMap.get(p); + break; + } + + const allFiles = fs.readdirSync(p); + const maybeRollupPath = allFiles.find(x => _isRollupPath(path.join(p, x))); + if (maybeRollupPath) { + const rollupFilePath = path.join(p, maybeRollupPath); + const rollupConfig = require(rollupFilePath).default; + match = {filePath: rollupFilePath, globals: rollupConfig && rollupConfig.globals}; + + // Update all paths that we checked along the way. + checkedPaths.forEach(path => rollupConfigMap.set(path, match)); + rollupConfigMap.set(rollupFilePath, match); + break; + } + + checkedPaths.push(p); + p = path.dirname(p); + } + if (!match) { + throw new Error( + `Could not find rollup.config.js for ${JSON.stringify(sourceFile.fileName)}.`); + } + + const rollupFilePath = match.filePath; + const globalConfig = match.globals || Object.create(null); + + return allImports + .map(importStatement => { + const modulePath = (importStatement.moduleSpecifier as ts.StringLiteral).text; + if (modulePath.startsWith('.')) { + return null; + } + + if (importsWhitelist.indexOf(modulePath) != -1) { + return null; + } + + for (const [re, arr] of packageScopedImportWhitelist) { + if (re.test(sourceFile.fileName) && arr.indexOf(modulePath) != -1) { + return null; + } + } + + if (!(modulePath in globalConfig)) { + return new RuleFailure( + sourceFile, importStatement.getStart(), importStatement.getWidth(), + `Import ${JSON.stringify(modulePath)} could not be found in the rollup config ` + + `at path ${JSON.stringify(rollupFilePath)}.`, + this.ruleName, ); + } + + return null; + }) + .filter(x => !!x); + } +} diff --git a/tslint.json b/tslint.json index 6993b73684..abf8688092 100644 --- a/tslint.json +++ b/tslint.json @@ -12,6 +12,7 @@ "no-jasmine-focus": true, "no-var-keyword": true, "require-internal-with-underscore": true, + "rollup-config": true, "semicolon": [true], "variable-name": [true, "ban-keywords"], "no-inner-declarations": [true, "function"]