Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
2 changes: 1 addition & 1 deletion LICENSES-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Component,Origin,Licence,Copyright
@jridgewell/set-array,npm,MIT,Justin Ridgewell (https://www.npmjs.com/package/@jridgewell/set-array)
@jridgewell/source-map,npm,MIT,Justin Ridgewell (https://www.npmjs.com/package/@jridgewell/source-map)
@jridgewell/sourcemap-codec,npm,MIT,Justin Ridgewell (https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec)
@jridgewell/trace-mapping,npm,MIT,Justin Ridgewell (https://www.npmjs.com/package/@jridgewell/trace-mapping)
@jridgewell/trace-mapping,npm,MIT,Justin Ridgewell (https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping)
@kwsites/file-exists,npm,MIT,Steve King (https://www.npmjs.com/package/@kwsites/file-exists)
@kwsites/promise-deferred,npm,MIT,Steve King (https://www.npmjs.com/package/@kwsites/promise-deferred)
@module-federation/error-codes,npm,MIT,zhanghang (https://www.npmjs.com/package/@module-federation/error-codes)
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/live-debugger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@
},
"dependencies": {
"@dd/core": "workspace:*",
"@jridgewell/remapping": "2.3.5",
"chalk": "2.3.1"
},
"devDependencies": {
"@babel/parser": "7.24.5",
"@babel/traverse": "7.24.5",
"@babel/types": "7.24.5",
"@jridgewell/trace-mapping": "0.3.31",
"magic-string": "0.30.21",
"typescript": "5.4.3"
},
Expand Down
113 changes: 113 additions & 0 deletions packages/plugins/live-debugger/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,119 @@ describe('getLiveDebuggerPlugin', () => {
});
});

describe('source-map composition', () => {
const LINES_SHIFTED = 4;

const buildShiftedInputMap = (sourcePath: string, source: string): string =>
JSON.stringify({
version: 3,
sources: [sourcePath],
sourcesContent: [source],
names: [],
mappings:
';'.repeat(LINES_SHIFTED) +
source
.split('\n')
.map((_, idx) => (idx === 0 ? 'AAAA' : 'AACA'))
.join(';'),
});

const makeBuildContext = (
inputSourceMap?: string | null,
): UnpluginBuildContext & UnpluginContext => ({
...mockBuildContext,
getNativeBuildContext: () => ({
framework: 'rspack',
compiler: {} as never,
compilation: {} as never,
inputSourceMap,
}),
});

const callHandler = (
ctx: UnpluginBuildContext & UnpluginContext,
code: string,
id: string,
) => {
const pluginContext = getContextMock({
buildRoot: '/',
getLogger: jest.fn(() => mockLog),
});
const plugin = getLiveDebuggerPlugin(
makeOptions({ include: [], exclude: [] }),
pluginContext,
);
const { handler } = getTransformHook(plugin);
const result = handler.call(ctx, code, id);
if (typeof result !== 'object' || result === null || !('code' in result)) {
throw new Error('Unexpected handler result');
}
return result;
};

it('composes its delta map with the previous loader so positions resolve to original-source lines', async () => {
const original = 'function getDebuggerServicesStatus() { return 0; }';
const id = '/src/use-debugger-services.hook.ts';
const postLoader = `// banner\n// banner\n// banner\n// banner\n${original}`;
const inputMap = buildShiftedInputMap(id, original);

const ctx = makeBuildContext(inputMap);
const result = callHandler(ctx, postLoader, id);

expect(result.map).toBeDefined();

const lines = result.code.split('\n');
const entryLineIndex = lines.findIndex((line) => line.includes('$dd_entry($dd_p'));
expect(entryLineIndex).toBeGreaterThan(-1);
const entryColumn = lines[entryLineIndex].indexOf('$dd_entry');

const { originalPositionFor, TraceMap } = await import('@jridgewell/trace-mapping');
const traceMap = new TraceMap(
typeof result.map === 'string' ? result.map : JSON.parse(String(result.map)),
);
const original_pos = originalPositionFor(traceMap, {
line: entryLineIndex + 1,
column: entryColumn,
});

expect(original_pos.line).toBe(1);
expect(original_pos.source).toBe(id);
});

it('returns the magic-string map verbatim when the previous loader did not provide one', async () => {
const id = '/src/utils.ts';
const code = 'function f() { return 1; }';

// No inputSourceMap, no getNativeBuildContext at all.
const result = callHandler(mockBuildContext, code, id);
expect(result.map).toBeDefined();

const map = JSON.parse(String(result.map));
expect(map.sources).toContain(id);
});

it('returns no map when the file has no instrumentable functions', () => {
const result = callHandler(mockBuildContext, 'const x = 42;', '/src/utils.ts');
expect(result.map).toBeUndefined();
});

it('falls back to the un-composed map and logs an error when composition throws', () => {
const id = '/src/utils.ts';
const code = 'function f() { return 1; }';

const ctx = makeBuildContext('not a valid sourcemap, this should throw');
const result = callHandler(ctx, code, id);

expect(result.map).toBeDefined();
expect(() => JSON.parse(String(result.map))).not.toThrow();

expect(mockLog.error).toHaveBeenCalledWith(
expect.stringContaining('Failed to compose source map'),
expect.objectContaining({ forward: true }),
);
});
});

describe('error handling', () => {
it('should return original code when transformCode throws', () => {
jest.isolateModules(() => {
Expand Down
40 changes: 39 additions & 1 deletion packages/plugins/live-debugger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

import type { GetPlugins, GlobalContext, PluginOptions } from '@dd/core/types';
import { InjectPosition } from '@dd/core/types';
import remapping from '@jridgewell/remapping';
import type { SourceMapInput } from '@jridgewell/remapping';
import type { SourceMap } from 'magic-string';
import type { SourceMapCompact, UnpluginBuildContext } from 'unplugin';

import { CONFIG_KEY, PLUGIN_NAME } from './constants';
import { getRuntimeBootstrap } from './runtime-bootstrap';
Expand Down Expand Up @@ -103,9 +107,15 @@ export const getLiveDebuggerPlugin = (

transformedFileCount++;

const inputMap = getInputSourceMap(this);
const composedMap =
result.map && inputMap
? composeWithInputMap(result.map, inputMap, id, log)
: result.map;

return {
code: result.code,
map: result.map,
map: composedMap,
};
} catch (e) {
log.error(`Instrumentation Error in ${id}: ${e}`, { forward: true });
Expand Down Expand Up @@ -158,3 +168,31 @@ export const getPlugins: GetPlugins = ({ options, context }) => {

return [getLiveDebuggerPlugin(validatedOptions, context)];
};

/**
* Return the source map produced by the previous loader, if any.
*/
function getInputSourceMap(ctx: UnpluginBuildContext): SourceMapInput | undefined {
const native = ctx.getNativeBuildContext?.();
return (native as { inputSourceMap?: SourceMapInput })?.inputSourceMap;
}

/**
* Compose a local source map with the previous loader's source map. The result maps instrumented
* output directly back to original source coordinates.
*/
function composeWithInputMap(
instrumentMap: SourceMap,
inputMap: SourceMapInput,
id: string,
log: ReturnType<GlobalContext['getLogger']>,
): SourceMapCompact | SourceMap {
try {
return remapping(instrumentMap as unknown as SourceMapInput, (_file, ctx) =>
ctx.depth === 1 ? inputMap : null,
) as unknown as SourceMapCompact;
} catch (e) {
log.error(`Failed to compose source map for ${id}: ${e}`, { forward: true });
return instrumentMap;
}
}
Loading
Loading