源映射

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);

// 每个添加映射的函数都有可选的偏移参数。
// 这些可用于按特定数量偏移生成的映射。
let lineOffset = 0;
let columnOffset = 0;

// 添加索引映射
// 这些是有时可以从库中提取的映射,即使在转换为 VLQ 映射之前
sourcemap.addIndexedMappings(
[
{
generated: {
// 行索引从 1 开始
line: 1,
// 列索引从 0 开始
column: 4,
},
original: {
// 行索引从 1 开始
line: 1,
// 列索引从 0 开始
column: 4,
},
source: "index.js",
// 名称是可选的
name: "A",
},
],
lineOffset,
columnOffset
);

// 添加 VLQ 映射。这是将输出到 VLQ 编码源映射中的内容
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
);

// 源映射可以序列化为缓冲区,这是我们在 Parcel 中用于缓存的方式
// 通过将缓冲区值传递给构造函数,可以实例化带有这些缓冲区值的 SourceMap
let map = new SourceMap(projectRoot, mapBuffer);

// 还可以使用 addBuffer 方法将缓冲区添加到现有的源映射中。
sourcemap.addBuffer(originalMapBuffer, lineOffset);

// 一个 SourceMap 对象可以使用 addSourceMap 方法添加到另一个中。
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) {
// 如果编译结果返回了映射,我们将其转换为 Parcel SourceMap 实例。
map = new SourceMap(options.projectRoot);

// 编译器返回了带有 VLQ 映射的完整编码源映射。
// 一些编译器可能返回索引映射,这可能会提高性能(如 Babel)。
// 通常,每个编译器都能返回原始映射,所以使用这个总是安全的。
map.addVLQMap(compilationResult.map);

// 我们从资源中获取原始源映射以扩展我们的映射。
// 这确保我们映射到原始源,而不仅仅是前一个转换的输出。
let originalMap = await asset.getMap();
if (originalMap) {
// `extends` 函数使用提供的映射重新映射调用它的映射的原始源位置。
// 在这个例子中,`map` 的原始源位置被重新映射到 `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 }) {
// 从资源获取原始映射。
let originalMap = await asset.getMap();
let compilationResult = someCompiler(await asset.getAST(), {
// 将 originalMap 的 VLQ 编码版本传递给编译器。
originalMap: originalMap.toVLQ(),
});

// 在这种情况下,编译器负责映射到 originalMap 中提供的原始位置,
// 所以我们可以直接将其转换为 Parcel SourceMap 并返回。
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}) {
// 读取捆绑包中每个资源的内容和源映射。
let promises = [];
bundle.traverseAssets(asset => {
promises.push(Promise.all([
asset.getCode(),
asset.getMap()
]);
});

let results = await Promise.all(promises);

// 实例化一个字符串来保存捆绑包内容,
// 以及一个 SourceMap 来保存组合的捆绑包源映射。
let contents = '';
let map = new SourceMap(options.projectRoot);
let lineOffset = 0;

// 添加每个资源的内容。
for (let [code, map] of assets) {
contents += code + '\n';

// 如果资源有源映射,则添加并按捆绑包中已有的行数进行偏移。
if (map) {
map.addSourceMap(map, lineOffset);
}

// 添加此资源的行数。
lineOffset += countLines(code) + 1;
}

// 返回内容和映射。
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 }) {
// 进行 AST 连接并返回编译结果
let compilationResult = concatAndCompile(bundle);

// 创建最终打包的源映射
let map = new SourceMap(options.projectRoot);
if (compilationResult.map) {
map.addVLQMap(compilationResult.map);
}

// 返回编译后的代码和映射
return {
code: compilationResult.code,
map,
};
},
});

优化器中的源映射后处理

#

在优化器中使用源映射与在转换器中使用方式相同。您获取一个输入文件,并期望返回相同的文件,但已优化。

诊断问题

#

如果您遇到不正确的映射并想调试这些问题,我们构建了可以帮助您诊断这些问题的工具。通过运行 @parcel/reporter-sourcemap-visualiser 报告器,Parcel 创建一个 sourcemap-info.json 文件,其中包含所有必要的信息,以可视化所有映射和源内容。

要启用它,请使用 --reporter 选项,或将其添加到您的 `.parcelrc。

parcel build src/index.js --reporter @parcel/reporter-sourcemap-visualiser

在报告器创建 sourcemap-info.json 文件后,您可以将其上传到 sourcemap visualiser

API

#

SourceMap source-map/src/SourceMap.js:8

interface SourceMap {
  constructor(projectRoot: string, buffer?: Buffer): void,

Construct a SourceMap instance

Params:
  • projectRoot: root directory of the project, this is to ensure all source paths are relative to this path
  libraryVersion(): string,
  static generateEmptyMap(v: GenerateEmptyMapOptions): SourceMap,

Generates an empty map from the provided fileName and sourceContent

Params:
  • sourceName: path of the source file
  • sourceContent: content of the source file
  • lineOffset: an offset that gets added to the sourceLine index of each mapping
  addEmptyMap(sourceName: string, sourceContent: string, lineOffset: number): SourceMap,

Generates an empty map from the provided fileName and sourceContent

Params:
  • sourceName: path of the source file
  • sourceContent: content of the source file
  • lineOffset: an offset that gets added to the sourceLine index of each mapping
  addVLQMap(map: VLQMap, lineOffset: number, columnOffset: number): SourceMap,

Appends raw VLQ mappings to the sourcemaps

  addSourceMap(sourcemap: SourceMap, lineOffset: number): SourceMap,

Appends another sourcemap instance to this sourcemap

Params:
  • buffer: the sourcemap buffer that should get appended to this sourcemap
  • lineOffset: an offset that gets added to the sourceLine index of each mapping
  addBuffer(buffer: Buffer, lineOffset: number): SourceMap,

Appends a buffer to this sourcemap Note: The buffer should be generated by this library

Params:
  • buffer: the sourcemap buffer that should get appended to this sourcemap
  • lineOffset: an offset that gets added to the sourceLine index of each mapping
  addIndexedMapping(mapping: IndexedMapping<string>, lineOffset?: number, columnOffset?: number): void,

Appends a Mapping object to this sourcemap Note: line numbers start at 1 due to mozilla's source-map library

Params:
  • mapping: the mapping that should be appended to this sourcemap
  • lineOffset: an offset that gets added to the sourceLine index of each mapping
  • columnOffset: an offset that gets added to the sourceColumn index of each mapping
  _indexedMappingsToInt32Array(mappings: Array<IndexedMapping<string>>, lineOffset?: number, columnOffset?: number): Int32Array,
  addIndexedMappings(mappings: Array<IndexedMapping<string>>, lineOffset?: number, columnOffset?: number): SourceMap,

Appends an array of Mapping objects to this sourcemap This is useful when improving performance if a library provides the non-serialised mappings
Note: This is only faster if they generate the serialised map lazily Note: line numbers start at 1 due to mozilla's source-map library

Params:
  • mappings: an array of mapping objects
  • lineOffset: an offset that gets added to the sourceLine index of each mapping
  • columnOffset: an offset that gets added to the sourceColumn index of each mapping
  addName(name: string): number,

Appends a name to the sourcemap

Params:
  • name: the name that should be appended to the names array
  addNames(names: Array<string>): Array<number>,

Appends an array of names to the sourcemap's names array

Params:
  • names: an array of names to add to the sourcemap
  addSource(source: string): number,

Appends a source to the sourcemap's sources array

Params:
  • source: a filepath that should be appended to the sources array
  addSources(sources: Array<string>): Array<number>,

Appends an array of sources to the sourcemap's sources array

Params:
  • sources: an array of filepaths which should sbe appended to the sources array
  getSourceIndex(source: string): number,

Get the index in the sources array for a certain source file filepath

Params:
  • source: the filepath of the source file
  getSource(index: number): string,

Get the source file filepath for a certain index of the sources array

Params:
  • index: the index of the source in the sources array
  getSources(): Array<string>,

Get a list of all sources

  setSourceContent(sourceName: string, sourceContent: string): void,

Set the sourceContent for a certain file this is optional and is only recommended for files that we cannot read in at the end when we serialise the sourcemap

Params:
  • sourceName: the path of the sourceFile
  • sourceContent: the content of the sourceFile
  getSourceContent(sourceName: string): string | null,

Get the content of a source file if it is inlined as part of the source-map

Params:
  • sourceName: filename
  getSourcesContent(): Array<string | null>,

Get a list of all sources

  getSourcesContentMap(): {
    [key: string]: string | null
  },

Get a map of the source and it's corresponding source content

  getNameIndex(name: string): number,

Get the index in the names array for a certain name

Params:
  • name: the name you want to find the index of
  getName(index: number): string,

Get the name for a certain index of the names array

Params:
  • index: the index of the name in the names array
  getNames(): Array<string>,

Get a list of all names

  getMappings(): Array<IndexedMapping<number>>,

Get a list of all mappings

  indexedMappingToStringMapping(mapping: ?IndexedMapping<number>): ?IndexedMapping<string>,

Convert a Mapping object that uses indexes for name and source to the actual value of name and source
Note: This is only used internally, should not be used externally and will probably eventually get handled directly in C++ for improved performance

Params:
  • index: the Mapping that should get converted to a string-based Mapping
  extends(buffer: Buffer | SourceMap): SourceMap,

Remaps original positions from this map to the ones in the provided map
This works by finding the closest generated mapping in the provided map to original mappings of this map and remapping those to be the original mapping of the provided map.

Params:
  getMap(): ParsedMap,

Returns an object with mappings, sources and names This should only be used for tests, debugging and visualising sourcemaps
Note: This is a fairly slow operation

  findClosestMapping(line: number, column: number): ?IndexedMapping<string>,

Searches through the sourcemap and returns a mapping that is close to the provided generated line and column

Params:
  • line: the line in the generated code (starts at 1)
  • column: the column in the generated code (starts at 0)
  offsetLines(line: number, lineOffset: number): ?IndexedMapping<string>,

Offset mapping lines from a certain position

Params:
  • line: the line in the generated code (starts at 1)
  • lineOffset: the amount of lines to offset mappings by
  offsetColumns(line: number, column: number, columnOffset: number): ?IndexedMapping<string>,

Offset mapping columns from a certain position

Params:
  • line: the line in the generated code (starts at 1)
  • column: the column in the generated code (starts at 0)
  • columnOffset: the amount of columns to offset mappings by
  toBuffer(): Buffer,

Returns a buffer that represents this sourcemap, used for caching

  toVLQ(): VLQMap,

Returns a serialised map using VLQ Mappings

  delete(): void,

A function that has to be called at the end of the SourceMap's lifecycle to ensure all memory and native bindings get de-allocated

  stringify(options: SourceMapStringifyOptions): Promise<string | VLQMap>,

Returns a serialised map

Params:
  • options: options used for formatting the serialised map
}
Referenced by:
BaseAsset, BundleResult, GenerateOutput, MutableAsset, Optimizer, Packager, TransformerResult

MappingPosition source-map/src/types.js:2

type MappingPosition = {|
  line: number,
  column: number,
|}
Referenced by:
IndexedMapping

IndexedMapping source-map/src/types.js:7

type IndexedMapping<T> = {
  generated: MappingPosition,
  original?: MappingPosition,
  source?: T,
  name?: T,
}
Referenced by:
ParsedMap, SourceMap

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

type ParsedMap = {|
  sources: Array<string>,
  names: Array<string>,
  mappings: Array<IndexedMapping<number>>,
  sourcesContent: Array<string | null>,
|}
Referenced by:
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,
}
Referenced by:
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',
}
Referenced by:
SourceMap

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

type GenerateEmptyMapOptions = {
  projectRoot: string,
  sourceName: string,
  sourceContent: string,
  lineOffset?: number,
}
Referenced by:
SourceMap