Wire up Angular 9 templates as a ts_library plugin PiperOrigin-RevId: 297183761
diff --git a/internal/common/compilation.bzl b/internal/common/compilation.bzl index a514e9e..8e36bfa 100644 --- a/internal/common/compilation.bzl +++ b/internal/common/compilation.bzl
@@ -156,7 +156,7 @@ # Temporary until all imports of ngfactory/ngsummary files are removed # TODO(alexeagle): clean up after Ivy launch - if getattr(ctx, "compile_angular_templates", False): + if getattr(ctx.attr, "use_angular_plugin", False): closure_js_files += [ctx.actions.declare_file(basename + ".ngfactory.mjs")] closure_js_files += [ctx.actions.declare_file(basename + ".ngsummary.mjs")] @@ -166,9 +166,11 @@ # Temporary until all imports of ngfactory/ngsummary files are removed # TODO(alexeagle): clean up after Ivy launch - if getattr(ctx, "compile_angular_templates", False): + if getattr(ctx.attr, "use_angular_plugin", False): devmode_js_files += [ctx.actions.declare_file(basename + ".ngfactory.js")] devmode_js_files += [ctx.actions.declare_file(basename + ".ngsummary.js")] + declaration_files += [ctx.actions.declare_file(basename + ".ngfactory.d.ts")] + declaration_files += [ctx.actions.declare_file(basename + ".ngsummary.d.ts")] return struct( closure_js = closure_js_files, devmode_js = devmode_js_files, @@ -336,7 +338,7 @@ replay_params = None if has_sources: - inputs = compilation_inputs + [tsconfig] + inputs = compilation_inputs + [tsconfig] + getattr(ctx.files, "angular_assets", []) replay_params = compile_action( ctx, inputs, @@ -382,7 +384,7 @@ )) devmode_compile_action( ctx, - compilation_inputs + [tsconfig_json_es5], + compilation_inputs + [tsconfig_json_es5] + getattr(ctx.files, "angular_assets", []), outputs, tsconfig_json_es5, node_profile_args,
diff --git a/internal/common/tsconfig.bzl b/internal/common/tsconfig.bzl index 14b4df5..d30b1c5 100644 --- a/internal/common/tsconfig.bzl +++ b/internal/common/tsconfig.bzl
@@ -159,8 +159,18 @@ "expectedDiagnostics": getattr(ctx.attr, "expected_diagnostics", []), } - if hasattr(ctx.attr, "compile_angular_templates") and ctx.attr.compile_angular_templates: - bazel_options["compileAngularTemplates"] = True + if getattr(ctx.attr, "use_angular_plugin", False): + bazel_options["angularCompilerOptions"] = { + # Needed for back-compat with explicit AOT bootstrap + # which has imports from generated .ngfactory files + "generateNgFactoryShims": True, + # Needed for back-compat with AOT tests which import the + # .ngsummary files + "generateNgSummaryShims": True, + # Bazel expects output files will always be produced + "allowEmptyCodegenFiles": True, + "assets": [a.path for a in getattr(ctx.files, "angular_assets", [])], + } if disable_strict_deps: bazel_options["disableStrictDeps"] = disable_strict_deps
diff --git a/internal/tsc_wrapped/plugin_api.ts b/internal/tsc_wrapped/plugin_api.ts index e09954d..2bcfa21 100644 --- a/internal/tsc_wrapped/plugin_api.ts +++ b/internal/tsc_wrapped/plugin_api.ts
@@ -17,8 +17,8 @@ /** * @fileoverview - * Provides APIs for extending TypeScript. - * Based on the LanguageService plugin API in TS 2.3 + * Provides APIs for extending TypeScript command-line compiler. + * It's roughly analogous to how the Language Service allows plugins. */ import * as ts from 'typescript'; @@ -29,13 +29,6 @@ * In vanilla tsc, these are the ts.ParsedCommandLine#fileNames */ inputFiles: ReadonlyArray<string>; - - /** - * A helper the transformer can use when generating new import statements - * @param fileName the absolute path to the file as referenced in the ts.Program - * @return a string suitable for use in an import statement - */ - fileNameToModuleId: (fileName: string) => string; } /** @@ -48,10 +41,10 @@ * * The methods on the plugin will be called in the order shown below: * - wrapHost to intercept CompilerHost methods and contribute inputFiles to the program - * - wrap to intercept diagnostics requests on the program + * - setupCompilation to capture the ts.Program * - createTransformers once it's time to emit */ -export interface TscPlugin { +export interface EmitPlugin { /** * Allow plugins to add additional files to the program. * For example, Angular creates ngsummary and ngfactory files. @@ -59,27 +52,19 @@ * @param inputFiles the files that were part of the original program * @param compilerHost: the original host (likely a ts.CompilerHost) that we can delegate to */ - wrapHost?(inputFiles: string[], compilerHost: PluginCompilerHost): PluginCompilerHost; + wrapHost?(compilerHost: ts.CompilerHost, inputFiles: string[], options: ts.CompilerOptions): PluginCompilerHost; - /** - * Same API as ts.LanguageService: allow the plugin to contribute additional - * diagnostics - * IMPORTANT: plugins must propagate the diagnostics from the original program. - * Execution of plugins is not additive; only the result from the top-most - * wrapped Program is used. - */ - wrap(p: ts.Program, config?: {}, host?: ts.CompilerHost): ts.Program; + setupCompilation(program: ts.Program, oldProgram?: ts.Program): void; + getNextProgram?(): ts.Program; + /** * Allow plugins to contribute additional TypeScript CustomTransformers. * These can modify the TS AST, JS AST, or .d.ts output AST. */ - createTransformers?(host: PluginCompilerHost): ts.CustomTransformers; + createTransformers(): ts.CustomTransformers; } -// TODO(alexeagle): this should be unioned with tsserverlibrary.PluginModule -export type Plugin = TscPlugin; - /** * The proxy design pattern, allowing us to customize behavior of the delegate * object.
diff --git a/internal/tsc_wrapped/tsc_wrapped.ts b/internal/tsc_wrapped/tsc_wrapped.ts index 077a948..71ca1ee 100644 --- a/internal/tsc_wrapped/tsc_wrapped.ts +++ b/internal/tsc_wrapped/tsc_wrapped.ts
@@ -10,7 +10,7 @@ import * as bazelDiagnostics from './diagnostics'; import {constructManifest} from './manifest'; import * as perfTrace from './perf_trace'; -import {DiagnosticPlugin, PluginCompilerHost, TscPlugin} from './plugin_api'; +import {DiagnosticPlugin, PluginCompilerHost, EmitPlugin} from './plugin_api'; import {Plugin as StrictDepsPlugin} from './strict_deps'; import {BazelOptions, parseTsconfig, resolveNormalizedPath} from './tsconfig'; import {debug, log, runAsWorker, runWorkerLoop} from './worker'; @@ -62,11 +62,8 @@ */ export function gatherDiagnostics( options: ts.CompilerOptions, bazelOpts: BazelOptions, program: ts.Program, - disabledTsetseRules: string[], angularPlugin?: TscPlugin, + disabledTsetseRules: string[], plugins: DiagnosticPlugin[] = []): ts.Diagnostic[] { - if (angularPlugin) { - program = angularPlugin.wrap(program); - } const diagnostics: ts.Diagnostic[] = []; perfTrace.wrap('type checking', () => { @@ -218,13 +215,7 @@ throw new Error( 'Impossible state: if parseTsconfig returns no errors, then parsed should be non-null'); } - const { - options, - bazelOpts, - files, - disabledTsetseRules, - angularCompilerOptions - } = parsed; + const {options, bazelOpts, files, disabledTsetseRules} = parsed; let sourceFiles: string[] = []; if (bazelOpts.isJsTranspilation) { @@ -259,8 +250,7 @@ const perfTracePath = bazelOpts.perfTracePath; if (!perfTracePath) { const {diagnostics} = createProgramAndEmit( - fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules, - angularCompilerOptions); + fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules); if (diagnostics.length > 0) { console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics)); return false; @@ -271,8 +261,7 @@ log('Writing trace to', perfTracePath); const success = perfTrace.wrap('runOneBuild', () => { const {diagnostics} = createProgramAndEmit( - fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules, - angularCompilerOptions); + fileLoader, options, bazelOpts, sourceFiles, disabledTsetseRules); if (diagnostics.length > 0) { console.error(bazelDiagnostics.format(bazelOpts.target, diagnostics)); return false; @@ -321,8 +310,7 @@ */ export function createProgramAndEmit( fileLoader: FileLoader, options: ts.CompilerOptions, - bazelOpts: BazelOptions, files: string[], disabledTsetseRules: string[], - angularCompilerOptions?: {[key: string]: unknown}): + bazelOpts: BazelOptions, files: string[], disabledTsetseRules: string[]): {program?: ts.Program, diagnostics: ts.Diagnostic[]} { // Beware! createProgramAndEmit must not print to console, nor exit etc. // Handle errors by reporting and returning diagnostics. @@ -336,44 +324,52 @@ const moduleResolver = bazelOpts.isJsTranspilation ? makeJsModuleResolver(bazelOpts.workspaceName) : ts.resolveModuleName; + + // Files which should be allowed to be read, but aren't TypeScript code + const assets: string[] = []; + if (bazelOpts.angularCompilerOptions) { + if (bazelOpts.angularCompilerOptions.assets) { + assets.push(...bazelOpts.angularCompilerOptions.assets); + } + } + const tsickleCompilerHost = new CompilerHost( - files, options, bazelOpts, compilerHostDelegate, fileLoader, + [...files, ...assets], options, bazelOpts, compilerHostDelegate, fileLoader, moduleResolver); let compilerHost: PluginCompilerHost = tsickleCompilerHost; const diagnosticPlugins: DiagnosticPlugin[] = []; - let angularPlugin: TscPlugin|undefined; - if (bazelOpts.compileAngularTemplates) { + let angularPlugin: EmitPlugin&DiagnosticPlugin|undefined; + if (bazelOpts.angularCompilerOptions) { try { - const ngOptions = angularCompilerOptions || {}; + const ngOptions = bazelOpts.angularCompilerOptions; // Add the rootDir setting to the options passed to NgTscPlugin. // Required so that synthetic files added to the rootFiles in the program // can be given absolute paths, just as we do in tsconfig.ts, matching // the behavior in TypeScript's tsconfig parsing logic. ngOptions['rootDir'] = options.rootDir; - // Dynamically load the Angular compiler installed as a peerDep - const ngtsc = require('@angular/compiler-cli'); + let angularPluginEntryPoint = '@angular/compiler-cli'; - // TODO(alexeagle): re-enable after Angular API changes land - // See https://github.com/angular/angular/pull/34792 - // and pending CL/289493608 - // By commenting this out, we allow Angular caretaker to sync changes from - // GitHub without having to coordinate any Piper patches in the same CL. - // angularPlugin = new ngtsc.NgTscPlugin(ngOptions); + // Dynamically load the Angular compiler. + // Lazy load, so that code that does not use the plugin doesn't even + // have to spend the time to parse and load the plugin's source. + // + // tslint:disable-next-line:no-require-imports + const ngtsc = require(angularPluginEntryPoint); + angularPlugin = new ngtsc.NgTscPlugin(ngOptions); + diagnosticPlugins.push(angularPlugin!); } catch (e) { return { diagnostics: [errorDiag( - 'when using `ts_library(compile_angular_templates=True)`, ' + + 'when using `ts_library(use_angular_plugin=True)`, ' + `you must install @angular/compiler-cli (was: ${e})`)] }; } - // Wrap host only needed until after Ivy cleanup - // TODO(alexeagle): remove after ngsummary and ngfactory files eliminated - if (angularPlugin) { - compilerHost = angularPlugin.wrapHost!(files, compilerHost); - } + // Wrap host so that Ivy compiler can add a file to it (has synthetic types for checking templates) + // TODO(arick): remove after ngsummary and ngfactory files eliminated + compilerHost = angularPlugin!.wrapHost!(compilerHost, files, options); } @@ -384,6 +380,16 @@ compilerHost.inputFiles, options, compilerHost, oldProgram)); cache.putProgram(bazelOpts.target, program); + let transformers: ts.CustomTransformers = { + before: [], + after: [], + afterDeclarations: [], + }; + if (angularPlugin) { + angularPlugin.setupCompilation(program); + transformers = angularPlugin.createTransformers(); + } + for (const pluginConfig of options['plugins'] as ts.PluginImport[] || []) { if (pluginConfig.name === 'ts-lit-plugin') { const litTscPlugin = @@ -402,8 +408,7 @@ // messages refer to the original source. After any subsequent passes // (decorator downleveling or tsickle) we do not type check. let diagnostics = gatherDiagnostics( - options, bazelOpts, program, disabledTsetseRules, angularPlugin, - diagnosticPlugins); + options, bazelOpts, program, disabledTsetseRules, diagnosticPlugins); if (!expectDiagnosticsWhitelist.length || expectDiagnosticsWhitelist.some(p => bazelOpts.target.startsWith(p))) { diagnostics = bazelDiagnostics.filterExpected( @@ -415,33 +420,43 @@ 'expected_diagnostics, but got ' + bazelOpts.target)); } + // The Angular plugin creates a new program with template type-check information + // This consumes (destroys) the old program so it's not suitable for re-use anymore + // Ask Angular to give us the updated reusable program. + if (angularPlugin) { + cache.putProgram(bazelOpts.target, angularPlugin.getNextProgram!()); + } + if (diagnostics.length > 0) { debug('compilation failed at', new Error().stack!); return {program, diagnostics}; } } + // Angular might have added files like input.ngfactory.ts or input.ngsummary.ts + // and these need to be emitted. + // TODO(arick): remove after Ivy is enabled and ngsummary/ngfactory files no longer needed + function isAngularFile(sf: ts.SourceFile) { + if (!/\.ng(factory|summary)\.ts$/.test(sf.fileName)) { + return false; + } + return isCompilationTarget(bazelOpts, { + fileName: sf.fileName.slice(0, /*'.ngfactory|ngsummary.ts'.length*/ -13) + '.ts' + } as ts.SourceFile); + } + const compilationTargets = program.getSourceFiles().filter( - fileName => isCompilationTarget(bazelOpts, fileName)); + sf => isCompilationTarget(bazelOpts, sf) || isAngularFile(sf)); let diagnostics: ts.Diagnostic[] = []; let useTsickleEmit = bazelOpts.tsickle; - let transforms: ts.CustomTransformers = { - before: [], - after: [], - afterDeclarations: [], - }; - - if (angularPlugin) { - transforms = angularPlugin.createTransformers!(compilerHost); - } if (useTsickleEmit) { diagnostics = emitWithTsickle( program, tsickleCompilerHost, compilationTargets, options, bazelOpts, - transforms); + transformers); } else { - diagnostics = emitWithTypescript(program, compilationTargets, transforms); + diagnostics = emitWithTypescript(program, compilationTargets, transformers); } if (diagnostics.length > 0) {
diff --git a/internal/tsc_wrapped/tsconfig.ts b/internal/tsc_wrapped/tsconfig.ts index 13851ca..be964eb 100644 --- a/internal/tsc_wrapped/tsconfig.ts +++ b/internal/tsc_wrapped/tsconfig.ts
@@ -179,9 +179,17 @@ hasImplementation?: boolean; /** - * Enable the Angular ngtsc plugin. + * If present, run the Angular ngtsc plugin with the given options. */ - compileAngularTemplates?: boolean; + angularCompilerOptions?: { + [k: string]: any, + assets: string[], + // Ideally we would + // import {AngularCompilerOptions} from '@angular/compiler-cli'; + // and the type would be AngularCompilerOptions&{assets: string[]}; + // but we don't want a dependency from @bazel/typescript to @angular/compiler-cli + // as it's conceptually cyclical. + }; /** * Override for ECMAScript target language level to use for devmode. @@ -372,6 +380,10 @@ 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: string[] = []; for (const pluginConfig of options['plugins'] as PluginImportWithConfig[] ||