Files
Angular-Cn/packages/compiler-cli/ngcc/src/sourcemaps/source_file_loader.ts
T

145 lines
5.6 KiB
TypeScript

/**
* @license
* Copyright Google Inc. 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.io/license
*/
import {commentRegex, fromComment, mapFileCommentRegex} from 'convert-source-map';
import {absoluteFrom, AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
import {RawSourceMap} from './raw_source_map';
import {SourceFile} from './source_file';
/**
* This class can be used to load a source file, its associated source map and any upstream sources.
*
* Since a source file might reference (or include) a source map, this class can load those too.
* Since a source map might reference other source files, these are also loaded as needed.
*
* This is done recursively. The result is a "tree" of `SourceFile` objects, each containing
* mappings to other `SourceFile` objects as necessary.
*/
export class SourceFileLoader {
constructor(private fs: FileSystem) {}
/**
* Load a source file, compute its source map, and recursively load any referenced source files.
*
* @param sourcePath The path to the source file to load.
* @param contents The contents of the source file to load (if known).
* The contents may be known because the source file was inlined into a source map.
* If it is not known the contents will be read from the file at the `sourcePath`.
* @param mapAndPath The raw source-map and the path to the source-map file, if known.
* @param previousPaths An internal parameter used for cyclic dependency tracking.
* @returns a SourceFile if the content for one was provided or able to be loaded from disk,
* `null` otherwise.
*/
loadSourceFile(sourcePath: AbsoluteFsPath, contents: string, mapAndPath: MapAndPath): SourceFile;
loadSourceFile(sourcePath: AbsoluteFsPath, contents: string|null): SourceFile|null;
loadSourceFile(sourcePath: AbsoluteFsPath): SourceFile|null;
loadSourceFile(
sourcePath: AbsoluteFsPath, contents: string|null, mapAndPath: null,
previousPaths: AbsoluteFsPath[]): SourceFile|null;
loadSourceFile(
sourcePath: AbsoluteFsPath, contents: string|null = null, mapAndPath: MapAndPath|null = null,
previousPaths: AbsoluteFsPath[] = []): SourceFile|null {
if (contents === null) {
if (!this.fs.exists(sourcePath)) {
return null;
}
// Track source file paths if we have loaded them from disk so that we don't get into an
// infinite recursion
if (previousPaths.includes(sourcePath)) {
throw new Error(`Circular source file mapping dependency: ${
previousPaths.join(' -> ')} -> ${sourcePath}`);
}
previousPaths = previousPaths.concat([sourcePath]);
contents = this.fs.readFile(sourcePath);
}
// If not provided try to load the source map based on the source itself
if (mapAndPath === null) {
mapAndPath = this.loadSourceMap(sourcePath, contents);
}
let map: RawSourceMap|null = null;
let inline = true;
let sources: (SourceFile|null)[] = [];
if (mapAndPath !== null) {
const basePath = mapAndPath.mapPath || sourcePath;
sources = this.processSources(basePath, mapAndPath.map, previousPaths);
map = mapAndPath.map;
inline = mapAndPath.mapPath === null;
}
return new SourceFile(sourcePath, contents, map, inline, sources);
}
/**
* Find the source map associated with the source file whose `sourcePath` and `contents` are
* provided.
*
* Source maps can be inline, as part of a base64 encoded comment, or external as a separate file
* whose path is indicated in a comment or implied from the name of the source file itself.
*/
private loadSourceMap(sourcePath: AbsoluteFsPath, contents: string): MapAndPath|null {
const inline = commentRegex.exec(contents);
if (inline !== null) {
return {map: fromComment(inline.pop()!).sourcemap, mapPath: null};
}
const external = mapFileCommentRegex.exec(contents);
if (external) {
try {
const fileName = external[1] || external[2];
const externalMapPath = this.fs.resolve(this.fs.dirname(sourcePath), fileName);
return {map: this.loadRawSourceMap(externalMapPath), mapPath: externalMapPath};
} catch {
return null;
}
}
const impliedMapPath = absoluteFrom(sourcePath + '.map');
if (this.fs.exists(impliedMapPath)) {
return {map: this.loadRawSourceMap(impliedMapPath), mapPath: impliedMapPath};
}
return null;
}
/**
* Iterate over each of the "sources" for this source file's source map, recursively loading each
* source file and its associated source map.
*/
private processSources(
basePath: AbsoluteFsPath, map: RawSourceMap,
previousPaths: AbsoluteFsPath[]): (SourceFile|null)[] {
const sourceRoot = this.fs.resolve(this.fs.dirname(basePath), map.sourceRoot || '');
return map.sources.map((source, index) => {
const path = this.fs.resolve(sourceRoot, source);
const content = map.sourcesContent && map.sourcesContent[index] || null;
return this.loadSourceFile(path, content, null, previousPaths);
});
}
/**
* Load the source map from the file at `mapPath`, parsing its JSON contents into a `RawSourceMap`
* object.
*/
private loadRawSourceMap(mapPath: AbsoluteFsPath): RawSourceMap {
return JSON.parse(this.fs.readFile(mapPath));
}
}
/** A small helper structure that is returned from `loadSourceMap()`. */
interface MapAndPath {
/** The path to the source map if it was external or `null` if it was inline. */
mapPath: AbsoluteFsPath|null;
/** The raw source map itself. */
map: RawSourceMap;
}