原始碼地圖
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 Buffer (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,
將另一個原始碼對應執行個體新增至這個原始碼對應
緩衝區
:應附加到此原始碼對應的原始碼對應緩衝區lineOffset
:一個會新增至每個對應的 sourceLine 索引的偏移量
addBuffer(buffer: Buffer, lineOffset: number): SourceMap,
將緩衝區附加到此原始碼對應。注意:緩衝區應由這個函式庫產生
參數緩衝區
:應附加到此原始碼對應的原始碼對應緩衝區lineOffset
:一個會新增至每個對應的 sourceLine 索引的偏移量
addIndexedMapping(mapping: IndexedMapping<string>, lineOffset?: number, columnOffset?: number): void,
將對應物件附加到此原始碼對應。注意:行號從 1 開始,這是因為 mozilla 的原始碼對應函式庫
對應
:應附加到此原始碼對應的對應lineOffset
:一個會新增至每個對應的 sourceLine 索引的偏移量欄位偏移
:會新增到每個對應的 sourceColumn 索引的偏移
_indexedMappingsToInt32Array(mappings: Array<IndexedMapping<string>>, lineOffset?: number, columnOffset?: number): Int32Array,
addIndexedMappings(mappings: Array<IndexedMapping<string>>, lineOffset?: number, columnOffset?: number): SourceMap,
將對應物件陣列附加到此原始碼對應。如果函式庫提供非序列化的對應,這在提升效能時很有用
注意:只有在他們延遲產生序列化的對應時,這才會比較快。注意:行號從 1 開始,這是因為 mozilla 的原始碼對應函式庫
對應
:對應物件陣列lineOffset
:一個會新增至每個對應的 sourceLine 索引的偏移量欄位偏移
:會新增到每個對應的 sourceColumn 索引的偏移
addName(name: string): number,
將名稱附加到原始碼對應
名稱
:應附加到名稱陣列的名稱
addNames(names: Array<string>): Array<number>,
將名稱陣列附加到原始碼對應的名稱陣列
名稱
:要新增到原始碼對應的名稱陣列
addSource(source: string): number,
將來源附加到原始碼對應的來源陣列
來源
:應附加到來源陣列的檔案路徑
addSources(sources: Array<string>): Array<number>,
將來源陣列附加到原始碼對應的來源陣列
來源
:應附加到來源陣列的檔案路徑陣列
getSourceIndex(source: string): number,
取得來源陣列中特定來源檔案檔案路徑的索引
來源
:來源檔案的檔案路徑
getSource(index: number): string,
取得來源陣列特定索引的來源檔案檔案路徑
索引
:來源在來源陣列中的索引
getSources(): Array<string>,
取得所有來源的清單
setSourceContent(sourceName: string, sourceContent: string): void,
設定特定檔案的 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>,
將使用索引作為名稱和來源的對應物件轉換為名稱和來源的實際值
注意:這僅在內部使用,不應在外部使用,而且最終可能會直接在 C++ 中處理以提升效能
index
:應轉換為基於字串的對應的對應
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,
傳回一個表示這個 sourcemap 的緩衝區,用於快取
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,
}