Skip to content

Commit 9e1ccd9

Browse files
committed
Add loader context to control the sourcemap loading
1 parent b119600 commit 9e1ccd9

File tree

5 files changed

+412
-167
lines changed

5 files changed

+412
-167
lines changed

README.md

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,19 @@ npm install @ampproject/remapping
2121
```typescript
2222
function remapping(
2323
map: SourceMap | SourceMap[],
24-
loader: (file: string) => (SourceMap | null | undefined),
24+
loader: (file: string, ctx: LoaderContext) => (SourceMap | null | undefined),
2525
options?: { excludeContent: boolean, decodedMappings: boolean }
2626
): SourceMap;
27+
28+
// LoaderContext gives the loader the importing sourcemap, and the ability to override the "source"
29+
// location (where nested sources are resolved relative to, and where an original source exists),
30+
// and the ability to override the "content" of an original sourcemap for inclusion in the output
31+
// sourcemap.
32+
type LoaderContext = {
33+
readonly importer: string;
34+
source: string;
35+
content: string | null | undefined;
36+
}
2737
```
2838
2939
`remapping` takes the final output sourcemap, and a `loader` function. For every source file pointer
@@ -55,15 +65,20 @@ const minifiedTransformedMap = JSON.stringify({
5565

5666
const remapped = remapping(
5767
minifiedTransformedMap,
58-
(file) => {
68+
(file, ctx) => {
5969

6070
// The "transformed.js" file is an transformed file.
6171
if (file === 'transformed.js') {
72+
// The root importer is empty.
73+
console.assert(ctx.importer === '');
74+
6275
return transformedMap;
6376
}
6477

6578
// Loader will be called to load transformedMap's source file pointers as well.
6679
console.assert(file === 'helloworld.js');
80+
// `transformed.js`'s sourcemap points into `helloworld.js`.
81+
console.assert(ctx.importer === 'transformed.js');
6782
return null;
6883
}
6984
);
@@ -110,6 +125,76 @@ console.log(remapped);
110125
// };
111126
```
112127

128+
### Advanced control of the loading graph
129+
130+
#### `source`
131+
132+
The `source` property can overridden to any value to change the location of the current load. Eg,
133+
for an original source file, it allows us to change the filepath to the original source regardless
134+
of what the sourcemap source entry says. And for transformed files, it allows us to change the
135+
resolving location for nested sources files of the loaded sourcemap.
136+
137+
```js
138+
const remapped = remapping(
139+
minifiedTransformedMap,
140+
(file, ctx) => {
141+
142+
if (file === 'transformed.js') {
143+
// We pretend the transformed.js file actually exists in the 'src/' directory. When the nested
144+
// source files are loaded, they will now be relative to `src/`.
145+
ctx.source = 'src/transformed.js';
146+
return transformedMap;
147+
}
148+
149+
console.assert(file === 'src/helloworld.js');
150+
// We could futher change the source of this original file, eg, to be inside a nested directory
151+
// itself. This will be reflected in the remapped sourcemap.
152+
ctx.source = 'src/nested/transformed.js';
153+
return null;
154+
}
155+
);
156+
157+
console.log(remapped);
158+
// {
159+
// …,
160+
// sources: ['src/nested/helloworld.js'],
161+
// };
162+
```
163+
164+
165+
#### `content`
166+
167+
The `content` property can be overridden when we encounter an original source file. Eg, this allows
168+
you to manually provide the source content of the file regardless of whether the `sourcesContent`
169+
field is present in the parent sourcemap. Or, it can be set to `null` to remove the source content.
170+
171+
```js
172+
const remapped = remapping(
173+
minifiedTransformedMap,
174+
(file, ctx) => {
175+
176+
if (file === 'transformed.js') {
177+
// transformedMap does not include a `sourcesContent` field, so usually the remapped sourcemap
178+
// would not include any `sourcesContent` values.
179+
return transformedMap;
180+
}
181+
182+
console.assert(file === 'helloworld.js');
183+
// We can read the file to provide the source content.
184+
ctx.content = fs.readFileSync(file, 'utf8');
185+
return null;
186+
}
187+
);
188+
189+
console.log(remapped);
190+
// {
191+
// …,
192+
// sourcesContent: [
193+
// 'console.log("Hello world!")',
194+
// ],
195+
// };
196+
```
197+
113198
### Options
114199

115200
#### excludeContent

src/build-source-map-tree.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { TraceMap } from '@jridgewell/trace-mapping';
33
import OriginalSource from './original-source';
44
import { SourceMapTree } from './source-map-tree';
55

6-
import type { SourceMapInput, SourceMapLoader } from './types';
6+
import type { SourceMapInput, SourceMapLoader, LoaderContext } from './types';
77

88
function asArray<T>(value: T | T[]): T[] {
99
if (Array.isArray(value)) return value;
@@ -25,7 +25,7 @@ export default function buildSourceMapTree(
2525
input: SourceMapInput | SourceMapInput[],
2626
loader: SourceMapLoader
2727
): SourceMapTree {
28-
const maps = asArray(input).map((m) => new TraceMap(m));
28+
const maps = asArray(input).map((m) => new TraceMap(m, ''));
2929
const map = maps.pop()!;
3030

3131
for (let i = 0; i < maps.length; i++) {
@@ -37,35 +37,47 @@ export default function buildSourceMapTree(
3737
}
3838
}
3939

40-
let tree = build(map, loader);
40+
let tree = build(map, '', loader);
4141
for (let i = maps.length - 1; i >= 0; i--) {
4242
tree = new SourceMapTree(maps[i], [tree]);
4343
}
4444
return tree;
4545
}
4646

47-
function build(map: TraceMap, loader: SourceMapLoader): SourceMapTree {
47+
function build(map: TraceMap, importer: string, loader: SourceMapLoader): SourceMapTree {
4848
const { resolvedSources, sourcesContent } = map;
4949

5050
const children = resolvedSources.map(
5151
(sourceFile: string | null, i: number): SourceMapTree | OriginalSource => {
52-
const source = sourceFile || '';
52+
// The loading context gives the loader more information about why this file is being loaded
53+
// (eg, from which importer). It also allows the loader to override the location of the loaded
54+
// sourcemap/original source, or to override the content in the sourcesContent field if it's
55+
// an unmodified source file.
56+
const ctx: LoaderContext = {
57+
importer,
58+
source: sourceFile || '',
59+
content: undefined,
60+
};
61+
5362
// Use the provided loader callback to retrieve the file's sourcemap.
5463
// TODO: We should eventually support async loading of sourcemap files.
55-
const sourceMap = loader(source);
64+
const sourceMap = loader(ctx.source, ctx);
65+
66+
const { source, content } = ctx;
5667

5768
// If there is no sourcemap, then it is an unmodified source file.
5869
if (!sourceMap) {
59-
// The source file's actual contents must be included in the sourcemap
60-
// (done when generating the sourcemap) for it to be included as a
61-
// sourceContent in the output sourcemap.
62-
const sourceContent = sourcesContent ? sourcesContent[i] : null;
70+
// The contents of this unmodified source file can be overridden via the loader context,
71+
// allowing it to be explicitly null or a string. If it remains undefined, we fall back to
72+
// the importing sourcemap's `sourcesContent` field.
73+
const sourceContent =
74+
content !== undefined ? content : sourcesContent ? sourcesContent[i] : null;
6375
return new OriginalSource(source, sourceContent);
6476
}
6577

6678
// Else, it's a real sourcemap, and we need to recurse into it to load its
6779
// source files.
68-
return build(new TraceMap(sourceMap, source), loader);
80+
return build(new TraceMap(sourceMap, source), source, loader);
6981
}
7082
);
7183

src/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,16 @@ export interface SourceMapSegmentObject {
3636

3737
export type SourceMapInput = string | RawSourceMap | DecodedSourceMap;
3838

39-
export type SourceMapLoader = (file: string) => SourceMapInput | null | undefined;
39+
export type LoaderContext = {
40+
readonly importer: string;
41+
source: string;
42+
content: string | null | undefined;
43+
};
44+
45+
export type SourceMapLoader = (
46+
file: string,
47+
ctx: LoaderContext
48+
) => SourceMapInput | null | undefined;
4049

4150
export type Options = {
4251
excludeContent?: boolean;

test/samples/sourceless-transform/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('source-less transform', () => {
3030
const remapped = remapping([minified, original], loader);
3131

3232
expect(loader).toHaveBeenCalledTimes(1);
33-
expect(loader).toHaveBeenCalledWith('source.ts');
33+
expect(loader).toHaveBeenCalledWith('source.ts', expect.anything());
3434
expect(remapped.sources).toHaveLength(0);
3535
expect(remapped.mappings).toBe('');
3636
});

0 commit comments

Comments
 (0)