源代码映射

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 实例。

我们还提供了来自先前转换的源代码映射,以确保您映射到原始源代码,而不仅仅是先前转换的输出。如果编译器没有方法传递输入源代码映射,您可以使用 SourceMapextends 方法将原始映射映射到已编译的映射。

在转换器插件的 parsetransformgenerate 函数中传递的 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,

将此映射中的原始位置重新映射到提供的映射中的位置
这是通过在提供的映射中找到最接近此映射的原始映射的生成映射,并将这些映射重新映射为提供的映射的原始映射来完成的。

参数
  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: 用于格式化序列化映射的选项
}
被引用
BaseAssetBundleResultGenerateOutputMutableAssetOptimizerPackagerTransformerResult

MappingPosition 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,
}
被引用
ParsedMapSourceMap

ParsedMap source-map/src/types.js:15

type ParsedMap = {|
  sources: Array<string>,
  names: Array<string>,
  mappings: Array<IndexedMapping<number>>,
  sourcesContent: Array<string | null>,
|}
被引用
SourceMap

VLQMap 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,
}
被引用
SourceMap

SourceMapStringifyOptions 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',
}
被引用
SourceMap

GenerateEmptyMapOptions source-map/src/types.js:46

type GenerateEmptyMapOptions = {
  projectRoot: string,
  sourceName: string,
  sourceContent: string,
  lineOffset?: number,
}
被引用
SourceMap