Skip to content

Commit

Permalink
Provide tree depth on the LoaderContext while loading sourcemaps
Browse files Browse the repository at this point in the history
It's important to know both the importer and the depth of the sourcemap tree when loading, to
solve cases like babel/babel#14274. Here, a parent sourcemap contains a
source location that, when resolved, has the same location as the parent. This easily happens when
the file represents an in-place transformation of the original sourcemap (eg, `babel script.js
--out-file script.js --source-maps`). Because they exist in the same location, it's extremely easy
to create a loader which continues to return the transformed sourcemap when we intend to only return
a `null` (no sourcemap) for the original source file.
  • Loading branch information
jridgewell committed Feb 16, 2022
1 parent 13c136f commit 43f275f
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 23 deletions.
47 changes: 27 additions & 20 deletions README.md
Expand Up @@ -2,9 +2,9 @@

> Remap sequential sourcemaps through transformations to point at the original source code
Remapping allows you to take the sourcemaps generated through transforming your code and
"remap" them to the original source locations. Think "my minified code, transformed with babel and
bundled with webpack", all pointing to the correct location in your original source code.
Remapping allows you to take the sourcemaps generated through transforming your code and "remap"
them to the original source locations. Think "my minified code, transformed with babel and bundled
with webpack", all pointing to the correct location in your original source code.

With remapping, none of your source code transformations need to be aware of the input's sourcemap,
they only need to generate an output sourcemap. This greatly simplifies building custom
Expand All @@ -25,12 +25,13 @@ function remapping(
options?: { excludeContent: boolean, decodedMappings: boolean }
): SourceMap;

// LoaderContext gives the loader the importing sourcemap, and the ability to override the "source"
// location (where nested sources are resolved relative to, and where an original source exists),
// and the ability to override the "content" of an original sourcemap for inclusion in the output
// sourcemap.
// LoaderContext gives the loader the importing sourcemap, tree depth, the ability to override the
// "source" location (where child sources are resolved relative to, or the location of original
// source), and the ability to override the "content" of an original source for inclusion in the
// output sourcemap.
type LoaderContext = {
readonly importer: string;
readonly depth: number;
source: string;
content: string | null | undefined;
}
Expand Down Expand Up @@ -71,6 +72,9 @@ const remapped = remapping(
if (file === 'transformed.js') {
// The root importer is empty.
console.assert(ctx.importer === '');
// The depth in the sourcemap tree we're currently loading.
// The root `minifiedTransformedMap` is depth 0, and its source children are depth 1, etc.
console.assert(ctx.depth === 1);

return transformedMap;
}
Expand All @@ -79,6 +83,8 @@ const remapped = remapping(
console.assert(file === 'helloworld.js');
// `transformed.js`'s sourcemap points into `helloworld.js`.
console.assert(ctx.importer === 'transformed.js');
// This is a source child of `transformed`, which is a source child of `minifiedTransformedMap`.
console.assert(ctx.depth === 2);
return null;
}
);
Expand Down Expand Up @@ -107,8 +113,9 @@ column of the 2nd line of the file `helloworld.js`".
### Multiple transformations of a file

As a convenience, if you have multiple single-source transformations of a file, you may pass an
array of sourcemap files in the order of most-recent transformation sourcemap first. So our above
example could have been writen as:
array of sourcemap files in the order of most-recent transformation sourcemap first. Note that this
changes the `importer` and `depth` of each call to our loader. So our above example could have been
written as:

```js
const remapped = remapping(
Expand All @@ -130,9 +137,9 @@ console.log(remapped);
#### `source`

The `source` property can overridden to any value to change the location of the current load. Eg,
for an original source file, it allows us to change the filepath to the original source regardless
for an original source file, it allows us to change the location to the original source regardless
of what the sourcemap source entry says. And for transformed files, it allows us to change the
resolving location for nested sources files of the loaded sourcemap.
relative resolving location for child sources of the loaded sourcemap.

```js
const remapped = remapping(
Expand Down Expand Up @@ -165,8 +172,9 @@ console.log(remapped);
#### `content`

The `content` property can be overridden when we encounter an original source file. Eg, this allows
you to manually provide the source content of the file regardless of whether the `sourcesContent`
field is present in the parent sourcemap. Or, it can be set to `null` to remove the source content.
you to manually provide the source content of the original file regardless of whether the
`sourcesContent` field is present in the parent sourcemap. It can also be set to `null` to remove
the source content.

```js
const remapped = remapping(
Expand Down Expand Up @@ -199,13 +207,12 @@ console.log(remapped);

#### excludeContent

By default, `excludeContent` is `false`. Passing `{ excludeContent: true }`
will exclude the `sourcesContent` field from the returned sourcemap. This is
mainly useful when you want to reduce the size out the sourcemap.
By default, `excludeContent` is `false`. Passing `{ excludeContent: true }` will exclude the
`sourcesContent` field from the returned sourcemap. This is mainly useful when you want to reduce
the size out the sourcemap.

#### decodedMappings

By default, `decodedMappings` is `false`. Passing `{ decodedMappings: true }`
will leave the `mappings` field in a [decoded
state](https://github.com/rich-harris/sourcemap-codec) instead of encoding
into a VLQ string.
By default, `decodedMappings` is `false`. Passing `{ decodedMappings: true }` will leave the
`mappings` field in a [decoded state](https://github.com/rich-harris/sourcemap-codec) instead of
encoding into a VLQ string.
13 changes: 10 additions & 3 deletions src/build-source-map-tree.ts
Expand Up @@ -37,16 +37,22 @@ export default function buildSourceMapTree(
}
}

let tree = build(map, '', loader);
let tree = build(map, loader, '', 0);
for (let i = maps.length - 1; i >= 0; i--) {
tree = new SourceMapTree(maps[i], [tree]);
}
return tree;
}

function build(map: TraceMap, importer: string, loader: SourceMapLoader): SourceMapTree {
function build(
map: TraceMap,
loader: SourceMapLoader,
importer: string,
importerDepth: number
): SourceMapTree {
const { resolvedSources, sourcesContent } = map;

const depth = importerDepth + 1;
const children = resolvedSources.map(
(sourceFile: string | null, i: number): SourceMapTree | OriginalSource => {
// The loading context gives the loader more information about why this file is being loaded
Expand All @@ -55,6 +61,7 @@ function build(map: TraceMap, importer: string, loader: SourceMapLoader): Source
// an unmodified source file.
const ctx: LoaderContext = {
importer,
depth,
source: sourceFile || '',
content: undefined,
};
Expand All @@ -77,7 +84,7 @@ function build(map: TraceMap, importer: string, loader: SourceMapLoader): Source

// Else, it's a real sourcemap, and we need to recurse into it to load its
// source files.
return build(new TraceMap(sourceMap, source), source, loader);
return build(new TraceMap(sourceMap, source), loader, source, depth);
}
);

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Expand Up @@ -38,6 +38,7 @@ export type SourceMapInput = string | RawSourceMap | DecodedSourceMap;

export type LoaderContext = {
readonly importer: string;
readonly depth: number;
source: string;
content: string | null | undefined;
};
Expand Down
62 changes: 62 additions & 0 deletions test/unit/build-source-map-tree.ts
Expand Up @@ -199,6 +199,68 @@ describe('buildSourceMapTree', () => {
});
});

describe('depty', () => {
test('is 1 for sources loaded from the root', () => {
const loader = jest.fn();
buildSourceMapTree(
{
...decodedMap,
sources: ['first.js', 'second.js'],
},
loader
);

expect(loader).toHaveBeenCalledTimes(2);
expect(loader).toHaveBeenCalledWith(
'first.js',
expect.objectContaining({
depth: 1,
})
);
expect(loader).toHaveBeenCalledWith(
'second.js',
expect.objectContaining({
depth: 1,
})
);
});

test('is increased for nested sources', () => {
const loader = jest.fn();
loader.mockReturnValueOnce({
...rawMap,
sources: ['two.js'],
});
buildSourceMapTree(
{
...decodedMap,
sources: ['first.js', 'second.js'],
},
loader
);

expect(loader).toHaveBeenCalledTimes(3);
expect(loader).toHaveBeenCalledWith(
'first.js',
expect.objectContaining({
depth: 1,
})
);
expect(loader).toHaveBeenCalledWith(
'two.js',
expect.objectContaining({
depth: 2,
})
);
expect(loader).toHaveBeenCalledWith(
'second.js',
expect.objectContaining({
depth: 1,
})
);
});
});

describe('source', () => {
test('matches the loader source param', () => {
const loader = jest.fn();
Expand Down

0 comments on commit 43f275f

Please sign in to comment.