源代码映射
Parcel 使用 @parcel/source-map
包来处理源代码映射,以确保在跨插件和 Parcel 核心操作源代码映射时,性能和可靠性。该库从头开始使用 Rust 编写,与之前的基于 JavaScript 的实现相比,性能提高了 20 倍。性能的提升主要归功于数据结构和缓存源代码映射方式的优化。
如何使用该库
#要使用 @parcel/source-map
,请创建一个导出的 SourceMap
类的实例,您可以在该实例上调用各种函数来添加和编辑源代码映射。应将 projectRoot
目录路径作为参数传递。源代码映射中的所有路径都将转换为相对于此路径的路径。
以下是一个涵盖所有将映射添加到 SourceMap
实例的方法的示例
import SourceMap from '@parcel/source-map';
let sourcemap = new SourceMap(projectRoot);
// Each function that adds mappings has optional offset arguments.
// These can be used to offset the generated mappings by a certain amount.
let lineOffset = 0;
let columnOffset = 0;
// Add indexed mappings
// These are mappings that can sometimes be extracted from a library even before they get converted into VLQ Mappings
sourcemap.addIndexedMappings(
[
{
generated: {
// line index starts at 1
line: 1,
// column index starts at 0
column: 4,
},
original: {
// line index starts at 1
line: 1,
// column index starts at 0
column: 4,
},
source: "index.js",
// Name is optional
name: "A",
},
],
lineOffset,
columnOffset
);
// Add vlq mappings. This is what would be outputted into a vlq encoded source map
sourcemap.addVLQMap(
{
file: "min.js",
names: ["bar", "baz", "n"],
sources: ["one.js", "two.js"],
sourceRoot: "/the/root",
mappings:
"CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA",
},
lineOffset,
columnOffset
);
// Source maps can be serialized to buffers, which is what we use for caching in Parcel.
// You can instantiate a SourceMap with these buffer values by passing it to the constructor
let map = new SourceMap(projectRoot, mapBuffer);
// You can also add a buffer to an existing source map using the addBuffer method.
sourcemap.addBuffer(originalMapBuffer, lineOffset);
// One SourceMap object may be added to another using the addSourceMap method.
sourcemap.addSourceMap(map, lineOffset);
转换/操作
#如果您的插件执行任何代码操作,您应该确保它创建了指向原始源代码的正确映射,以保证我们在捆绑过程结束时仍然能够创建准确的源代码映射。您应该在 转换器 插件中转换结束时返回一个 SourceMap
实例。
我们还提供了来自先前转换的源代码映射,以确保您映射到原始源代码,而不仅仅是先前转换的输出。如果编译器没有方法传递输入源代码映射,您可以使用 SourceMap
的 extends
方法将原始映射映射到已编译的映射。
在转换器插件的 parse
、transform
和 generate
函数中传递的 asset
值包含一个名为 getMap()
和 getMapBuffer()
的函数。这些函数可用于获取 SourceMap 实例 (getMap()
) 和缓存的 SourceMap 缓冲区 (getMapBuffer()
)。
您可以在转换器中的任何这些步骤中自由操作源代码映射,只要您确保在 generate
中返回的源代码映射正确映射到原始源文件即可。
以下是如何在转换器插件中操作源代码映射的示例
import {Transformer} from '@parcel/plugin';
import SourceMap from '@parcel/source-map';
export default new Transformer({
// ...
async generate({asset, ast, resolve, options}) {
let compilationResult = someCompiler(await asset.getAST());
let map = null;
if (compilationResult.map) {
// If the compilationResult returned a map we convert
// it to a Parcel SourceMap instance.
map = new SourceMap(options.projectRoot);
// The compiler returned a full, encoded sourcemap with vlq mappings.
// Some compilers might have the possibility of returning
// indexedMappings which might improve performance (like Babel does).
// In general, every compiler is able to return rawMappings, so
// it's always a safe bet to use this.
map.addVLQMap(compilationResult.map);
// We get the original source map from the asset to extend our mappings
// on top of it. This ensures we are mapping to the original source
// instead of the previous transformation.
let originalMap = await asset.getMap();
if (originalMap) {
// The `extends` function uses the provided map to remap the original
// source positions of the map it is called on. In this case, the
// original source positions of `map` get remapped to the positions
// in `originalMap`.
map.extends(originalMap);
}
}
return {
code: compilationResult.code,
map,
};
},
});
如果您的编译器支持传递现有源代码映射的选项,这可能会导致比使用先前示例中的方法更准确的源代码映射。
此工作原理的示例
import {Transformer} from '@parcel/plugin';
import SourceMap from '@parcel/source-map';
export default new Transformer({
// ...
async generate({asset, ast, resolve, options}) {
// Get the original map from the asset.
let originalMap = await asset.getMap();
let compilationResult = someCompiler(await asset.getAST(), {
// Pass the VLQ encoded version of the originalMap to the compiler.
originalMap: originalMap.toVLQ(),
});
// In this case the compiler is responsible for mapping to the original
// positions provided in the originalMap, so we can just convert it to
// a Parcel SourceMap and return it.
let map = new SourceMap(options.projectRoot);
if (compilationResult.map) {
map.addVLQMap(compilationResult.map);
}
return {
code: compilationResult.code,
map,
};
},
});
在打包器中连接源代码映射
#如果您正在编写自定义打包器,您有责任在打包时连接所有资产的源代码映射。这是通过创建一个新的 SourceMap
实例并使用 addSourceMap(map, lineOffset)
函数向其添加新映射来完成的。lineOffset
应该等于资产输出开始处的行索引。
以下是如何执行此操作的示例
import {Packager} from '@parcel/plugin';
import SourceMap from '@parcel/source-map';
export default new Packager({
async package({bundle, options}) {
// Read content and source maps for each asset in the bundle.
let promises = [];
bundle.traverseAssets(asset => {
promises.push(Promise.all([
asset.getCode(),
asset.getMap()
]);
});
let results = await Promise.all(promises);
// Instantiate a string to hold the bundle contents, and
// a SourceMap to hold the combined bundle source map.
let contents = '';
let map = new SourceMap(options.projectRoot);
let lineOffset = 0;
// Add the contents of each asset.
for (let [code, map] of assets) {
contents += code + '\n';
// Add the source map if the asset has one, and offset
// it by the number of lines in the bundle so far.
if (map) {
map.addSourceMap(map, lineOffset);
}
// Add the number of lines in this asset.
lineOffset += countLines(code) + 1;
}
// Return the contents and map.
return {contents, map};
},
});
连接 AST
#如果您正在连接 AST 而不是源内容,那么您已经将源代码映射嵌入到 AST 中,您可以使用它来生成最终的源代码映射。但是,您必须确保在编辑 AST 节点时这些映射保持完整。如果您进行大量修改,有时这可能非常具有挑战性。
此工作原理的示例
import {Packager} from '@parcel/plugin';
import SourceMap from '@parcel/source-map';
export default new Packager({
async package({bundle, options}) {
// Do the AST concatenation and return the compiled result
let compilationResult = concatAndCompile(bundle);
// Create the final packaged sourcemap
let map = new SourceMap(options.projectRoot);
if (compilationResult.map) {
map.addVLQMap(compilationResult.map);
}
// Return the compiled code and map
return {
code: compilationResult.code,
map,
};
},
});
在优化器中后处理源代码映射
#在优化器中使用源代码映射与在转换器中使用源代码映射相同。您获得一个文件作为输入,并应返回相同的文件作为输出,但经过优化。
优化器唯一的区别是映射不是作为资产的一部分提供,而是作为单独的参数/选项提供,如以下代码片段所示。与往常一样,映射是 SourceMap
类的实例。
import {Optimizer} from '@parcel/plugin';
export default new Optimizer({
// The contents and map are passed separately
async optimize({bundle, contents, map}) {
return {contents, map};
}
});
诊断问题
#如果您遇到不正确的映射并希望调试这些映射,我们已经构建了可以帮助您诊断这些问题的工具。通过运行 @parcel/reporter-sourcemap-visualiser
报告器,Parcel 会创建一个 sourcemap-info.json
文件,其中包含所有必要的信息,以可视化所有映射和源内容。
要启用它,请使用 --reporter
选项,或将其添加到您的 .parcelrc
中。
parcel build src/index.js --reporter @parcel/reporter-sourcemap-visualiser
报告器创建 sourcemap-info.json
文件后,您可以将其上传到 源代码映射可视化器。
API
#SourceMap source-map/src/SourceMap.js:8
interface SourceMap {
constructor(projectRoot: string, buffer?: Buffer): void,
构造一个 SourceMap 实例
projectRoot
: 项目的根目录,这是为了确保所有源路径都相对于此路径
libraryVersion(): string,
static generateEmptyMap(v: GenerateEmptyMapOptions): SourceMap,
从提供的 fileName 和 sourceContent 生成一个空映射
sourceName
: 源文件的路径sourceContent
: 源文件的内容lineOffset
: 添加到每个映射的 sourceLine 索引的偏移量
addEmptyMap(sourceName: string, sourceContent: string, lineOffset: number): SourceMap,
从提供的 fileName 和 sourceContent 生成一个空映射
sourceName
: 源文件的路径sourceContent
: 源文件的内容lineOffset
: 添加到每个映射的 sourceLine 索引的偏移量
addVLQMap(map: VLQMap, lineOffset: number, columnOffset: number): SourceMap,
将原始 VLQ 映射追加到源代码映射
addSourceMap(sourcemap: SourceMap, lineOffset: number): SourceMap,
将另一个源代码映射实例追加到此源代码映射
buffer
: 应该追加到此源代码映射的源代码映射缓冲区lineOffset
: 添加到每个映射的 sourceLine 索引的偏移量
addBuffer(buffer: Buffer, lineOffset: number): SourceMap,
将缓冲区追加到此源代码映射 注意:缓冲区应由此库生成
参数buffer
: 应该追加到此源代码映射的源代码映射缓冲区lineOffset
: 添加到每个映射的 sourceLine 索引的偏移量
addIndexedMapping(mapping: IndexedMapping<string>, lineOffset?: number, columnOffset?: number): void,
将 Mapping 对象追加到此源代码映射 注意:行号从 1 开始,因为 mozilla 的 source-map 库
mapping
: 应该追加到此源代码映射的映射lineOffset
: 添加到每个映射的 sourceLine 索引的偏移量columnOffset
: 添加到每个映射的 sourceColumn 索引的偏移量
_indexedMappingsToInt32Array(mappings: Array<IndexedMapping<string>>, lineOffset?: number, columnOffset?: number): Int32Array,
addIndexedMappings(mappings: Array<IndexedMapping<string>>, lineOffset?: number, columnOffset?: number): SourceMap,
将 Mapping 对象数组追加到此源代码映射 这在库提供非序列化映射时提高性能很有用
注意:这只有在它们延迟生成序列化映射时才更快 注意:行号从 1 开始,因为 mozilla 的 source-map 库
mappings
: 映射对象数组lineOffset
: 添加到每个映射的 sourceLine 索引的偏移量columnOffset
: 添加到每个映射的 sourceColumn 索引的偏移量
addName(name: string): number,
将名称追加到源代码映射
name
: 应该追加到名称数组的名称
addNames(names: Array<string>): Array<number>,
将名称数组追加到源代码映射的名称数组
names
: 要添加到源代码映射的名称数组
addSource(source: string): number,
将源追加到源代码映射的源数组
source
: 应该追加到源数组的文件路径
addSources(sources: Array<string>): Array<number>,
将源数组追加到源代码映射的源数组
sources
: 应该追加到源数组的文件路径数组
getSourceIndex(source: string): number,
获取源数组中特定源文件文件路径的索引
source
: 源文件的路径
getSource(index: number): string,
获取源数组中特定索引的源文件文件路径
index
: 源在源数组中的索引
getSources(): Array<string>,
获取所有源的列表
setSourceContent(sourceName: string, sourceContent: string): void,
为特定文件设置 sourceContent 这是可选的,仅建议用于我们无法在序列化源代码映射时读取的文件
sourceName
: 源文件的路径sourceContent
: 源文件的内容
getSourceContent(sourceName: string): string | null,
获取源文件的原始内容,如果它作为源代码映射的一部分内联
sourceName
: 文件名
getSourcesContent(): Array<string | null>,
获取所有源的列表
getSourcesContentMap(): {
[key: string]: string | null
},
获取源及其对应源内容的映射
getNameIndex(name: string): number,
获取名称数组中特定名称的索引
name
: 您要查找其索引的名称
getName(index: number): string,
获取名称数组中特定索引的名称
index
: 名称在名称数组中的索引
getNames(): Array<string>,
获取所有名称的列表
getMappings(): Array<IndexedMapping<number>>,
获取所有映射的列表
indexedMappingToStringMapping(mapping: ?IndexedMapping<number>): ?IndexedMapping<string>,
将使用名称和源索引的 Mapping 对象转换为名称和源的实际值
注意:这仅用于内部,不应在外部使用,并且最终可能会在 C++ 中直接处理以提高性能
index
: 应该转换为基于字符串的 Mapping 的 Mapping
extends(buffer: Buffer | SourceMap): SourceMap,
将此映射中的原始位置重新映射到提供的映射中的位置
这是通过在提供的映射中找到最接近此映射的原始映射的生成映射,并将这些映射重新映射为提供的映射的原始映射来完成的。
buffer
: 导出 SourceMap 作为缓冲区
getMap(): ParsedMap,
返回一个包含映射、源和名称的对象 这仅应用于测试、调试和可视化源代码映射
注意:这是一个相当缓慢的操作
findClosestMapping(line: number, column: number): ?IndexedMapping<string>,
在源代码映射中搜索并返回一个接近提供的生成行和列的映射
line
: 生成代码中的行(从 1 开始)column
: 生成代码中的列(从 0 开始)
offsetLines(line: number, lineOffset: number): ?IndexedMapping<string>,
从特定位置偏移映射行
line
: 生成代码中的行(从 1 开始)lineOffset
: 要偏移映射的行数
offsetColumns(line: number, column: number, columnOffset: number): ?IndexedMapping<string>,
从特定位置偏移映射列
line
: 生成代码中的行(从 1 开始)column
: 生成代码中的列(从 0 开始)columnOffset
: 要偏移映射的列数
toBuffer(): Buffer,
返回表示此源代码映射的缓冲区,用于缓存
toVLQ(): VLQMap,
返回使用 VLQ 映射序列化的映射
delete(): void,
必须在 SourceMap 生命周期结束时调用的函数,以确保所有内存和本机绑定都被释放
stringify(options: SourceMapStringifyOptions): Promise<string | VLQMap>,
返回序列化的映射
options
: 用于格式化序列化映射的选项
}
被引用
BaseAsset、BundleResult、GenerateOutput、MutableAsset、Optimizer、Packager、TransformerResultMappingPosition source-map/src/types.js:2
type MappingPosition = {|
line: number,
column: number,
|}
被引用
索引映射IndexedMapping source-map/src/types.js:7
type IndexedMapping<T> = {
generated: MappingPosition,
original?: MappingPosition,
source?: T,
name?: T,
}
被引用
ParsedMap、SourceMapParsedMap source-map/src/types.js:15
type ParsedMap = {|
sources: Array<string>,
names: Array<string>,
mappings: Array<IndexedMapping<number>>,
sourcesContent: Array<string | null>,
|}
被引用
SourceMapVLQMap source-map/src/types.js:22
type VLQMap = {
+sources: $ReadOnlyArray<string>,
+sourcesContent?: $ReadOnlyArray<string | null>,
+names: $ReadOnlyArray<string>,
+mappings: string,
+version?: number,
+file?: string,
+sourceRoot?: string,
}
被引用
SourceMapSourceMapStringifyOptions source-map/src/types.js:33
type SourceMapStringifyOptions = {
file?: string,
sourceRoot?: string,
inlineSources?: boolean,
fs?: {
readFile(path: string, encoding: string): Promise<string>,
...
},
format?: 'inline' | 'string' | 'object',
}
被引用
SourceMapGenerateEmptyMapOptions source-map/src/types.js:46
type GenerateEmptyMapOptions = {
projectRoot: string,
sourceName: string,
sourceContent: string,
lineOffset?: number,
}