| /** |
| * @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 ES5 into filename.es5.js. */ |
| 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. |
| */ |
| 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 transpiledJsOutputFileName |
| * must be set. |
| */ |
| isJsTranspilation?: boolean; |
| |
| /** |
| * The path where the file containing the JS transpiled output should |
| * be written. Ignored if isJsTranspilation is false. |
| */ |
| transpiledJsOutputFileName?: 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; |
| } |
| |
| 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|{}; |
| } |
| |
| /** |
| * Prints messages to stderr if the given config object contains certain known |
| * properties that Bazel will override in the generated tsconfig.json. |
| * Note that this is not an exhaustive list of such properties; just the ones |
| * thought to commonly cause problems. |
| * Note that we can't error out, because users might have a legitimate reason: |
| * - during a transition to Bazel they can use the same tsconfig with other |
| * tools |
| * - if they have multiple packages in their repo, they might need to use path |
| * mapping so the editor knows where to resolve some absolute imports |
| * |
| * @param userConfig the parsed json for the full tsconfig.json file |
| */ |
| function warnOnOverriddenOptions(userConfig: any) { |
| const overrideWarnings: string[] = []; |
| if (userConfig.files) { |
| overrideWarnings.push( |
| 'files is ignored because it is controlled by the srcs[] attribute'); |
| } |
| const options: ts.CompilerOptions = userConfig.compilerOptions; |
| if (options) { |
| if (options.target || options.module) { |
| overrideWarnings.push( |
| 'compilerOptions.target and compilerOptions.module are controlled by downstream dependencies, such as ts_devserver'); |
| } |
| if (options.declaration) { |
| overrideWarnings.push( |
| `compilerOptions.declaration is always true, as it's needed for dependent libraries to type-check`); |
| } |
| if (options.paths) { |
| overrideWarnings.push( |
| 'compilerOptions.paths is determined by the module_name attribute in transitive deps[]'); |
| } |
| if (options.typeRoots) { |
| overrideWarnings.push( |
| 'compilerOptions.typeRoots is always set to the @types subdirectory of the node_modules attribute'); |
| } |
| if (options.traceResolution || (options as any).diagnostics) { |
| overrideWarnings.push( |
| 'compilerOptions.traceResolution and compilerOptions.diagnostics are set by the DEBUG flag in tsconfig.bzl under rules_typescript'); |
| } |
| if (options.rootDir || options.baseUrl) { |
| overrideWarnings.push( |
| 'compilerOptions.rootDir and compilerOptions.baseUrl are always the workspace root directory'); |
| } |
| if (options.preserveConstEnums) { |
| overrideWarnings.push( |
| 'compilerOptions.preserveConstEnums is always false under Bazel'); |
| } |
| if (options.noEmitOnError) { |
| // TODO(alexeagle): why?? |
| overrideWarnings.push( |
| 'compilerOptions.noEmitOnError is always false under Bazel'); |
| } |
| } |
| if (overrideWarnings.length) { |
| console.error( |
| '\nWARNING: your tsconfig.json file specifies options which are overridden by Bazel:'); |
| for (const w of overrideWarnings) console.error(` - ${w}`); |
| console.error('\n'); |
| } |
| } |
| |
| /** |
| * 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 {config, error} = ts.readConfigFile(tsconfigFile, host.readFile); |
| if (error) { |
| // target is in the config file we failed to load... |
| return [null, [error], {target: ''}]; |
| } |
| |
| // 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 || []; |
| |
| // 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. |
| if (config.extends) { |
| let userConfigFile = |
| resolveNormalizedPath(path.dirname(tsconfigFile), config.extends); |
| if (!userConfigFile.endsWith('.json')) userConfigFile += '.json'; |
| const {config: userConfig, error} = |
| ts.readConfigFile(userConfigFile, host.readFile); |
| if (error) { |
| return [null, [error], {target}]; |
| } |
| if (userConfig.bazelOptions) { |
| bazelOpts.disableStrictDeps = bazelOpts.disableStrictDeps || |
| userConfig.bazelOptions.disableStrictDeps; |
| bazelOpts.suppressTsconfigOverrideWarnings = |
| bazelOpts.suppressTsconfigOverrideWarnings || |
| userConfig.bazelOptions.suppressTsconfigOverrideWarnings; |
| bazelOpts.tsickle = bazelOpts.tsickle || userConfig.bazelOptions.tsickle; |
| bazelOpts.googmodule = |
| bazelOpts.googmodule || userConfig.bazelOptions.googmodule; |
| } |
| if (!bazelOpts.suppressTsconfigOverrideWarnings) { |
| warnOnOverriddenOptions(userConfig); |
| } |
| } |
| |
| const {options, errors, fileNames} = |
| ts.parseJsonConfigFileContent(config, host, path.dirname(tsconfigFile)); |
| if (errors && errors.length) { |
| return [null, errors, {target}]; |
| } |
| |
| // 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} |
| ]; |
| } |