Skip to content

Commit 2bfebf1

Browse files
committed
feat: compose tree-shaking source maps
1 parent 2dac52d commit 2bfebf1

5 files changed

Lines changed: 126 additions & 6 deletions

File tree

packages/tree-shaking-plugin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
}
7474
},
7575
"devDependencies": {
76+
"@jridgewell/trace-mapping": "^0.3.31",
7677
"@openapi-qraft/eslint-config": "workspace:*",
7778
"@openapi-qraft/rollup-config": "workspace:*",
7879
"@qraft/test-utils": "workspace:*",

packages/tree-shaking-plugin/src/core.test.ts

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import '@qraft/test-utils/vitestFsMock';
22
import fs from 'node:fs/promises';
33
import os from 'node:os';
44
import path from 'node:path';
5+
import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping';
6+
import type { SourceMapInput } from '@jridgewell/trace-mapping';
57
import { describe, expect, it } from 'vitest';
68
import { transformQraftTreeShaking as transformQraftTreeShakingImpl } from './core.js';
79
import { createTransformPlan } from './lib/transform/plan.js';
@@ -59,11 +61,29 @@ export const createAPIClientOptions = () => ({
5961
`;
6062

6163
type TransformOptions = Parameters<typeof transformQraftTreeShakingImpl>[2];
64+
type TransformWithInputSourceMap = (
65+
code: string,
66+
id: string,
67+
options: TransformOptions,
68+
resolver: Parameters<typeof transformQraftTreeShakingImpl>[3],
69+
inputSourceMap?: SourceMapInput
70+
) => ReturnType<typeof transformQraftTreeShakingImpl>;
71+
72+
const transformQraftTreeShakingImplWithInputSourceMap = transformQraftTreeShakingImpl satisfies (
73+
code: string,
74+
id: string,
75+
options: TransformOptions,
76+
resolver: Parameters<typeof transformQraftTreeShakingImpl>[3]
77+
) => ReturnType<typeof transformQraftTreeShakingImpl>;
78+
79+
const transformQraftTreeShakingWithInputSourceMap =
80+
transformQraftTreeShakingImplWithInputSourceMap as unknown as TransformWithInputSourceMap;
6281

6382
async function transformQraftTreeShaking(
6483
code: string,
6584
id: string,
66-
options: TransformOptions
85+
options: TransformOptions,
86+
inputSourceMap?: SourceMapInput
6787
) {
6888
const fixtureRoot = path.dirname(path.dirname(id));
6989
const fixtureResolver = createFixtureResolver(fixtureRoot);
@@ -80,7 +100,13 @@ async function transformQraftTreeShaking(
80100
return fixtureResolver(specifier, importer);
81101
};
82102

83-
return transformQraftTreeShakingImpl(code, id, options, resolver);
103+
return transformQraftTreeShakingWithInputSourceMap(
104+
code,
105+
id,
106+
options,
107+
resolver,
108+
inputSourceMap
109+
);
84110
}
85111

86112
describe('transformQraftTreeShaking', () => {
@@ -141,6 +167,62 @@ export function App() {
141167
`);
142168
});
143169

170+
it('keeps a rewritten user call site traceable through an incoming source map', async () => {
171+
const fixture = await createFixture();
172+
const generatedSourceFile = path.join(fixture, 'src/App.generated.tsx');
173+
const originalSourceFile = path.join(fixture, 'src/App.tsx');
174+
const code = [
175+
"import { createAPIClient } from './api';",
176+
'',
177+
'const api = createAPIClient();',
178+
'',
179+
'export function App() {',
180+
' return api.pets.getPets.useQuery();',
181+
'}',
182+
].join('\n');
183+
const inputSourceMap = createIdentitySourceMap(
184+
generatedSourceFile,
185+
originalSourceFile,
186+
code
187+
);
188+
189+
const result = await transformQraftTreeShaking(
190+
code,
191+
generatedSourceFile,
192+
{ createAPIClientFn: [{ name: 'createAPIClient', module: './api' }] },
193+
inputSourceMap
194+
);
195+
196+
if (!result) {
197+
throw new Error('Expected transform result');
198+
}
199+
200+
const generatedLineIndex = result.code
201+
.split('\n')
202+
.findIndex((line) => line.includes('api_pets_getPets.useQuery()'));
203+
204+
if (generatedLineIndex === -1) {
205+
throw new Error('Expected rewritten user call site in generated output');
206+
}
207+
208+
const generatedLine = generatedLineIndex + 1;
209+
const generatedColumn = result.code
210+
.split('\n')
211+
[generatedLineIndex].indexOf('api_pets_getPets');
212+
213+
const traceMapInput = result.map! as SourceMapInput;
214+
215+
const position = originalPositionFor(new TraceMap(traceMapInput), {
216+
line: generatedLine,
217+
column: generatedColumn,
218+
});
219+
220+
expect(position).toMatchObject({
221+
source: originalSourceFile,
222+
line: 6,
223+
});
224+
});
225+
144226
it('aliases an imported operation when a local binding uses the same name', async () => {
145227
const fixture = await createFixture();
146228
const sourceFile = path.join(fixture, 'src/App.tsx');
@@ -1582,6 +1664,26 @@ function createFixtureResolver(fixtureRoot: string) {
15821664
};
15831665
}
15841666

1667+
function createIdentitySourceMap(
1668+
generatedSourceFile: string,
1669+
originalSourceFile: string,
1670+
source: string
1671+
): SourceMapInput {
1672+
const lineCount = source.split('\n').length;
1673+
const mappings = Array.from({ length: lineCount }, (_, index) =>
1674+
index === 0 ? 'AAAA' : 'AACA'
1675+
).join(';');
1676+
1677+
return {
1678+
version: 3,
1679+
file: generatedSourceFile,
1680+
names: [],
1681+
sources: [originalSourceFile],
1682+
sourcesContent: [source],
1683+
mappings,
1684+
};
1685+
}
1686+
15851687
async function resolveFixtureModule(baseDir: string, importPath: string) {
15861688
const base = path.resolve(baseDir, importPath);
15871689
const candidateBases = new Set([base]);

packages/tree-shaking-plugin/src/core.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { GeneratorOptions as BabelGeneratorOptions } from '@babel/generator';
2+
// eslint-disable-next-line import-x/no-extraneous-dependencies
3+
import type { SourceMapInput } from '@jridgewell/trace-mapping';
14
import type { QraftResolver } from './lib/resolvers/common.js';
25
import * as generateModule from '@babel/generator';
36
import { createAgnosticResolver } from './lib/resolvers/agnostic.js';
@@ -34,14 +37,18 @@ export type QraftTreeShakeOptions = {
3437
};
3538

3639
type GenerateFn = (typeof import('@babel/generator'))['default'];
40+
type GeneratorOptions = Omit<BabelGeneratorOptions, 'inputSourceMap'> & {
41+
inputSourceMap?: SourceMapInput;
42+
};
3743

3844
const generate = resolveDefaultExport<GenerateFn>(generateModule);
3945

4046
export async function transformQraftTreeShaking(
4147
code: string,
4248
id: string,
4349
options: QraftTreeShakeOptions,
44-
resolver: QraftResolver = createAgnosticResolver(options.resolve)
50+
resolver: QraftResolver = createAgnosticResolver(options.resolve),
51+
inputSourceMap?: SourceMapInput
4552
) {
4653
if (!shouldTransformId(id, options)) return null;
4754

@@ -56,11 +63,14 @@ export async function transformQraftTreeShaking(
5663

5764
applyTransformPlan(plan, plan.runtimeLocalNames);
5865

59-
const result = generate(plan.ast, {
66+
const generatorOptions = {
6067
sourceMaps: true,
6168
sourceFileName: id,
6269
jsescOption: { minimal: true },
63-
});
70+
inputSourceMap,
71+
} satisfies GeneratorOptions;
72+
73+
const result = generate(plan.ast, generatorOptions);
6474

6575
return {
6676
code: result.code,

packages/tree-shaking-plugin/src/lib/plugin/create-qraft-tree-shake-plugin.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ export function createQraftTreeShakePlugin<TRuntimeContext = unknown>(
2323
},
2424
handler(this: any, code, id) {
2525
const resolver = createResolver(this, options.resolve);
26-
return transformQraftTreeShaking(code, id, options, resolver);
26+
return transformQraftTreeShaking(
27+
code,
28+
id,
29+
options,
30+
resolver,
31+
this.inputSourceMap
32+
);
2733
},
2834
},
2935
});

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4484,6 +4484,7 @@ __metadata:
44844484
"@babel/parser": "npm:^7.29.0"
44854485
"@babel/traverse": "npm:^7.29.0"
44864486
"@babel/types": "npm:^7.29.0"
4487+
"@jridgewell/trace-mapping": "npm:^0.3.31"
44874488
"@openapi-qraft/eslint-config": "workspace:*"
44884489
"@openapi-qraft/rollup-config": "workspace:*"
44894490
"@rspack/resolver": "npm:^0.4.0"

0 commit comments

Comments
 (0)