blob: a50ac1ccc02dd92e4fe90a81e847518059d5c16d [file] [log] [blame]
/**
* @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.
*/
import * as path from 'path';
import * as ts from 'typescript';
/**
* The configuration block provided by the tsconfig "bazelOptions".
* Note that all paths here are relative to the rootDir, not absolute nor
* relative to the location containing the tsconfig file.
*/
export interface BazelOptions {
/** Name of the bazel workspace where we are building. */
workspaceName: string;
/** The full bazel target that is being built, e.g. //my/pkg:library. */
target: string;
/** The bazel package, eg my/pkg */
package: string;
/** If true, convert require()s into goog.module(). */
googmodule: boolean;
/**
* If true, emit devmode output into filename.js.
* If false, emit prodmode output into filename.mjs.
*/
es5Mode: boolean;
/** If true, convert TypeScript code into a Closure-compatible variant. */
tsickle: boolean;
/** If true, generate externs from declarations in d.ts files. */
tsickleGenerateExterns: boolean;
/** Write generated externs to the given path. */
tsickleExternsPath: string;
/** Paths of declarations whose types must not appear in result .d.ts. */
typeBlackListPaths: string[];
/** If true, emit Closure types in TypeScript->JS output. */
untyped: boolean;
/** The list of sources we're interested in (emitting and type checking). */
compilationTargetSrc: string[];
/** Path to write the module dependency manifest to. */
manifest: string;
/**
* Whether to disable strict deps check. If true the next parameter is
* ignored.
*/
disableStrictDeps?: boolean;
/**
* Paths of dependencies that are allowed by strict deps, i.e. that may be
* imported by the source files in compilationTargetSrc.
*/
allowedStrictDeps: string[];
/** Write a performance trace to this path. Disabled when falsy. */
perfTracePath?: string;
/**
* An additional prelude to insert after the `goog.module` call,
* e.g. with additional imports or requires.
*/
prelude: string;
/**
* Name of the current locale if processing a locale-specific file.
*/
locale?: string;
/**
* A list of errors this compilation is expected to generate, in the form
* "TS1234:regexp". If empty, compilation is expected to succeed.
*/
expectedDiagnostics: string[];
/**
* To support node_module resolution, allow TypeScript to make arbitrary
* file system access to paths under this prefix.
*/
nodeModulesPrefix: string;
/**
* List of regexes on file paths for which we suppress tsickle's warnings.
*/
ignoreWarningPaths: string[];
/**
* Whether to add aliases to the .d.ts files to add the exports to the
* ಠ_ಠ.clutz namespace.
*/
addDtsClutzAliases: true;
/**
* Whether to type check inputs that aren't srcs. Differs from
* --skipLibCheck, which skips all .d.ts files, even those which are
* srcs.
*/
typeCheckDependencies: boolean;
/**
* The maximum cache size for bazel outputs, in megabytes.
*/
maxCacheSizeMb?: number;
/**
* Suppress warnings about tsconfig.json properties that are overridden.
* Currently unused, remains here for backwards compat for users who set it.
*/
suppressTsconfigOverrideWarnings: boolean;
/**
* An explicit name for this module, given by the module_name attribute on a
* ts_library.
*/
moduleName?: string;
/**
* An explicit entry point for this module, given by the module_root attribute
* on a ts_library.
*/
moduleRoot?: string;
/**
* If true, indicates that this job is transpiling JS sources. If true, only
* one file can appear in compilationTargetSrc, and either
* transpiledJsOutputFileName or the transpiledJs*Directory options must be
* set.
*/
isJsTranspilation?: boolean;
/**
* The path where the file containing the JS transpiled output should be
* written. Ignored if isJsTranspilation is false. transpiledJsOutputFileName
*
*/
transpiledJsOutputFileName?: string;
/**
* The path where transpiled JS output should be written. Ignored if
* isJsTranspilation is false. Must not be set together with
* transpiledJsOutputFileName.
*/
transpiledJsInputDirectory?: string;
/**
* The path where transpiled JS output should be written. Ignored if
* isJsTranspilation is false. Must not be set together with
* transpiledJsOutputFileName.
*/
transpiledJsOutputDirectory?: string;
/**
* Whether the user provided an implementation shim for .d.ts files in the
* compilation unit.
*/
hasImplementation?: boolean;
/**
* Enable the Angular ngtsc plugin.
*/
compileAngularTemplates?: boolean;
/**
* Override for ECMAScript target language level to use for devmode.
*
* This setting can be set in a user's tsconfig to override the default
* devmode target.
*
* EXPERIMENTAL: This setting is experimental and may be removed in the
* future.
*/
devmodeTargetOverride?: string;
}
export interface ParsedTsConfig {
options: ts.CompilerOptions;
bazelOpts: BazelOptions;
angularCompilerOptions?: {[k: string]: unknown};
files: string[];
disabledTsetseRules: string[];
config: {};
}
// TODO(calebegg): Upstream?
interface PluginImportWithConfig extends ts.PluginImport {
[optionName: string]: string|{};
}
/**
* 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.
*/
export function resolveNormalizedPath(...segments: string[]): string {
return path.resolve(...segments).replace(/\\/g, '/');
}
/**
* 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
*/
export function parseTsconfig(
tsconfigFile: string, host: ts.ParseConfigHost = ts.sys):
[ParsedTsConfig|null, ts.Diagnostic[]|null, {target: string}] {
// TypeScript expects an absolute path for the tsconfig.json file
tsconfigFile = resolveNormalizedPath(tsconfigFile);
const isUndefined = (value: any): value is undefined => value === undefined;
// Handle bazel specific options, but make sure not to crash when reading a
// vanilla tsconfig.json.
const readExtendedConfigFile =
(configFile: string, existingConfig?: any): {config?: any, error?: ts.Diagnostic} => {
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: BazelOptions = existingConfig.bazelOptions || {};
const newBazelBazelOpts: BazelOptions = config.bazelOptions || {};
mergedConfig.bazelOptions = {
...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: BazelOptions = 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);
}
let disabledTsetseRules: string[] = [];
for (const pluginConfig of options['plugins'] as PluginImportWithConfig[] ||
[]) {
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 as string[];
break;
}
}
return [
{options, bazelOpts, files, config, disabledTsetseRules}, null, {target}
];
}