ソースマップ
Parcel は、プラグインと Parcel のコア全体でソースマップを操作する際のパフォーマンスと信頼性を確保するために、`@parcel/source-map` パッケージを使用してソースマップを処理します。このライブラリは 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,
};
},
});
オプティマイザーでのソースマップの後処理
#オプティマイザーでのソースマップの使用方法は、トランスフォーマーでの使用方法と同じです。1 つのファイルを入力として受け取り、同じファイルを最適化された出力として返す必要があります。
オプティマイザーとの唯一の違いは、以下のコードスニペットに示すように、マップがアセットの一部としてではなく、別のパラメーター/オプションとして提供されることです。いつものように、マップは `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,
別の sourcemap インスタンスをこの sourcemap に追加します
- `buffer`: この sourcemap に追加される sourcemap バッファ
- `lineOffset`: 各マッピングの sourceLine インデックスに追加されるオフセット
addBuffer(buffer: Buffer, lineOffset: number): SourceMap,
バッファをこの sourcemap に追加します 注: バッファはこのライブラリによって生成される必要があります
パラメータ- `buffer`: この sourcemap に追加される sourcemap バッファ
- `lineOffset`: 各マッピングの sourceLine インデックスに追加されるオフセット
addIndexedMapping(mapping: IndexedMapping<string>, lineOffset?: number, columnOffset?: number): void,
Mapping オブジェクトをこの sourcemap に追加します 注: mozilla の source-map ライブラリのため、行番号は 1 から始まります
- `mapping`: この sourcemap に追加されるマッピング
- `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 オブジェクトの配列をこの sourcemap に追加します ライブラリがシリアライズされていないマッピングを提供する場合、パフォーマンスを向上させるのに役立ちます
注: シリアライズされたマップを遅延的に生成する場合にのみ高速になります 注: mozilla の source-map ライブラリのため、行番号は 1 から始まります
- `mappings`: マッピングオブジェクトの配列
- `lineOffset`: 各マッピングの sourceLine インデックスに追加されるオフセット
- `columnOffset`: 各マッピングの sourceColumn インデックスに追加されるオフセット
addName(name: string): number,
sourcemap に名前を追加します
- `name`: names 配列に追加される名前
addNames(names: Array<string>): Array<number>,
名前の配列を sourcemap の names 配列に追加します
- `names`: sourcemap に追加する名前の配列
addSource(source: string): number,
sourcemap の sources 配列にソースを追加します
- `source`: sources 配列に追加されるファイルパス
addSources(sources: Array<string>): Array<number>,
ソースの配列を sourcemap の sources 配列に追加します
- `sources`: sources 配列に追加されるファイルパスの配列
getSourceIndex(source: string): number,
特定のソースファイルのファイルパスに対応する sources 配列のインデックスを取得します
- `source`: ソースファイルのファイルパス
getSource(index: number): string,
sources 配列の特定のインデックスに対応するソースファイルのファイルパスを取得します
- `index`: sources 配列内のソースのインデックス
getSources(): Array<string>,
すべてのソースのリストを取得します
setSourceContent(sourceName: string, sourceContent: string): void,
特定のファイルの sourceContent を設定します これはオプションであり、sourcemap をシリアライズする際に最後に読み取ることができないファイルにのみ推奨されます
- `sourceName`: sourceFile のパス
- `sourceContent`: sourceFile の内容
getSourceContent(sourceName: string): string | null,
ソースファイルの内容が source-map の一部としてインライン化されている場合、その内容を取得します
- `sourceName`: ファイル名
getSourcesContent(): Array<string | null>,
すべてのソースのリストを取得します
getSourcesContentMap(): {
[key: string]: string | null
},
ソースとそれに対応するソースコンテンツのマップを取得します
getNameIndex(name: string): number,
特定の名前に対応する names 配列のインデックスを取得します
- `name`: インデックスを見つけたい名前
getName(index: number): string,
names 配列の特定のインデックスに対応する名前を取得します
- `index`: names 配列内の名前のインデックス
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,
|}
参照元
IndexedMappingIndexedMapping 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,
}