refactor(bazel): extract function to patch fileNameToModuleName on host (#42974)
This commit extracts the patching operation that adds `fileNameToModuleName` to the Angular compiler's `ts.CompilerHost` into a separate function, so that it can be invoked in other compilation flows besides the one outlined in `ngc-wrapped`. This is primarily needed for the xi18n operation in g3. PR Close #42974
This commit is contained in:
parent
d8183c94d4
commit
0af354ce05
|
@ -298,98 +298,8 @@ export function compile({
|
||||||
};
|
};
|
||||||
|
|
||||||
const ngHost = ng.createCompilerHost({options: compilerOpts, tsHost: bazelHost});
|
const ngHost = ng.createCompilerHost({options: compilerOpts, tsHost: bazelHost});
|
||||||
const fileNameToModuleNameCache = new Map<string, string>();
|
patchNgHostWithFileNameToModuleName(
|
||||||
ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath?: string) => {
|
ngHost, compilerOpts, bazelOpts, useManifestPathsAsModuleName);
|
||||||
const cacheKey = `${importedFilePath}:${containingFilePath}`;
|
|
||||||
// Memoize this lookup to avoid expensive re-parses of the same file
|
|
||||||
// When run as a worker, the actual ts.SourceFile is cached
|
|
||||||
// but when we don't run as a worker, there is no cache.
|
|
||||||
// For one example target in g3, we saw a cache hit rate of 7590/7695
|
|
||||||
if (fileNameToModuleNameCache.has(cacheKey)) {
|
|
||||||
return fileNameToModuleNameCache.get(cacheKey);
|
|
||||||
}
|
|
||||||
const result = doFileNameToModuleName(importedFilePath, containingFilePath);
|
|
||||||
fileNameToModuleNameCache.set(cacheKey, result);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
function doFileNameToModuleName(importedFilePath: string, containingFilePath?: string): string {
|
|
||||||
const relativeTargetPath =
|
|
||||||
relativeToRootDirs(importedFilePath, compilerOpts.rootDirs).replace(EXT, '');
|
|
||||||
const manifestTargetPath = `${bazelOpts.workspaceName}/${relativeTargetPath}`;
|
|
||||||
if (useManifestPathsAsModuleName === true) {
|
|
||||||
return manifestTargetPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unless manifest paths are explicitly enforced, we initially check if a module name is
|
|
||||||
// set for the given source file. The compiler host from `@bazel/typescript` sets source
|
|
||||||
// file module names if the compilation targets either UMD or AMD. To ensure that the AMD
|
|
||||||
// module names match, we first consider those.
|
|
||||||
try {
|
|
||||||
const sourceFile = ngHost.getSourceFile(importedFilePath, ts.ScriptTarget.Latest);
|
|
||||||
if (sourceFile && sourceFile.moduleName) {
|
|
||||||
return sourceFile.moduleName;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// File does not exist or parse error. Ignore this case and continue onto the
|
|
||||||
// other methods of resolving the module below.
|
|
||||||
}
|
|
||||||
|
|
||||||
// It can happen that the ViewEngine compiler needs to write an import in a factory file,
|
|
||||||
// and is using an ngsummary file to get the symbols.
|
|
||||||
// The ngsummary comes from an upstream ng_module rule.
|
|
||||||
// The upstream rule based its imports on ngsummary file which was generated from a
|
|
||||||
// metadata.json file that was published to npm in an Angular library.
|
|
||||||
// However, the ngsummary doesn't propagate the 'importAs' from the original metadata.json
|
|
||||||
// so we would normally not be able to supply the correct module name for it.
|
|
||||||
// For example, if the rootDir-relative filePath is
|
|
||||||
// node_modules/@angular/material/toolbar/typings/index
|
|
||||||
// we would supply a module name
|
|
||||||
// @angular/material/toolbar/typings/index
|
|
||||||
// but there is no JavaScript file to load at this path.
|
|
||||||
// This is a workaround for https://github.com/angular/angular/issues/29454
|
|
||||||
if (importedFilePath.indexOf('node_modules') >= 0) {
|
|
||||||
const maybeMetadataFile = importedFilePath.replace(EXT, '') + '.metadata.json';
|
|
||||||
if (fs.existsSync(maybeMetadataFile)) {
|
|
||||||
const moduleName = (JSON.parse(fs.readFileSync(maybeMetadataFile, {encoding: 'utf-8'})) as {
|
|
||||||
importAs: string
|
|
||||||
}).importAs;
|
|
||||||
if (moduleName) {
|
|
||||||
return moduleName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((compilerOpts.module === ts.ModuleKind.UMD || compilerOpts.module === ts.ModuleKind.AMD) &&
|
|
||||||
ngHost.amdModuleName) {
|
|
||||||
return ngHost.amdModuleName({fileName: importedFilePath} as ts.SourceFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no AMD module name has been set for the source file by the `@bazel/typescript` compiler
|
|
||||||
// host, and the target file is not part of a flat module node module package, we use the
|
|
||||||
// following rules (in order):
|
|
||||||
// 1. If target file is part of `node_modules/`, we use the package module name.
|
|
||||||
// 2. If no containing file is specified, or the target file is part of a different
|
|
||||||
// compilation unit, we use a Bazel manifest path. Relative paths are not possible
|
|
||||||
// since we don't have a containing file, and the target file could be located in the
|
|
||||||
// output directory, or in an external Bazel repository.
|
|
||||||
// 3. If both rules above didn't match, we compute a relative path between the source files
|
|
||||||
// since they are part of the same compilation unit.
|
|
||||||
// Note that we don't want to always use (2) because it could mean that compilation outputs
|
|
||||||
// are always leaking Bazel-specific paths, and the output is not self-contained. This could
|
|
||||||
// break `esm2015` or `esm5` output for Angular package release output
|
|
||||||
// Omit the `node_modules` prefix if the module name of an NPM package is requested.
|
|
||||||
if (relativeTargetPath.startsWith(NODE_MODULES)) {
|
|
||||||
return relativeTargetPath.substr(NODE_MODULES.length);
|
|
||||||
} else if (
|
|
||||||
containingFilePath == null || !bazelOpts.compilationTargetSrc.includes(importedFilePath)) {
|
|
||||||
return manifestTargetPath;
|
|
||||||
}
|
|
||||||
const containingFileDir =
|
|
||||||
path.dirname(relativeToRootDirs(containingFilePath, compilerOpts.rootDirs));
|
|
||||||
const relativeImportPath = path.posix.relative(containingFileDir, relativeTargetPath);
|
|
||||||
return relativeImportPath.startsWith('.') ? relativeImportPath : `./${relativeImportPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngHost.toSummaryFileName = (fileName: string, referringSrcFileName: string) => path.posix.join(
|
ngHost.toSummaryFileName = (fileName: string, referringSrcFileName: string) => path.posix.join(
|
||||||
bazelOpts.workspaceName,
|
bazelOpts.workspaceName,
|
||||||
|
@ -553,3 +463,108 @@ function gatherDiagnosticsForInputsOnly(
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
process.exitCode = main(process.argv.slice(2));
|
process.exitCode = main(process.argv.slice(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds support for the optional `fileNameToModuleName` operation to a given `ng.CompilerHost`.
|
||||||
|
*
|
||||||
|
* This is used within `ngc-wrapped` and the Bazel compilation flow, but is exported here to allow
|
||||||
|
* for other consumers of the compiler to access this same logic. For example, the xi18n operation
|
||||||
|
* in g3 configures its own `ng.CompilerHost` which also requires `fileNameToModuleName` to work
|
||||||
|
* correctly.
|
||||||
|
*/
|
||||||
|
export function patchNgHostWithFileNameToModuleName(
|
||||||
|
ngHost: ng.CompilerHost, compilerOpts: ng.CompilerOptions, bazelOpts: BazelOptions,
|
||||||
|
useManifestPathsAsModuleName: boolean): void {
|
||||||
|
const fileNameToModuleNameCache = new Map<string, string>();
|
||||||
|
ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath?: string) => {
|
||||||
|
const cacheKey = `${importedFilePath}:${containingFilePath}`;
|
||||||
|
// Memoize this lookup to avoid expensive re-parses of the same file
|
||||||
|
// When run as a worker, the actual ts.SourceFile is cached
|
||||||
|
// but when we don't run as a worker, there is no cache.
|
||||||
|
// For one example target in g3, we saw a cache hit rate of 7590/7695
|
||||||
|
if (fileNameToModuleNameCache.has(cacheKey)) {
|
||||||
|
return fileNameToModuleNameCache.get(cacheKey);
|
||||||
|
}
|
||||||
|
const result = doFileNameToModuleName(importedFilePath, containingFilePath);
|
||||||
|
fileNameToModuleNameCache.set(cacheKey, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
function doFileNameToModuleName(importedFilePath: string, containingFilePath?: string): string {
|
||||||
|
const relativeTargetPath =
|
||||||
|
relativeToRootDirs(importedFilePath, compilerOpts.rootDirs).replace(EXT, '');
|
||||||
|
const manifestTargetPath = `${bazelOpts.workspaceName}/${relativeTargetPath}`;
|
||||||
|
if (useManifestPathsAsModuleName === true) {
|
||||||
|
return manifestTargetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unless manifest paths are explicitly enforced, we initially check if a module name is
|
||||||
|
// set for the given source file. The compiler host from `@bazel/typescript` sets source
|
||||||
|
// file module names if the compilation targets either UMD or AMD. To ensure that the AMD
|
||||||
|
// module names match, we first consider those.
|
||||||
|
try {
|
||||||
|
const sourceFile = ngHost.getSourceFile(importedFilePath, ts.ScriptTarget.Latest);
|
||||||
|
if (sourceFile && sourceFile.moduleName) {
|
||||||
|
return sourceFile.moduleName;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// File does not exist or parse error. Ignore this case and continue onto the
|
||||||
|
// other methods of resolving the module below.
|
||||||
|
}
|
||||||
|
|
||||||
|
// It can happen that the ViewEngine compiler needs to write an import in a factory file,
|
||||||
|
// and is using an ngsummary file to get the symbols.
|
||||||
|
// The ngsummary comes from an upstream ng_module rule.
|
||||||
|
// The upstream rule based its imports on ngsummary file which was generated from a
|
||||||
|
// metadata.json file that was published to npm in an Angular library.
|
||||||
|
// However, the ngsummary doesn't propagate the 'importAs' from the original metadata.json
|
||||||
|
// so we would normally not be able to supply the correct module name for it.
|
||||||
|
// For example, if the rootDir-relative filePath is
|
||||||
|
// node_modules/@angular/material/toolbar/typings/index
|
||||||
|
// we would supply a module name
|
||||||
|
// @angular/material/toolbar/typings/index
|
||||||
|
// but there is no JavaScript file to load at this path.
|
||||||
|
// This is a workaround for https://github.com/angular/angular/issues/29454
|
||||||
|
if (importedFilePath.indexOf('node_modules') >= 0) {
|
||||||
|
const maybeMetadataFile = importedFilePath.replace(EXT, '') + '.metadata.json';
|
||||||
|
if (fs.existsSync(maybeMetadataFile)) {
|
||||||
|
const moduleName = (JSON.parse(fs.readFileSync(maybeMetadataFile, {encoding: 'utf-8'})) as {
|
||||||
|
importAs: string
|
||||||
|
}).importAs;
|
||||||
|
if (moduleName) {
|
||||||
|
return moduleName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((compilerOpts.module === ts.ModuleKind.UMD || compilerOpts.module === ts.ModuleKind.AMD) &&
|
||||||
|
ngHost.amdModuleName) {
|
||||||
|
return ngHost.amdModuleName({fileName: importedFilePath} as ts.SourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no AMD module name has been set for the source file by the `@bazel/typescript` compiler
|
||||||
|
// host, and the target file is not part of a flat module node module package, we use the
|
||||||
|
// following rules (in order):
|
||||||
|
// 1. If target file is part of `node_modules/`, we use the package module name.
|
||||||
|
// 2. If no containing file is specified, or the target file is part of a different
|
||||||
|
// compilation unit, we use a Bazel manifest path. Relative paths are not possible
|
||||||
|
// since we don't have a containing file, and the target file could be located in the
|
||||||
|
// output directory, or in an external Bazel repository.
|
||||||
|
// 3. If both rules above didn't match, we compute a relative path between the source files
|
||||||
|
// since they are part of the same compilation unit.
|
||||||
|
// Note that we don't want to always use (2) because it could mean that compilation outputs
|
||||||
|
// are always leaking Bazel-specific paths, and the output is not self-contained. This could
|
||||||
|
// break `esm2015` or `esm5` output for Angular package release output
|
||||||
|
// Omit the `node_modules` prefix if the module name of an NPM package is requested.
|
||||||
|
if (relativeTargetPath.startsWith(NODE_MODULES)) {
|
||||||
|
return relativeTargetPath.substr(NODE_MODULES.length);
|
||||||
|
} else if (
|
||||||
|
containingFilePath == null || !bazelOpts.compilationTargetSrc.includes(importedFilePath)) {
|
||||||
|
return manifestTargetPath;
|
||||||
|
}
|
||||||
|
const containingFileDir =
|
||||||
|
path.dirname(relativeToRootDirs(containingFilePath, compilerOpts.rootDirs));
|
||||||
|
const relativeImportPath = path.posix.relative(containingFileDir, relativeTargetPath);
|
||||||
|
return relativeImportPath.startsWith('.') ? relativeImportPath : `./${relativeImportPath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue