@bazel/typescript
Version:
TypeScript rules for Bazel
167 lines (166 loc) • 8.49 kB
JavaScript
;
/**
* @license
* Copyright 2017 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const ts = require("typescript");
/**
* The same as Node's path.resolve, however it returns a path with forward
* slashes rather than joining the resolved path with the platform's path
* separator.
* Note that even path.posix.resolve('.') returns C:\Users\... with backslashes.
*/
function resolveNormalizedPath(...segments) {
return path.resolve(...segments).replace(/\\/g, '/');
}
exports.resolveNormalizedPath = resolveNormalizedPath;
/**
* Load a tsconfig.json and convert all referenced paths (including
* bazelOptions) to absolute paths.
* Paths seen by TypeScript should be absolute, to match behavior
* of the tsc ModuleResolution implementation.
* @param tsconfigFile path to tsconfig, relative to process.cwd() or absolute
* @return configuration parsed from the file, or error diagnostics
*/
function parseTsconfig(tsconfigFile, host = ts.sys) {
// TypeScript expects an absolute path for the tsconfig.json file
tsconfigFile = resolveNormalizedPath(tsconfigFile);
const isUndefined = (value) => value === undefined;
// Handle bazel specific options, but make sure not to crash when reading a
// vanilla tsconfig.json.
const readExtendedConfigFile = (configFile, existingConfig) => {
const { config, error } = ts.readConfigFile(configFile, host.readFile);
if (error) {
return { error };
}
// Allow Bazel users to control some of the bazel options.
// Since TypeScript's "extends" mechanism applies only to "compilerOptions"
// we have to repeat some of their logic to get the user's bazelOptions.
const mergedConfig = existingConfig || config;
if (existingConfig) {
const existingBazelOpts = existingConfig.bazelOptions || {};
const newBazelBazelOpts = config.bazelOptions || {};
mergedConfig.bazelOptions = Object.assign(Object.assign({}, existingBazelOpts), { disableStrictDeps: isUndefined(existingBazelOpts.disableStrictDeps)
? newBazelBazelOpts.disableStrictDeps
: existingBazelOpts.disableStrictDeps, suppressTsconfigOverrideWarnings: isUndefined(existingBazelOpts.suppressTsconfigOverrideWarnings)
? newBazelBazelOpts.suppressTsconfigOverrideWarnings
: existingBazelOpts.suppressTsconfigOverrideWarnings, tsickle: isUndefined(existingBazelOpts.tsickle)
? newBazelBazelOpts.tsickle
: existingBazelOpts.tsickle, googmodule: isUndefined(existingBazelOpts.googmodule)
? newBazelBazelOpts.googmodule
: existingBazelOpts.googmodule, devmodeTargetOverride: isUndefined(existingBazelOpts.devmodeTargetOverride)
? newBazelBazelOpts.devmodeTargetOverride
: existingBazelOpts.devmodeTargetOverride });
}
if (config.extends) {
let extendedConfigPath = resolveNormalizedPath(path.dirname(configFile), config.extends);
if (!extendedConfigPath.endsWith('.json'))
extendedConfigPath += '.json';
return readExtendedConfigFile(extendedConfigPath, mergedConfig);
}
return { config: mergedConfig };
};
const { config, error } = readExtendedConfigFile(tsconfigFile);
if (error) {
// target is in the config file we failed to load...
return [null, [error], { target: '' }];
}
const { options, errors, fileNames } = ts.parseJsonConfigFileContent(config, host, path.dirname(tsconfigFile));
// Handle bazel specific options, but make sure not to crash when reading a
// vanilla tsconfig.json.
const bazelOpts = config.bazelOptions || {};
const target = bazelOpts.target;
bazelOpts.allowedStrictDeps = bazelOpts.allowedStrictDeps || [];
bazelOpts.typeBlackListPaths = bazelOpts.typeBlackListPaths || [];
bazelOpts.compilationTargetSrc = bazelOpts.compilationTargetSrc || [];
if (errors && errors.length) {
return [null, errors, { target }];
}
// Override the devmode target if devmodeTargetOverride is set
if (bazelOpts.es5Mode && bazelOpts.devmodeTargetOverride) {
switch (bazelOpts.devmodeTargetOverride.toLowerCase()) {
case 'es3':
options.target = ts.ScriptTarget.ES3;
break;
case 'es5':
options.target = ts.ScriptTarget.ES5;
break;
case 'es2015':
options.target = ts.ScriptTarget.ES2015;
break;
case 'es2016':
options.target = ts.ScriptTarget.ES2016;
break;
case 'es2017':
options.target = ts.ScriptTarget.ES2017;
break;
case 'es2018':
options.target = ts.ScriptTarget.ES2018;
break;
case 'esnext':
options.target = ts.ScriptTarget.ESNext;
break;
default:
console.error('WARNING: your tsconfig.json file specifies an invalid bazelOptions.devmodeTargetOverride value of: \'${bazelOpts.devmodeTargetOverride\'');
}
}
// Sort rootDirs with longest include directories first.
// When canonicalizing paths, we always want to strip
// `workspace/bazel-bin/file` to just `file`, not to `bazel-bin/file`.
if (options.rootDirs)
options.rootDirs.sort((a, b) => b.length - a.length);
// If the user requested goog.module, we need to produce that output even if
// the generated tsconfig indicates otherwise.
if (bazelOpts.googmodule)
options.module = ts.ModuleKind.CommonJS;
// TypeScript's parseJsonConfigFileContent returns paths that are joined, eg.
// /path/to/project/bazel-out/arch/bin/path/to/package/../../../../../../path
// We normalize them to remove the intermediate parent directories.
// This improves error messages and also matches logic in tsc_wrapped where we
// expect normalized paths.
const files = fileNames.map(f => path.posix.normalize(f));
// The bazelOpts paths in the tsconfig are relative to
// options.rootDir (the workspace root) and aren't transformed by
// parseJsonConfigFileContent (because TypeScript doesn't know
// about them). Transform them to also be absolute here.
bazelOpts.compilationTargetSrc = bazelOpts.compilationTargetSrc.map(f => resolveNormalizedPath(options.rootDir, f));
bazelOpts.allowedStrictDeps = bazelOpts.allowedStrictDeps.map(f => resolveNormalizedPath(options.rootDir, f));
bazelOpts.typeBlackListPaths = bazelOpts.typeBlackListPaths.map(f => resolveNormalizedPath(options.rootDir, f));
if (bazelOpts.nodeModulesPrefix) {
bazelOpts.nodeModulesPrefix =
resolveNormalizedPath(options.rootDir, bazelOpts.nodeModulesPrefix);
}
if (bazelOpts.angularCompilerOptions && bazelOpts.angularCompilerOptions.assets) {
bazelOpts.angularCompilerOptions.assets = bazelOpts.angularCompilerOptions.assets.map(f => resolveNormalizedPath(options.rootDir, f));
}
let disabledTsetseRules = [];
for (const pluginConfig of options['plugins'] ||
[]) {
if (pluginConfig.name && pluginConfig.name === '@bazel/tsetse') {
const disabledRules = pluginConfig['disabledRules'];
if (disabledRules && !Array.isArray(disabledRules)) {
throw new Error('Disabled tsetse rules must be an array of rule names');
}
disabledTsetseRules = disabledRules;
break;
}
}
return [
{ options, bazelOpts, files, config, disabledTsetseRules }, null, { target }
];
}
exports.parseTsconfig = parseTsconfig;