Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
import { collectHmrCandidates } from './hmr-candidates';
import { printSourceFileWithMap } from './typescript-printer';

/**
* The modified files count limit for performing component HMR analysis.
Expand All @@ -37,6 +38,7 @@ class AngularCompilationState {
public readonly affectedFiles: ReadonlySet<ts.SourceFile>,
public readonly templateDiagnosticsOptimization: ng.OptimizeFor,
public readonly webWorkerTransform: ts.TransformerFactory<ts.SourceFile>,
public readonly useTypeScriptTranspilation: boolean,
public readonly diagnosticCache = new WeakMap<ts.SourceFile, ts.Diagnostic[]>(),
) {}

Expand Down Expand Up @@ -76,6 +78,10 @@ export class AotCompilation extends AngularCompilation {
const compilerOptions =
compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;

const useTypeScriptTranspilation =
(compilerOptions['_useTypeScriptTranspilation'] as boolean | undefined) ??
!compilerOptions.isolatedModules;

if (compilerOptions.externalRuntimeStyles) {
hostOptions.externalStylesheets ??= new Map();
}
Expand Down Expand Up @@ -209,6 +215,7 @@ export class AotCompilation extends AngularCompilation {
affectedFiles,
affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram,
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
useTypeScriptTranspilation,
this.#state?.diagnosticCache,
);

Expand Down Expand Up @@ -297,14 +304,16 @@ export class AotCompilation extends AngularCompilation {

emitAffectedFiles(): Iterable<EmitFileResult> {
assert(this.#state, 'Angular compilation must be initialized prior to emitting files.');
const { affectedFiles, angularCompiler, compilerHost, typeScriptProgram, webWorkerTransform } =
this.#state;
const {
affectedFiles,
angularCompiler,
compilerHost,
typeScriptProgram,
webWorkerTransform,
useTypeScriptTranspilation,
} = this.#state;
const compilerOptions = typeScriptProgram.getCompilerOptions();
const buildInfoFilename = compilerOptions.tsBuildInfoFile ?? '.tsbuildinfo';
const useTypeScriptTranspilation =
!compilerOptions.isolatedModules ||
!!compilerOptions.sourceMap ||
!!compilerOptions.inlineSourceMap;

const emittedFiles = new Map<ts.SourceFile, EmitFileResult>();
const writeFileCallback: ts.WriteFileCallback = (filename, contents, _a, _b, sourceFiles) => {
Expand Down Expand Up @@ -401,14 +410,31 @@ export class AotCompilation extends AngularCompilation {
'TypeScript transforms should not produce multiple outputs for ' + sourceFile.fileName,
);

let contents;
let contents: string;
if (sourceFile === transformResult.transformed[0]) {
// Use original content if no changes were made
contents = sourceFile.text;
} else {
// Otherwise, print the transformed source file
// Otherwise, print the transformed source file with map if needed
const printer = ts.createPrinter(compilerOptions, transformResult);
contents = printer.printFile(transformResult.transformed[0]);
const printResult = printSourceFileWithMap(
transformResult.transformed[0],
printer,
compilerHost,
compilerOptions,
);

contents = printResult.code;

if (printResult.map) {
if (compilerOptions.inlineSourceMap) {
const base64Map = Buffer.from(printResult.map).toString('base64');
contents += `\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64Map}`;
} else if (compilerOptions.sourceMap) {
const mapFilename = sourceFile.fileName + '.map';
emittedFiles.set(sourceFile, { filename: mapFilename, contents: printResult.map });
}
}
}

angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export async function initialize(request: InitRequest) {
isolatedModules: compilerOptions.isolatedModules,
sourceMap: compilerOptions.sourceMap,
inlineSourceMap: compilerOptions.inlineSourceMap,
_useTypeScriptTranspilation: compilerOptions['_useTypeScriptTranspilation'] as
boolean | undefined,
},
componentResourcesDependencies,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* @license
* Copyright Google LLC 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.dev/license
*/

/**
* @fileoverview Helper functions and typings for utilizing internal TypeScript printer
* and sourcemap generation APIs.
*/

import assert from 'node:assert';
import { dirname } from 'node:path';
import ts from 'typescript';

/**
* Partial interface representing the internal TypeScript `EmitTextWriter` API.
* Used to collect printed text output from the printer.
*/
export interface EmitTextWriter {
write(s: string): void;
rawWrite(s: string): void;
writeLine(force?: boolean): void;
getText(): string;
clear(): void;
isAtStartOfLine(): boolean;
getTextPos(): number;
writeComment(s: string): void;
}

/**
* Partial interface representing the internal TypeScript `SourceMapGenerator` API.
* Used to construct mappings and serialize a sourcemap.
*/
export interface SourceMapGenerator {
getSources(): string[];
addSource(fileName: string): number;
setSourceContent(sourceIndex: number, content: string | null): void;
addMapping(
generatedLine: number,
generatedCharacter: number,
sourceIndex: number,
sourceLine: number,
sourceCharacter: number,
nameIndex?: number,
): void;
toJSON(): object;
toString(): string;
}

/**
* Extended `ts.Printer` interface containing the internal `writeFile` method
* which supports sourcemap generation.
*/
export interface ExtendedPrinter extends ts.Printer {
writeFile(
sourceFile: ts.SourceFile,
writer: EmitTextWriter,
sourceMapGenerator?: SourceMapGenerator,
): void;
}

/**
* Typing structure for internal TypeScript module exports that are not exposed
* in the public `@types/typescript` package.
*/
export interface TypeScriptInternals {
createTextWriter(newLine: string): EmitTextWriter;
createSourceMapGenerator(
host: {
getCurrentDirectory(): string;
getCanonicalFileName(fileName: string): string;
},
file: string,
sourceRoot: string | undefined,
sourcesDirectoryPath: string,
generatorOptions: ts.CompilerOptions,
): SourceMapGenerator;
}

const tsInternals = ts as unknown as TypeScriptInternals;

/**
* Asserts that the required internal TypeScript APIs are present in the currently
* loaded TypeScript module.
*
* @throws {AssertionError} If any required internal API is missing.
*/
export function assertTypeScriptPrinterInternals(): void {
assert(
typeof tsInternals.createTextWriter === 'function',
'TypeScript internal "createTextWriter" is missing.',
);
assert(
typeof tsInternals.createSourceMapGenerator === 'function',
'TypeScript internal "createSourceMapGenerator" is missing.',
);
}

/**
* Result object returned from printing a source file.
*/
export interface PrintResult {
/** The printed source code text. */
code: string;

/** The generated sourcemap JSON string, if sourcemap generation was requested. */
map?: string;
}

/**
* Prints a TypeScript source file AST to a string, optionally generating a sourcemap
* using internal TypeScript APIs.
*
* @param sourceFile The TypeScript AST node representing the file to print.
* @param printer The printer instance to print the file with.
* @param compilerHost The TypeScript compiler host, used for path canonicalization and context.
* @param compilerOptions The compiler options configured for the build target.
* @returns A result containing the printed code and optional sourcemap text.
*/
export function printSourceFileWithMap(
sourceFile: ts.SourceFile,
printer: ts.Printer,
compilerHost: ts.CompilerHost,
compilerOptions: ts.CompilerOptions,
): PrintResult {
const shouldGenerateMap = compilerOptions.sourceMap || compilerOptions.inlineSourceMap;
if (!shouldGenerateMap) {
return { code: printer.printFile(sourceFile) };
}

assertTypeScriptPrinterInternals();

const extendedPrinter = printer as ExtendedPrinter;
assert(
typeof extendedPrinter.writeFile === 'function',
'TypeScript Printer is missing internal "writeFile" method.',
);

const writer = tsInternals.createTextWriter(compilerHost.getNewLine());

const sourceMapGenerator = tsInternals.createSourceMapGenerator(
{
getCurrentDirectory: () => compilerHost.getCurrentDirectory(),
getCanonicalFileName: (fileName) => compilerHost.getCanonicalFileName(fileName),
},
sourceFile.fileName,
compilerOptions.sourceRoot,
dirname(sourceFile.fileName),
compilerOptions,
);

extendedPrinter.writeFile(sourceFile, writer, sourceMapGenerator);

const code = writer.getText();
const map = sourceMapGenerator.toString();

return { code, map };
}
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,8 @@ export function createCompilerPlugin(
),
);
shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
// Isolated modules option ensures safe non-TypeScript transpilation.
// Typescript printing support for sourcemaps is not yet integrated.
useTypeScriptTranspilation =
!initializationResult.compilerOptions.isolatedModules ||
!!initializationResult.compilerOptions.sourceMap ||
!!initializationResult.compilerOptions.inlineSourceMap;
!!initializationResult.compilerOptions['_useTypeScriptTranspilation'];
referencedFiles = initializationResult.referencedFiles;
externalStylesheets = initializationResult.externalStylesheets;
if (initializationResult.templateUpdates) {
Expand Down Expand Up @@ -753,6 +749,12 @@ function createCompilerOptionsTransformer(
preserveSymlinks,
externalRuntimeStyles: pluginOptions.externalRuntimeStyles,
_enableHmr: !!pluginOptions.templateUpdates,
// TypeScript transpilation is forced if:
// - isolatedModules is disabled (TS needs full module types to emit JS).
// - Karma code coverage is active (the coverage instrumentation transformer is Babel-based
// and cannot parse raw TypeScript code; Vitest handles coverage instrumentation downstream).
_useTypeScriptTranspilation:
!compilerOptions.isolatedModules || !!pluginOptions.instrumentForCoverage,
supportTestBed: !!pluginOptions.includeTestMetadata,
supportJitMode: !!pluginOptions.includeTestMetadata,
};
Expand Down
Loading