Skip to content

Commit 7b4b0b0

Browse files
timfishLuca Forstner
andauthored
feat: Add module metadata injection for esbuild (#381)
Co-authored-by: Luca Forstner <luca.forstner@sentry.io>
1 parent a1d0e0e commit 7b4b0b0

File tree

4 files changed

+88
-4
lines changed

4 files changed

+88
-4
lines changed

packages/bundler-plugin-core/src/types.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,6 @@ export interface Options {
251251
* - `org`: The organization slug.
252252
* - `project`: The project slug.
253253
* - `release`: The release name.
254-
*
255-
*
256-
* Note: This option is currently only supported by `@sentry/webpack-plugin`.
257254
*/
258255
// eslint-disable-next-line @typescript-eslint/no-explicit-any
259256
moduleMetadata?: any | ModuleMetadataCallback;

packages/esbuild-plugin/src/index.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,88 @@ function esbuildDebugIdInjectionPlugin(): UnpluginOptions {
115115
};
116116
}
117117

118+
function esbuildModuleMetadataInjectionPlugin(injectionCode: string): UnpluginOptions {
119+
const pluginName = "sentry-esbuild-module-metadata-injection-plugin";
120+
const stubNamespace = "sentry-module-metadata-stub";
121+
122+
return {
123+
name: pluginName,
124+
125+
esbuild: {
126+
setup({ onLoad, onResolve }) {
127+
onResolve({ filter: /.*/ }, (args) => {
128+
if (args.kind !== "entry-point") {
129+
return;
130+
} else {
131+
return {
132+
pluginName,
133+
// needs to be an abs path, otherwise esbuild will complain
134+
path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path),
135+
pluginData: {
136+
isMetadataProxyResolver: true,
137+
originalPath: args.path,
138+
originalResolveDir: args.resolveDir,
139+
},
140+
// We need to add a suffix here, otherwise esbuild will mark the entrypoint as resolved and won't traverse
141+
// the module tree any further down past the proxy module because we're essentially creating a dependency
142+
// loop back to the proxy module.
143+
// By setting a suffix we're telling esbuild that the entrypoint and proxy module are two different things,
144+
// making it re-resolve the entrypoint when it is imported from the proxy module.
145+
// Super confusing? Yes. Works? Apparently... Let's see.
146+
suffix: "?sentryMetadataProxyModule=true",
147+
};
148+
}
149+
});
150+
151+
onLoad({ filter: /.*/ }, (args) => {
152+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
153+
if (!(args.pluginData?.isMetadataProxyResolver as undefined | boolean)) {
154+
return null;
155+
}
156+
157+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
158+
const originalPath = args.pluginData.originalPath as string;
159+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
160+
const originalResolveDir = args.pluginData.originalResolveDir as string;
161+
162+
return {
163+
loader: "js",
164+
pluginName,
165+
// We need to use JSON.stringify below so that any escape backslashes stay escape backslashes, in order not to break paths on windows
166+
contents: `
167+
import "_sentry-module-metadata-injection-stub";
168+
import * as OriginalModule from ${JSON.stringify(originalPath)};
169+
export default OriginalModule.default;
170+
export * from ${JSON.stringify(originalPath)};`,
171+
resolveDir: originalResolveDir,
172+
};
173+
});
174+
175+
onResolve({ filter: /_sentry-module-metadata-injection-stub/ }, (args) => {
176+
return {
177+
path: args.path,
178+
sideEffects: true,
179+
pluginName,
180+
namespace: stubNamespace,
181+
suffix: "?sentry-module-id=" + uuidv4(), // create different module, each time this is resolved
182+
};
183+
});
184+
185+
onLoad(
186+
{ filter: /_sentry-module-metadata-injection-stub/, namespace: stubNamespace },
187+
() => {
188+
return {
189+
loader: "js",
190+
pluginName,
191+
contents: injectionCode,
192+
};
193+
}
194+
);
195+
},
196+
},
197+
};
198+
}
199+
118200
function esbuildDebugIdUploadPlugin(
119201
upload: (buildArtifacts: string[]) => Promise<void>
120202
): UnpluginOptions {
@@ -135,6 +217,7 @@ function esbuildDebugIdUploadPlugin(
135217
const sentryUnplugin = sentryUnpluginFactory({
136218
releaseInjectionPlugin: esbuildReleaseInjectionPlugin,
137219
debugIdInjectionPlugin: esbuildDebugIdInjectionPlugin,
220+
moduleMetadataInjectionPlugin: esbuildModuleMetadataInjectionPlugin,
138221
debugIdUploadPlugin: esbuildDebugIdUploadPlugin,
139222
});
140223

packages/integration-tests/fixtures/metadata-injection/metadata-injection.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ describe("metadata injection", () => {
2424
checkBundle(path.join(__dirname, "out", "webpack5", "bundle.js"));
2525
});
2626

27+
test("esbuild bundle", () => {
28+
checkBundle(path.join(__dirname, "out", "esbuild", "bundle.js"));
29+
});
30+
2731
test("rollup bundle", () => {
2832
checkBundle(path.join(__dirname, "out", "rollup", "bundle.js"));
2933
});

packages/integration-tests/fixtures/metadata-injection/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ createCjsBundles(
1313
moduleMetadata: { team: "frontend" },
1414
},
1515
},
16-
["webpack4", "webpack5", "rollup", "vite"]
16+
["webpack4", "webpack5", "esbuild", "rollup", "vite"]
1717
);

0 commit comments

Comments
 (0)